Skip to content

Commit

Permalink
handle other tabs pushing the selected tab
Browse files Browse the repository at this point in the history
  • Loading branch information
snowystinger committed Jan 18, 2023
1 parent 1b84c88 commit 70007d6
Show file tree
Hide file tree
Showing 3 changed files with 135 additions and 23 deletions.
40 changes: 26 additions & 14 deletions packages/@react-spectrum/tabs/src/Tabs.tsx
Expand Up @@ -17,7 +17,16 @@ import {FocusRing} from '@react-aria/focus';
import {Item, Picker} from '@react-spectrum/picker';
import {ListCollection} from '@react-stately/list';
import {mergeProps, useId, useLayoutEffect} from '@react-aria/utils';
import React, {Key, MutableRefObject, ReactElement, useCallback, useContext, useEffect, useRef, useState} from 'react';
import React, {
Key,
MutableRefObject,
ReactElement,
useCallback,
useContext,
useEffect,
useRef,
useState
} from 'react';
import {SpectrumPickerProps} from '@react-types/select';
import {SpectrumTabListProps, SpectrumTabPanelsProps, SpectrumTabsProps} from '@react-types/tabs';
import styles from '@adobe/spectrum-css-temp/components/tabs/vars.css';
Expand All @@ -44,7 +53,8 @@ interface TabsContext<T> {
},
tabPanelProps: {
'aria-labelledby': string
}
},
tabLineState: Array<DOMRect>
}

const TabContext = React.createContext<TabsContext<any>>(null);
Expand All @@ -67,6 +77,8 @@ function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDiv
let [collapsed, setCollapsed] = useState(false);
let [selectedTab, setSelectedTab] = useState<HTMLElement>();
const [tabListState, setTabListState] = useState<TabListState<T>>(null);
let [tabPositions, setTabPositions] = useState([]);
let prevTabPositions = useRef(tabPositions);

useEffect(() => {
if (tablistRef.current) {
Expand All @@ -83,15 +95,19 @@ function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDiv
if (wrapperRef.current && orientation !== 'vertical') {
let tabsComponent = wrapperRef.current;
let tabs = tablistRef.current.querySelectorAll('[role="tab"]');
let lastTab = tabs[tabs.length - 1];
let tabDimensions = [...tabs].map(tab => tab.getBoundingClientRect());

let end = direction === 'rtl' ? 'left' : 'right';
let farEdgeTabList = tabsComponent.getBoundingClientRect()[end];
let farEdgeLastTab = lastTab?.getBoundingClientRect()[end];
let farEdgeLastTab = tabDimensions[tabDimensions.length - 1][end];
let shouldCollapse = direction === 'rtl' ? farEdgeLastTab < farEdgeTabList : farEdgeTabList < farEdgeLastTab;
setCollapsed(shouldCollapse);
if (tabDimensions.length !== prevTabPositions.current.length || tabDimensions.some((box, index) => box?.left !== tabPositions[index]?.left && box?.right !== tabPositions[index]?.right)) {
setTabPositions(tabDimensions);
prevTabPositions.current = tabDimensions;
}
}
}, [tablistRef, wrapperRef, direction, orientation, setCollapsed]);
}, [tablistRef, wrapperRef, direction, orientation, setCollapsed, prevTabPositions, setTabPositions]);

