diff --git a/packages/@react-spectrum/utils/src/Slots.tsx b/packages/@react-spectrum/utils/src/Slots.tsx index e910ccd7048..d6aa6204ccf 100644 --- a/packages/@react-spectrum/utils/src/Slots.tsx +++ b/packages/@react-spectrum/utils/src/Slots.tsx @@ -36,7 +36,7 @@ export function cssModuleToSlots(cssModule) { export function SlotProvider(props) { const emptyObj = useMemo(() => ({}), []); - // eslint-disable-next-line react-hooks/exhaustive-deps + let parentSlots = useContext(SlotContext) || emptyObj; let {slots = emptyObj, children} = props; diff --git a/packages/@react-stately/tabs/src/useTabListState.ts b/packages/@react-stately/tabs/src/useTabListState.ts index 74a55aaf09e..c9bc6c3c23d 100644 --- a/packages/@react-stately/tabs/src/useTabListState.ts +++ b/packages/@react-stately/tabs/src/useTabListState.ts @@ -43,7 +43,7 @@ export function useTabListState(props: TabListStateOptions) 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 diff --git a/packages/react-aria-components/test/Tabs.test.js b/packages/react-aria-components/test/Tabs.test.js index 5b471156605..d93c6f52843 100644 --- a/packages/react-aria-components/test/Tabs.test.js +++ b/packages/react-aria-components/test/Tabs.test.js @@ -11,8 +11,8 @@ */ import {act, fireEvent, pointerMap, render, waitFor, within} from '@react-spectrum/test-utils-internal'; -import React from 'react'; -import {Tab, TabList, TabPanel, Tabs} from '../'; +import {Button, Collection, Tab, TabList, TabPanel, Tabs} from '../'; +import React, {useState} from 'react'; import {TabsExample} from '../stories/Tabs.stories'; import userEvent from '@testing-library/user-event'; @@ -474,4 +474,92 @@ 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 ( + +
+ + {(item) => ( + + {({isSelected}) => ( +

+ {item.title} +

+ )} +
+ )} +
+
+ + +
+
+ + {(item) => ( + + {item.content} + + )} + +
+ ); + } + render(); + await user.tab(); + await user.keyboard('{ArrowRight}'); + await user.tab(); + onSelectionChange.mockClear(); + await user.keyboard('{Enter}'); + expect(onSelectionChange).not.toHaveBeenCalled(); + }); });