Skip to content

Commit 1c0e07c

Browse files
authored
fix(Tabs): Add resize observer for dynamic tab selection adjustments (#909)
* fix(Tabs): Add resize observer for dynamic tab selection adjustments * review: Optimize tab list resize observer for better performance (with RAF)
1 parent 339946c commit 1c0e07c

File tree

2 files changed

+61
-10
lines changed

2 files changed

+61
-10
lines changed

scripts/jestSetup.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,15 @@ import { toHaveNoViolations } from 'jest-axe'
22
import '@testing-library/jest-dom'
33

44
expect.extend(toHaveNoViolations)
5+
6+
/* Stub out ResizeObserver */
7+
if (!window.ResizeObserver) {
8+
class ResizeObserver {
9+
observe() {}
10+
unobserve() {}
11+
disconnect() {}
12+
}
13+
14+
window.ResizeObserver = ResizeObserver
15+
global.ResizeObserver = ResizeObserver
16+
}

src/tabs/tabs.tsx

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -180,16 +180,18 @@ function TabList({
180180
exceptionallySetClassName,
181181
...props
182182
}: TabListProps): React.ReactElement | null {
183+
const tabListRef = React.useRef<HTMLDivElement | null>(null)
184+
const tabListPrevWidthRef = React.useRef(0)
185+
183186
const tabContextValue = React.useContext(TabsContext)
184187

185188
const [selectedTabElement, setSelectedTabElement] = React.useState<HTMLElement | null>(null)
186189
const [selectedTabStyle, setSelectedTabStyle] = React.useState<React.CSSProperties>({})
187-
const tabListRef = React.useRef<HTMLDivElement>(null)
188190

189191
const selectedId = tabContextValue?.tabStore.useState('selectedId')
190192

191-
React.useLayoutEffect(() => {
192-
function updateSelectedTabStyle() {
193+
const updateSelectedTabPosition = React.useCallback(
194+
function updateSelectedTabPositionCallback() {
193195
if (!selectedId || !tabListRef.current) {
194196
return
195197
}
@@ -207,16 +209,53 @@ function TabList({
207209
width: `${selectedTab.offsetWidth}px`,
208210
})
209211
}
210-
}
212+
},
213+
[selectedId],
214+
)
215+
216+
React.useEffect(
217+
function updateSelectedTabPositionOnTabChange() {
218+
updateSelectedTabPosition()
219+
},
220+
// `selectedId` is a dependency to ensure the effect runs when the selected tab changes
221+
[selectedId, updateSelectedTabPosition],
222+
)
223+
224+
React.useEffect(
225+
function observeTabListWidthChange() {
226+
let animationFrameId: number | null = null
227+
228+
const tabListObserver = new ResizeObserver(([entry]) => {
229+
const width = entry?.contentRect.width
211230

212-
updateSelectedTabStyle()
231+
if (width && tabListPrevWidthRef.current !== width) {
232+
tabListPrevWidthRef.current = width
213233

214-
window.addEventListener('resize', updateSelectedTabStyle)
234+
if (animationFrameId !== null) {
235+
cancelAnimationFrame(animationFrameId)
236+
}
215237

216-
return function cleanupEventListener() {
217-
window.removeEventListener('resize', updateSelectedTabStyle)
218-
}
219-
}, [selectedId])
238+
animationFrameId = requestAnimationFrame(() => {
239+
updateSelectedTabPosition()
240+
animationFrameId = null
241+
})
242+
}
243+
})
244+
245+
if (tabListRef.current) {
246+
tabListObserver.observe(tabListRef.current)
247+
}
248+
249+
return function cleanupResizeObserver() {
250+
if (animationFrameId) {
251+
cancelAnimationFrame(animationFrameId)
252+
}
253+
254+
tabListObserver.disconnect()
255+
}
256+
},
257+
[updateSelectedTabPosition],
258+
)
220259

221260
if (!tabContextValue) {
222261
return null

0 commit comments

Comments
 (0)