useEffect(() => {
checkShouldCollapse();
Expand All @@ -114,7 +130,8 @@ function Tabs<T extends object>(props: SpectrumTabsProps<T>, ref: DOMRef<HTMLDiv
tabProps: {...props, orientation, density},
tabState: {tabListState, setTabListState, selectedTab, collapsed},
refs: {tablistRef, wrapperRef},
tabPanelProps
tabPanelProps,
tabLineState: tabPositions
}}>
<div
{...filterDOMProps(otherProps)}
Expand Down Expand Up @@ -204,17 +221,13 @@ function TabLine(props: TabLineProps) {

let {direction} = useLocale();
let {scale} = useProvider();
let {tabLineState} = useContext(TabContext);

let [style, setStyle] = useState({
width: undefined,
height: undefined
});

let selectedTabRef = useRef<HTMLElement>();
if (selectedTab) {
selectedTabRef.current = selectedTab;
}

let onResize = useCallback(() => {
if (selectedTab) {
let styleObj = {transform: undefined, width: undefined, height: undefined};
Expand All @@ -235,9 +248,7 @@ function TabLine(props: TabLineProps) {

useLayoutEffect(() => {
onResize();
}, [onResize, direction, scale, selectedKey, orientation, selectedTab?.offsetLeft]);

useResizeObserver({ref: selectedTabRef, onResize: onResize});
}, [onResize, scale, selectedKey, tabLineState]);

return <div className={classNames(styles, 'spectrum-Tabs-selectionIndicator')} role="presentation" style={style} />;
}
Expand Down Expand Up @@ -272,6 +283,7 @@ export function TabList<T>(props: SpectrumTabListProps<T>) {
}

let tabListclassName = classNames(styles, 'spectrum-TabsPanel-tabs');

const tabContent = (
<div
{...stylePropsFinal}
Expand Down
8 changes: 5 additions & 3 deletions packages/@react-spectrum/tabs/stories/Tabs.stories.tsx
Expand Up @@ -251,17 +251,19 @@ storiesOf('Tabs', module)
}
)
.add(
'Tab 1 controlled TabList item',
'changing tab titles',
() => {
let [tab1Text, setTab1Text] = useState('Tab 1');
let [tab2Text, setTab2Text] = useState('Tab 2');

return (
<Flex minHeight={400} minWidth={400} direction="column">
<TextField label="Tab Title" value={tab1Text} onChange={setTab1Text} />
<TextField label="Tab1 Title" value={tab1Text} onChange={setTab1Text} />
<TextField label="Tab2 Title" value={tab2Text} onChange={setTab2Text} />
<Tabs maxWidth={500}>
<TabList>
<Item>{tab1Text}</Item>
<Item>Tab 2</Item>
<Item>{tab2Text}</Item>
</TabList>
<TabPanels>
<Item>Tab 1 Content</Item>
Expand Down
110 changes: 104 additions & 6 deletions packages/@react-spectrum/tabs/test/Tabs.test.js
Expand Up @@ -49,14 +49,18 @@ describe('Tabs', function () {
let onSelectionChange = jest.fn();

beforeAll(function () {
jest.useFakeTimers();
});

beforeEach(() => {
jest.spyOn(window.HTMLElement.prototype, 'clientWidth', 'get').mockImplementation(() => 1000);
jest.spyOn(window.HTMLElement.prototype, 'clientHeight', 'get').mockImplementation(() => 1000);
window.HTMLElement.prototype.scrollIntoView = jest.fn();
jest.useFakeTimers();
});

afterEach(() => {
jest.clearAllMocks();
jest.restoreAllMocks();
act(() => jest.runAllTimers());
});

Expand Down Expand Up @@ -351,13 +355,26 @@ describe('Tabs', function () {
let mockCalls = [
function () {
return {
right: 500
left: 0,
right: 100
};
},
function () {
return {
left: 100,
right: 400
};
},
function () {
return {
left: 400,
right: 700
};
},
function () {
return {
right: 500
};
}
];

Expand Down Expand Up @@ -417,13 +434,26 @@ describe('Tabs', function () {
let mockCalls = [
function () {
return {
right: 500
left: 0,
right: 200
};
},
function () {
return {
left: 200,
right: 300
};
},
function () {
return {
left: 300,
right: 400
};
},
function () {
return {
right: 500
};
}
];

Expand Down Expand Up @@ -453,12 +483,28 @@ describe('Tabs', function () {

spy.mockImplementationOnce(function () {
return {
right: 500
left: 0,
right: 200
};
}).mockImplementationOnce(function () {
return {
left: 200,
right: 300
};
}).mockImplementationOnce(function () {
return {
left: 300,
right: 400
};
}).mockImplementationOnce(function () {
return {
left: 400,
right: 700
};
}).mockImplementationOnce(function () {
return {
right: 500
};
});

let newItems = [...defaultItems];
Expand Down Expand Up @@ -491,6 +537,32 @@ describe('Tabs', function () {
expect(picker).toBeTruthy();
expect(tabpanel).toHaveAttribute('aria-labelledby', `${picker.id}`);

spy.mockImplementationOnce(function () {
return {
left: 0,
right: 200
};
}).mockImplementationOnce(function () {
return {
left: 200,
right: 300
};
}).mockImplementationOnce(function () {
return {
left: 300,
right: 400
};
}).mockImplementationOnce(function () {
return {
left: 400,
right: 700
};
}).mockImplementationOnce(function () {
return {
right: 500
};
});

rerender(
<Provider theme={theme}>
<Tabs aria-label="Tab Example" items={newItems} orientation="vertical">
Expand Down Expand Up @@ -521,12 +593,23 @@ describe('Tabs', function () {

spy.mockImplementationOnce(function () {
return {
right: 500
left: 0,
right: 200
};
}).mockImplementationOnce(function () {
return {
left: 200,
right: 300
};
}).mockImplementationOnce(function () {
return {
left: 300,
right: 400
};
}).mockImplementationOnce(function () {
return {
right: 500
};
});

newItems = [...defaultItems];
Expand Down Expand Up @@ -558,20 +641,35 @@ describe('Tabs', function () {
tablist = getByRole('tablist');
expect(tablist).toBeTruthy();
expect(queryByRole('button')).toBeNull();

});

it('disabled tabs cannot be selected via collapse picker', function () {
console.log('start')
let target = [HTMLDivElement.prototype, 'getBoundingClientRect'];
let mockCalls = [
function () {
return {
right: 500
left: 0,
right: 100
};
},
function () {
return {
left: 100,
right: 400
};
},
function () {
return {
left: 400,
right: 700
};
},
function () {
return {
right: 500
};
}
];

Expand Down

0 comments on commit 70007d6

Please sign in to comment.