Skip to content

Commit

Permalink
fix: tablist auto selection (#7529)
Browse files Browse the repository at this point in the history
* Fix tabs auto selection

* fix lint

* see if it passes on CI

* remove forced selection in controlled

* add a little more to the test
  • Loading branch information
snowystinger authored Jan 27, 2025
1 parent f6e1bd9 commit fd7075c
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 3 deletions.
2 changes: 1 addition & 1 deletion packages/@react-stately/tabs/src/useTabListState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export function useTabListState<T extends object>(props: TabListStateOptions<T>)
useEffect(() => {
// Ensure a tab is always selected (in case no selected key was specified or if selected item was deleted from collection)
let selectedKey = currentSelectedKey;
if (selectionManager.isEmpty || selectedKey == null || !collection.getItem(selectedKey)) {
if (props.selectedKey == null && (selectionManager.isEmpty || selectedKey == null || !collection.getItem(selectedKey))) {
selectedKey = findDefaultSelectedKey(collection, state.disabledKeys);
if (selectedKey != null) {
// directly set selection because replace/toggle selection won't consider disabled keys
Expand Down
102 changes: 100 additions & 2 deletions packages/react-aria-components/test/Tabs.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@
* governing permissions and limitations under the License.
*/

import {Button, Collection, Tab, TabList, TabPanel, Tabs} from '../';
import {fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal';
import React from 'react';
import {Tab, TabList, TabPanel, Tabs} from '../';
import React, {useState} from 'react';
import {TabsExample} from '../stories/Tabs.stories';
import {User} from '@react-aria/test-utils';
import userEvent from '@testing-library/user-event';
Expand Down Expand Up @@ -497,4 +497,102 @@ describe('Tabs', () => {
expect(innerTabs[0]).toHaveTextContent('One');
expect(innerTabs[1]).toHaveTextContent('Two');
});

it('can add tabs and keep the current selected key', async () => {
let onSelectionChange = jest.fn();
function Example(props) {
let [tabs, setTabs] = useState([
{id: 1, title: 'Tab 1', content: 'Tab body 1'},
{id: 2, title: 'Tab 2', content: 'Tab body 2'},
{id: 3, title: 'Tab 3', content: 'Tab body 3'}
]);

const [selectedTabId, setSelectedTabId] = useState(tabs[0].id);

let addTab = () => {
const tabId = tabs.length + 1;

setTabs((prevTabs) => [
...prevTabs,
{
id: tabId,
title: `Tab ${tabId}`,
content: `Tab body ${tabId}`
}
]);

// Use functional update to ensure you're working with the most recent state
setSelectedTabId(tabId);
};

let removeTab = () => {
if (tabs.length > 1) {
setTabs((prevTabs) => {
const updatedTabs = prevTabs.slice(0, -1);
// Update selectedTabId to the last remaining tab's ID if the current selected tab is removed
const newSelectedTabId = updatedTabs[updatedTabs.length - 1].id;
setSelectedTabId(newSelectedTabId);
return updatedTabs;
});
}
};

const onSelectionChange = (value) => {
setSelectedTabId(value);
props.onSelectionChange(value);
};

return (
<Tabs selectedKey={selectedTabId} onSelectionChange={onSelectionChange}>
<div style={{display: 'flex'}}>
<TabList aria-label="Dynamic tabs" items={tabs} style={{flex: 1}}>
{(item) => (
<Tab>
{({isSelected}) => (
<p
style={{
color: isSelected ? 'red' : 'black'
}}>
{item.title}
</p>
)}
</Tab>
)}
</TabList>
<div className="button-group">
<Button onPress={addTab}>Add tab</Button>
<Button onPress={removeTab}>Remove tab</Button>
</div>
</div>
<Collection items={tabs}>
{(item) => (
<TabPanel
style={{
borderTop: '2px solid black'
}}>
{item.content}
</TabPanel>
)}
</Collection>
</Tabs>
);
}
let {getAllByRole} = render(<Example onSelectionChange={onSelectionChange} />);
let tabs = getAllByRole('tab');
await user.tab();
await user.keyboard('{ArrowRight}');
expect(tabs[1]).toHaveAttribute('aria-selected', 'true');
await user.tab();
onSelectionChange.mockClear();
await user.keyboard('{Enter}');
expect(onSelectionChange).not.toHaveBeenCalled();
tabs = getAllByRole('tab');
expect(tabs[3]).toHaveAttribute('aria-selected', 'true');

await user.tab();
await user.keyboard('{Enter}');
expect(onSelectionChange).not.toHaveBeenCalled();
tabs = getAllByRole('tab');
expect(tabs[2]).toHaveAttribute('aria-selected', 'true');
});
});

1 comment on commit fd7075c

@rspbot
Copy link

@rspbot rspbot commented on fd7075c Jan 27, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.