Skip to content

fix: tablist auto selection #7529

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jan 27, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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');
});
});
Loading