diff --git a/packages/@headlessui-react/src/components/tabs/tabs.test.tsx b/packages/@headlessui-react/src/components/tabs/tabs.test.tsx index 310f847..3292579 100644 --- a/packages/@headlessui-react/src/components/tabs/tabs.test.tsx +++ b/packages/@headlessui-react/src/components/tabs/tabs.test.tsx @@ -804,6 +804,145 @@ describe('Rendering', () => { assertTabs({ active: 2 }) }) ) + + it( + 'should select first tab if no tabs were provided originally', + suppressConsoleLogs(async () => { + function Example({ defaultIndex = undefined }: { defaultIndex?: number } = {}) { + let [tabs, setTabs] = useState([]) + + return ( + <> + + + {tabs.map((tab, index) => ( + {tab} + ))} + + + {tabs.map((tab, index) => ( + content: {tab} + ))} + + + + + + ) + } + + render() + + assertActiveElement(document.body) + + // There are no tab initially + assertTabs({ active: -1 }) + + // There are not tabs so this should not change anything + await press(Keys.Tab) + assertTabs({ active: -1 }) + + // Add some tabs + await click(getByText('change')) + + // When going from no tabs to some tabs, the tab based on defaultIndex should be selected + assertTabs({ active: 0 }) + }) + ) + + it( + 'should select first tab if no tabs were provided originally (with a defaultIndex of 1)', + suppressConsoleLogs(async () => { + function Example({ defaultIndex = undefined }: { defaultIndex?: number } = {}) { + let [tabs, setTabs] = useState([]) + + return ( + <> + + + {tabs.map((tab, index) => ( + {tab} + ))} + + + {tabs.map((tab, index) => ( + content: {tab} + ))} + + + + + + ) + } + + render() + + assertActiveElement(document.body) + + // There are no tab initially + assertTabs({ active: -1 }) + + // There are not tabs so this should not change anything + await press(Keys.Tab) + assertTabs({ active: -1 }) + + // Add some tabs + await click(getByText('change')) + + // When going from no tabs to some tabs, the tab based on defaultIndex should be selected + assertTabs({ active: 1 }) + }) + ) + + it( + 'should select first tab if no tabs were provided originally (with a defaultIndex of 1)', + suppressConsoleLogs(async () => { + function Example({ defaultIndex = undefined }: { defaultIndex?: number } = {}) { + let [tabs, setTabs] = useState([]) + + return ( + <> + + + {tabs.map((tab, index) => ( + {tab} + ))} + + + {tabs.map((tab, index) => ( + content: {tab} + ))} + + + + + + + + ) + } + + render() + + assertActiveElement(document.body) + + // There are no tab initially + assertTabs({ active: -1 }) + + // There are not tabs so this should not change anything + await press(Keys.Tab) + assertTabs({ active: -1 }) + + // Add some tabs + await click(getByText('change 1')) + await click(getByText('change 2')) + await click(getByText('change 3')) + + // When going from no tabs to some tabs, the tab based on defaultIndex should be selected + assertTabs({ active: 1 }) + }) + ) }) describe('`selectedIndex`', () => { diff --git a/packages/@headlessui-react/src/components/tabs/tabs.tsx b/packages/@headlessui-react/src/components/tabs/tabs.tsx index 4a633b5..748ccd1 100644 --- a/packages/@headlessui-react/src/components/tabs/tabs.tsx +++ b/packages/@headlessui-react/src/components/tabs/tabs.tsx @@ -99,6 +99,15 @@ let reducers: { [Ordering.Greater]: () => Direction.Forwards, }) + // If there are no focusable tabs then. + // We won't change the selected index + // because it's likely the user is + // lazy loading tabs and there's + // nothing to focus on yet + if (focusableTabs.length === 0) { + return nextState + } + return { ...nextState, selectedIndex: match(direction, { diff --git a/packages/@headlessui-vue/src/components/tabs/tabs.test.ts b/packages/@headlessui-vue/src/components/tabs/tabs.test.ts index 64d7104..638a367 100644 --- a/packages/@headlessui-vue/src/components/tabs/tabs.test.ts +++ b/packages/@headlessui-vue/src/components/tabs/tabs.test.ts @@ -721,6 +721,140 @@ describe('Rendering', () => { // Nothing should change... assertTabs({ active: 2 }) }) + + it( + 'should select first tab if no tabs were provided originally', + suppressConsoleLogs(async () => { + renderTemplate({ + template: html` + + + {{ tab }} + + + content for: {{ tab }} + + + + + + `, + setup() { + let tabs = ref([]) + return { + tabs, + } + }, + }) + + assertActiveElement(document.body) + + // There are no tab initially + assertTabs({ active: -1 }) + + // There are not tabs so this should not change anything + await press(Keys.Tab) + assertTabs({ active: -1 }) + + // Add some tabs + await click(getByText('change')) + + // When going from no tabs to some tabs, the tab based on defaultIndex should be selected + assertTabs({ active: 0 }) + }) + ) + + it( + 'should select first tab if no tabs were provided originally (with a defaultIndex of 1)', + suppressConsoleLogs(async () => { + renderTemplate({ + template: html` + + + {{ tab }} + + + content for: {{ tab }} + + + + + + `, + setup() { + let tabs = ref([]) + return { + tabs, + } + }, + }) + + assertActiveElement(document.body) + + // There are no tab initially + assertTabs({ active: -1 }) + + // There are not tabs so this should not change anything + await press(Keys.Tab) + assertTabs({ active: -1 }) + + // Add some tabs + await click(getByText('change')) + + // When going from no tabs to some tabs, the tab based on defaultIndex should be selected + assertTabs({ active: 1 }) + }) + ) + + it( + 'should select first tab if no tabs were provided originally (with a defaultIndex of 1)', + suppressConsoleLogs(async () => { + renderTemplate({ + template: html` + + + {{ tab }} + + + content for: {{ tab }} + + + + + + + + `, + setup() { + let tabs = ref([]) + return { + tabs, + } + }, + }) + + assertActiveElement(document.body) + + // There are no tab initially + assertTabs({ active: -1 }) + + // There are not tabs so this should not change anything + await press(Keys.Tab) + assertTabs({ active: -1 }) + + // Add some tabs + await click(getByText('change 1')) + + // Add some tabs + await click(getByText('change 2')) + + // Add some tabs + await click(getByText('change 3')) + + // When going from no tabs to some tabs, the tab based on defaultIndex should be selected + assertTabs({ active: 1 }) + }) + ) }) })