Skip to content

Commit fd7075c

Browse files
authored
fix: tablist auto selection (#7529)
* Fix tabs auto selection * fix lint * see if it passes on CI * remove forced selection in controlled * add a little more to the test
1 parent f6e1bd9 commit fd7075c

File tree

2 files changed

+101
-3
lines changed

2 files changed

+101
-3
lines changed

packages/@react-stately/tabs/src/useTabListState.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export function useTabListState<T extends object>(props: TabListStateOptions<T>)
4343
useEffect(() => {
4444
// Ensure a tab is always selected (in case no selected key was specified or if selected item was deleted from collection)
4545
let selectedKey = currentSelectedKey;
46-
if (selectionManager.isEmpty || selectedKey == null || !collection.getItem(selectedKey)) {
46+
if (props.selectedKey == null && (selectionManager.isEmpty || selectedKey == null || !collection.getItem(selectedKey))) {
4747
selectedKey = findDefaultSelectedKey(collection, state.disabledKeys);
4848
if (selectedKey != null) {
4949
// directly set selection because replace/toggle selection won't consider disabled keys

packages/react-aria-components/test/Tabs.test.js

+100-2
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@
1010
* governing permissions and limitations under the License.
1111
*/
1212

13+
import {Button, Collection, Tab, TabList, TabPanel, Tabs} from '../';
1314
import {fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal';
14-
import React from 'react';
15-
import {Tab, TabList, TabPanel, Tabs} from '../';
15+
import React, {useState} from 'react';
1616
import {TabsExample} from '../stories/Tabs.stories';
1717
import {User} from '@react-aria/test-utils';
1818
import userEvent from '@testing-library/user-event';
@@ -497,4 +497,102 @@ describe('Tabs', () => {
497497
expect(innerTabs[0]).toHaveTextContent('One');
498498
expect(innerTabs[1]).toHaveTextContent('Two');
499499
});
500+
501+
it('can add tabs and keep the current selected key', async () => {
502+
let onSelectionChange = jest.fn();
503+
function Example(props) {
504+
let [tabs, setTabs] = useState([
505+
{id: 1, title: 'Tab 1', content: 'Tab body 1'},
506+
{id: 2, title: 'Tab 2', content: 'Tab body 2'},
507+
{id: 3, title: 'Tab 3', content: 'Tab body 3'}
508+
]);
509+
510+
const [selectedTabId, setSelectedTabId] = useState(tabs[0].id);
511+
512+
let addTab = () => {
513+
const tabId = tabs.length + 1;
514+
515+
setTabs((prevTabs) => [
516+
...prevTabs,
517+
{
518+
id: tabId,
519+
title: `Tab ${tabId}`,
520+
content: `Tab body ${tabId}`
521+
}
522+
]);
523+
524+
// Use functional update to ensure you're working with the most recent state
525+
setSelectedTabId(tabId);
526+
};
527+
528+
let removeTab = () => {
529+
if (tabs.length > 1) {
530+
setTabs((prevTabs) => {
531+
const updatedTabs = prevTabs.slice(0, -1);
532+
// Update selectedTabId to the last remaining tab's ID if the current selected tab is removed
533+
const newSelectedTabId = updatedTabs[updatedTabs.length - 1].id;
534+
setSelectedTabId(newSelectedTabId);
535+
return updatedTabs;
536+
});
537+
}
538+
};
539+
540+
const onSelectionChange = (value) => {
541+
setSelectedTabId(value);
542+
props.onSelectionChange(value);
543+
};
544+
545+
return (
546+
<Tabs selectedKey={selectedTabId} onSelectionChange={onSelectionChange}>
547+
<div style={{display: 'flex'}}>
548+
<TabList aria-label="Dynamic tabs" items={tabs} style={{flex: 1}}>
549+
{(item) => (
550+
<Tab>
551+
{({isSelected}) => (
552+
<p
553+
style={{
554+
color: isSelected ? 'red' : 'black'
555+
}}>
556+
{item.title}
557+
</p>
558+
)}
559+
</Tab>
560+
)}
561+
</TabList>
562+
<div className="button-group">
563+
<Button onPress={addTab}>Add tab</Button>
564+
<Button onPress={removeTab}>Remove tab</Button>
565+
</div>
566+
</div>
567+
<Collection items={tabs}>
568+
{(item) => (
569+
<TabPanel
570+
style={{
571+
borderTop: '2px solid black'
572+
}}>
573+
{item.content}
574+
</TabPanel>
575+
)}
576+
</Collection>
577+
</Tabs>
578+
);
579+
}
580+
let {getAllByRole} = render(<Example onSelectionChange={onSelectionChange} />);
581+
let tabs = getAllByRole('tab');
582+
await user.tab();
583+
await user.keyboard('{ArrowRight}');
584+
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
585+
await user.tab();
586+
onSelectionChange.mockClear();
587+
await user.keyboard('{Enter}');
588+
expect(onSelectionChange).not.toHaveBeenCalled();
589+
tabs = getAllByRole('tab');
590+
expect(tabs[3]).toHaveAttribute('aria-selected', 'true');
591+
592+
await user.tab();
593+
await user.keyboard('{Enter}');
594+
expect(onSelectionChange).not.toHaveBeenCalled();
595+
tabs = getAllByRole('tab');
596+
expect(tabs[2]).toHaveAttribute('aria-selected', 'true');
597+
});
500598
});

0 commit comments

Comments
 (0)