Skip to content

Commit 583b6fb

Browse files
tomsontomsnowystingerLFDanLu
authored
3199 resize window tabs flicker (#3202)
* fix flickering line in collapsed mode instead of modifying the DOM all nodes are kept in the DOM and are hiden/shown upon request * fix tests to pass an aria label * fix remarks and rename property * Update packages/@react-spectrum/tabs/src/Tabs.tsx Co-authored-by: Robert Snow <[email protected]> * fixed overflow behavior Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Robert Snow <[email protected]> Co-authored-by: Daniel Lu <[email protected]>
1 parent fe9aeac commit 583b6fb

File tree

2 files changed

+45
-45
lines changed

2 files changed

+45
-45
lines changed

packages/@react-spectrum/tabs/src/Tabs.tsx

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
import {classNames, SlotProvider, unwrapDOMRef, useDOMRef, useStyleProps} from '@react-spectrum/utils';
1414
import {DOMProps, DOMRef, Node, Orientation} from '@react-types/shared';
15-
import {filterDOMProps, useValueEffect} from '@react-aria/utils';
15+
import {filterDOMProps} from '@react-aria/utils';
1616
import {FocusRing} from '@react-aria/focus';
1717
import {Item, Picker} from '@react-spectrum/picker';
1818
import {ListCollection, SingleSelectListState} from '@react-stately/list';
@@ -36,7 +36,7 @@ interface TabsContext<T> {
3636
tabListState: TabListState<T>,
3737
setTabListState: (state: TabListState<T>) => void,
3838
selectedTab: HTMLElement,
39-
collapse: boolean
39+
collapsed: boolean
4040
},
4141
refs: {
4242
wrapperRef: MutableRefObject<HTMLDivElement>,
@@ -64,7 +64,7 @@ function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDiv
6464

6565
let {direction} = useLocale();
6666
let {styleProps} = useStyleProps(otherProps);
67-
let [collapse, setCollapse] = useValueEffect(false);
67+
let [collapsed, setCollapsed] = useState(false);
6868
let [selectedTab, setSelectedTab] = useState<HTMLElement>();
6969
const [tabListState, setTabListState] = useState<TabListState<T>>(null);
7070

@@ -77,34 +77,21 @@ function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDiv
7777
}
7878
}
7979
// collapse is in the dep array so selectedTab can be updated for TabLine positioning
80-
}, [children, tabListState?.selectedKey, collapse, tablistRef]);
80+
}, [children, tabListState?.selectedKey, collapsed, tablistRef]);
8181

8282
let checkShouldCollapse = useCallback(() => {
83-
let computeShouldCollapse = () => {
84-
if (wrapperRef.current) {
85-
let tabsComponent = wrapperRef.current;
86-
let tabs = tablistRef.current.querySelectorAll('[role="tab"]');
87-
let lastTab = tabs[tabs.length - 1];
88-
89-
let end = direction === 'rtl' ? 'left' : 'right';
90-
let farEdgeTabList = tabsComponent.getBoundingClientRect()[end];
91-
let farEdgeLastTab = lastTab?.getBoundingClientRect()[end];
92-
let shouldCollapse = direction === 'rtl' ? farEdgeLastTab < farEdgeTabList : farEdgeTabList < farEdgeLastTab;
93-
94-
return shouldCollapse;
95-
}
96-
};
97-
98-
if (orientation !== 'vertical') {
99-
setCollapse(function* () {
100-
// Make Tabs render in non-collapsed state
101-
yield false;
102-
103-
// Compute if Tabs should collapse and update
104-
yield computeShouldCollapse();
105-
});
83+
if (wrapperRef.current && orientation !== 'vertical') {
84+
let tabsComponent = wrapperRef.current;
85+
let tabs = tablistRef.current.querySelectorAll('[role="tab"]');
86+
let lastTab = tabs[tabs.length - 1];
87+
88+
let end = direction === 'rtl' ? 'left' : 'right';
89+
let farEdgeTabList = tabsComponent.getBoundingClientRect()[end];
90+
let farEdgeLastTab = lastTab?.getBoundingClientRect()[end];
91+
let shouldCollapse = direction === 'rtl' ? farEdgeLastTab < farEdgeTabList : farEdgeTabList < farEdgeLastTab;
92+
setCollapsed(shouldCollapse);
10693
}
107-
}, [tablistRef, wrapperRef, direction, orientation, setCollapse]);
94+
}, [tablistRef, wrapperRef, direction, orientation, setCollapsed]);
10895

