Bug - Tabs - Tabs overflow does not respond to container resize (only listens to window.resize)
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 3/5
- Temps estimé
- 1-2 jours
- Accessibilité débutants
- 65/100
- Type d'issue
- Bug
- Clarté
- Clairement spécifiée
- Activité
- Calme
- Stack technique
- react, typescript
- Domaine
- frontend
Piste de recherche
Commencez dans Tabs.tsx en lisant componentDidMount, componentWillUnmount, handleScrollButtons et componentDidUpdate, puis suivez le comportement existant de redimensionnement de window et de debounce. Reproduisez le problème dans un Drawer redimensionnable ou un autre conteneur qui change, et vérifiez que les contrôles de débordement et le compte More se mettent à jour sans réagir de manière répétée aux changements qui concernent uniquement la hauteur.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
Description
The Tabs component only recalculates its overflow state (scroll buttons and "More" dropdown count) in response to window.resize events. It does not observe its own container for size changes. This means that when Tabs are placed inside any dynamically-sized container — such as a resizable Drawer, a CSS Grid/Flexbox layout that redistributes space, or a collapsible sidebar — the overflow state becomes stale:
-
Scroll buttons appear when all tabs fit, or disappear when tabs overflow
-
The "More (N)" dropdown count does not update
-
The active tab accent bar drifts out of position
Root Cause
In Tabs.tsx, componentDidMount (line 381) registers only:
window.addEventListener('resize', this.handleResize, false);
There is no ResizeObserver on this.tabList.current (the <ul> element), so container-level width changes are invisible to the component.
Additionally, handleScrollButtons() (line 258) uses a 100ms debounce and componentDidUpdate (line 429) accumulates overflowingTabCount rather than recalculating from scratch, which can compound errors during rapid resizes.
Steps to Reproduce
- Place a
<Tabs>component with 8+ tabs inside a<DrawerPanelContent isResizable> - Open the drawer — tabs may display correctly initially
- Drag the drawer splitter to make the panel narrower
- Expected: Scroll buttons appear / "More" count increases as tabs overflow
- Actual: Overflow state is unchanged until the browser window itself is resized
This also reproduces with any non-window container resize (e.g., a parent element resized via JavaScript, CSS transitions, or layout shifts).
Proposed Fix
Add a ResizeObserver on this.tabList.current in componentDidMount, alongside the existing window.resize listener. The observer should filter by width changes only — when scroll buttons render/hide, they change the container height, which would re-trigger the observer and create a feedback loop.
// New instance property
private resizeObserver: ResizeObserver | null = null;
private lastObservedWidth: number = 0;
componentDidMount() {
if (!this.props.isVertical) {
if (canUseDOM) {
window.addEventListener('resize', this.handleResize, false);
// Observe container size changes (e.g., inside resizable Drawer)
this.resizeObserver = new ResizeObserver((entries) => {
const newWidth = entries[0].contentRect.width;
// Only react to WIDTH changes — scroll button rendering changes height,
// which would cause an infinite feedback loop if not filtered out.
if (Math.abs(newWidth - this.lastObservedWidth) < 1) {
return;
}
this.lastObservedWidth = newWidth;
this.handleResize();
});
if (this.tabList.current) {
this.resizeObserver.observe(this.tabList.current);
}
}
this.direction = getLanguageDirection(this.tabList.current);
this.handleScrollButtons();
}
this.setAccentStyles(true);
}
componentWillUnmount() {
if (!this.props.isVertical) {
if (canUseDOM) {
window.removeEventListener('resize', this.handleResize, false);
this.resizeObserver?.disconnect();
}
}
// ...existing cleanup
}
This would fix Tabs in any dynamically-sized container, not just Drawer.
Current Workaround
Consumers can work around this by attaching their own ResizeObserver and forcing a Tabs remount via a key prop. However, the workaround has three non-obvious pitfalls:
-
Must use a callback ref, not
useRef+useEffect—DrawerPanelContentdelays rendering children until a CSS transition completes (isExpandedInternalis set ontransitionEnd). AuseEffectkeyed onisExpandedruns before the panel DOM node exists, so the observer never attaches after a drawer toggle. -
Must filter by width changes only — When scroll buttons appear/disappear, they change the container height. A naive
ResizeObservercallback re-triggers on height changes, causing an infinite remount loop (buttons render → height changes → observer fires → remount → repeat). -
Must debounce to ~150ms — Tabs internally uses a 100ms debounce in
handleScrollButtons()and a 100ms delay forshowScrollButtons. The consumer's debounce must be longer than these to avoid interfering with the multi-phase scroll button rendering cycle.
const [resizeKey, setResizeKey] = useState(0);
const lastWidthRef = useRef<number>(0);
const observerRef = useRef<ResizeObserver | null>(null);
const debounceRef = useRef<ReturnType<typeof setTimeout>>();
const panelCallbackRef = useCallback((node: HTMLDivElement | null) => {
clearTimeout(debounceRef.current);
observerRef.current?.disconnect();
observerRef.current = null;
if (!node) return;
lastWidthRef.current = 0;
observerRef.current = new ResizeObserver((entries) => {
const newWidth = entries[0].contentRect.width;
if (Math.abs(newWidth - lastWidthRef.current) < 1) return;
lastWidthRef.current = newWidth;
clearTimeout(debounceRef.current);
debounceRef.current = setTimeout(() => setResizeKey(c => c + 1), 150);
});
observerRef.current.observe(node);
}, []);
// In your DrawerPanelContent:
<div ref={panelCallbackRef}>
<Tabs key={resizeKey} activeKey={activeKey} onSelect={handleSelect}>
{/* ... */}
</Tabs>
</div>
Environment
-
@patternfly/react-coreversion: current main -
Browsers: all (ResizeObserver is widely supported)
Related
- Jira: PF-3907
Jira Issue: PF-4026
- Langage dominant
- TypeScript
- Étoiles
- 862
- Forks
- 392
- Merge moyen
- 4 j 8 h
- PR mergées (30 j)
- 9
Guide de contribution
Ouvrir le guide de contribution
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de patternfly/patternfly-react
-
PF Team
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
patternfly/patternfly-react#12651 ·
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
patternfly/patternfly-react#12637 ·
-
PF Team
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
patternfly/patternfly-react#12617 · 1 commentaire ·
-
PF Team
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
patternfly/patternfly-react#12610 · 1 commentaire · 1 réaction ·
-
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
patternfly/patternfly-react#12600 ·
Toutes les issues de patternfly/patternfly-react
Issues similaires
-
Difficulté 1/5 1-3 heures Accessibilité débutants 88/100
motiondivision/motion#3849 ·
-
Add: S Play Event HD Ouvertecheck:passed streams:add
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
-
Improvement for contact popover Ouverte
Difficulté 2/5 1-3 heures Accessibilité débutants 68/100
-
LiteLLM proxy response_cost (x-litellm-response-cost) is never applied to ChatModelOutput.cost Ouvertebug
Difficulté 2/5 1-3 heures Accessibilité débutants 84/100
i-am-bee/beeai-framework#1697 · 1 réaction ·
-
Support bun dedupe Ouverteenhancement
Difficulté 2/5 1-3 heures Accessibilité débutants 75/100
antfu/node-modules-inspector#214 ·