10996
useEffect(() => {
11097
checkShouldCollapse();
@@ -118,14 +105,14 @@ function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDiv
118105

119106
// When the tabs are collapsed, the tabPanel should be labelled by the Picker button element.
120107
let collapsibleTabListId = useId();
121-
if (collapse && orientation !== 'vertical') {
108+
if (collapsed && orientation !== 'vertical') {
122109
tabPanelProps['aria-labelledby'] = collapsibleTabListId;
123110
}
124111
return (
125112
<TabContext.Provider
126113
value={{
127114
tabProps: {...props, orientation, density},
128-
tabState: {tabListState, setTabListState, selectedTab, collapse},
115+
tabState: {tabListState, setTabListState, selectedTab, collapsed},
129116
refs: {tablistRef, wrapperRef},
130117
tabPanelProps
131118
}}>
@@ -255,7 +242,7 @@ export function TabList<T>(props: SpectrumTabListProps<T>) {
255242
const tabContext = useContext(TabContext);
256243
const {refs, tabState, tabProps, tabPanelProps} = tabContext;
257244
const {isQuiet, density, isDisabled, isEmphasized, orientation} = tabProps;
258-
const {selectedTab, collapse, setTabListState} = tabState;
245+
const {selectedTab, collapsed, setTabListState} = tabState;
259246
const {tablistRef, wrapperRef} = refs;
260247
// Pass original Tab props but override children to create the collection.
261248
const state = useTabListState({...tabProps, children: props.children});
@@ -268,12 +255,18 @@ export function TabList<T>(props: SpectrumTabListProps<T>) {
268255
setTabListState(state);
269256
// eslint-disable-next-line react-hooks/exhaustive-deps
270257
}, [state.disabledKeys, state.selectedItem, state.selectedKey, props.children]);
271-
let stylePropsForVertical = orientation === 'vertical' ? styleProps : {};
258+
259+
let collapseStyle : React.CSSProperties = collapsed && orientation !== 'vertical' ? {maxWidth: 'calc(100% + 1px)', overflow: 'hidden', visibility: 'hidden', position: 'absolute'} : {maxWidth: 'calc(100% + 1px)'};
260+
let stylePropsFinal = orientation === 'vertical' ? styleProps : {style: collapseStyle};
261+
262+
if (collapsed && orientation !== 'vertical') {
263+
tabListProps['aria-hidden'] = true;
264+
}
272265

273266
let tabListclassName = classNames(styles, 'spectrum-TabsPanel-tabs');
274267
const tabContent = (
275268
<div
276-
{...stylePropsForVertical}
269+
{...stylePropsFinal}
277270
{...tabListProps}
278271
ref={tablistRef}
279272
className={classNames(
@@ -309,7 +302,8 @@ export function TabList<T>(props: SpectrumTabListProps<T>) {
309302
'spectrum-TabsPanel-collapseWrapper',
310303
styleProps.className
311304
)}>
312-
{collapse ? <TabPicker {...props} {...tabProps} id={tabPanelProps['aria-labelledby']} state={state} className={tabListclassName} /> : tabContent}
305+
<TabPicker {...props} {...tabProps} visible={collapsed} id={tabPanelProps['aria-labelledby']} state={state} className={tabListclassName} />
306+
{tabContent}
313307
</div>
314308
);
315309
}
@@ -359,7 +353,8 @@ interface TabPickerProps<T> extends Omit<SpectrumPickerProps<T>, 'children'> {
359353
density?: 'compact' | 'regular',
360354
isEmphasized?: boolean,
361355
state: SingleSelectListState<T>,
362-
className?: string
356+
className?: string,
357+
visible: boolean
363358
}
364359

365360
function TabPicker<T>(props: TabPickerProps<T>) {
@@ -372,7 +367,8 @@ function TabPicker<T>(props: TabPickerProps<T>) {
372367
'aria-label': ariaLabel,
373368
density,
374369
className,
375-
id
370+
id,
371+
visible
376372
} = props;
377373

378374
let ref = useRef();
@@ -394,6 +390,8 @@ function TabPicker<T>(props: TabPickerProps<T>) {
394390
'aria-label': ariaLabel
395391
};
396392

393+
const style : React.CSSProperties = visible ? {} : {visibility: 'hidden', position: 'absolute'};
394+
397395
// TODO: Figure out if tabListProps should go onto the div here, v2 doesn't do it
398396
return (
399397
<div
@@ -408,7 +406,9 @@ function TabPicker<T>(props: TabPickerProps<T>) {
408406
'spectrum-Tabs--emphasized': isEmphasized
409407
},
410408
className
411-
)}>
409+
)}
410+
style={style}
411+
aria-hidden={visible ? undefined : true}>
412412
<SlotProvider
413413
slots={{
414414
icon: {
@@ -425,7 +425,7 @@ function TabPicker<T>(props: TabPickerProps<T>) {
425425
items={items}
426426
ref={ref}
427427
isQuiet
428-
isDisabled={isDisabled}
428+
isDisabled={!visible || isDisabled}
429429
selectedKey={state.selectedKey}
430430
disabledKeys={state.disabledKeys}
431431
onSelectionChange={state.setSelectedKey}>

packages/@react-spectrum/tabs/test/Tabs.test.js

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ function renderComponent(props = {}, itemProps) {
2727
let {items = defaultItems} = props;
2828
return render(
2929
<Provider theme={theme}>
30-
<Tabs {...props} items={items}>
30+
<Tabs aria-label="Tab Sample" {...props} items={items}>
3131
<TabList>
3232
{item => (
3333
<Item {...itemProps} key={item.name} title={item.name || item.children} />
@@ -222,7 +222,7 @@ describe('Tabs', function () {
222222
it('does not generate conflicting ids between multiple tabs instances', function () {
223223
let tree = render(
224224
<Provider theme={theme}>
225-
<Tabs>
225+
<Tabs aria-label="Tab Sample">
226226
<TabList>
227227
{defaultItems.map(item => (
228228
<Item key={item.name} title={item.name || item.children} />
@@ -236,7 +236,7 @@ describe('Tabs', function () {
236236
))}
237237
</TabPanels>
238238
</Tabs>
239-
<Tabs>
239+
<Tabs aria-label="Tab Sample 2">
240240
<TabList>
241241
{defaultItems.map(item => (
242242
<Item key={item.name} title={item.name || item.children} />
@@ -315,7 +315,7 @@ describe('Tabs', function () {
315315

316316
tree.rerender(
317317
<Provider theme={theme}>
318-
<Tabs disabledKeys={[defaultItems[0].name]} onSelectionChange={onSelectionChange} items={defaultItems.slice(0, 1)}>
318+
<Tabs aria-label="Tab Example" disabledKeys={[defaultItems[0].name]} onSelectionChange={onSelectionChange} items={defaultItems.slice(0, 1)}>
319319
<TabList>
320320
{item => (
321321
<Item key={item.name} title={item.name || item.children} />
@@ -498,7 +498,7 @@ describe('Tabs', function () {
498498

499499
rerender(
500500
<Provider theme={theme}>
501-
<Tabs items={newItems} orientation="vertical">
501+
<Tabs aria-label="Tab Example" items={newItems} orientation="vertical">
502502
<TabList>
503503
{item => (
504504
<Item key={item.name} title={item.name || item.children} />
@@ -617,7 +617,7 @@ describe('Tabs', function () {
617617
it('tabpanel should have tabIndex=0 only when there are no focusable elements', async function () {
618618
let {getByRole, getAllByRole} = render(
619619
<Provider theme={theme}>
620-
<Tabs maxWidth={500}>
620+
<Tabs aria-label="Tab Example" maxWidth={500}>
621621
<TabList>
622622
<Item>Tab 1</Item>
623623
<Item>Tab 2</Item>
@@ -652,7 +652,7 @@ describe('Tabs', function () {
652652
it('TabPanel children do not share values between panels', () => {
653653
let {getByDisplayValue, getAllByRole, getByTestId} = render(
654654
<Provider theme={theme}>
655-
<Tabs maxWidth={500}>
655+
<Tabs aria-label="Tab Example" maxWidth={500}>
656656
<TabList>
657657
<Item>Tab 1</Item>
658658
<Item>Tab 2</Item>

0 commit comments

Comments
 (0)