Skip to content

Commit 0d28d58

Browse files
Preserve default index when starting out with no tabs (#2250)
* Preserve default index when starting out with no tabs * Add test to Vue * Add test for Vue it already works here so… yeah
1 parent 0276231 commit 0d28d58

File tree

3 files changed

+282
-0
lines changed

3 files changed

+282
-0
lines changed

packages/@headlessui-react/src/components/tabs/tabs.test.tsx

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -804,6 +804,145 @@ describe('Rendering', () => {
804804
assertTabs({ active: 2 })
805805
})
806806
)
807+
808+
it(
809+
'should select first tab if no tabs were provided originally',
810+
suppressConsoleLogs(async () => {
811+
function Example({ defaultIndex = undefined }: { defaultIndex?: number } = {}) {
812+
let [tabs, setTabs] = useState<string[]>([])
813+
814+
return (
815+
<>
816+
<Tab.Group defaultIndex={defaultIndex}>
817+
<Tab.List>
818+
{tabs.map((tab, index) => (
819+
<Tab key={index}>{tab}</Tab>
820+
))}
821+
</Tab.List>
822+
<Tab.Panels>
823+
{tabs.map((tab, index) => (
824+
<Tab.Panel key={index}>content: {tab}</Tab.Panel>
825+
))}
826+
</Tab.Panels>
827+
</Tab.Group>
828+
829+
<button onClick={() => setTabs(['tab 1', 'tab 2', 'tab 3'])}>change</button>
830+
</>
831+
)
832+
}
833+
834+
render(<Example defaultIndex={0} />)
835+
836+
assertActiveElement(document.body)
837+
838+
// There are no tab initially
839+
assertTabs({ active: -1 })
840+
841+
// There are not tabs so this should not change anything
842+
await press(Keys.Tab)
843+
assertTabs({ active: -1 })
844+
845+
// Add some tabs
846+
await click(getByText('change'))
847+
848+
// When going from no tabs to some tabs, the tab based on defaultIndex should be selected
849+
assertTabs({ active: 0 })
850+
})
851+
)
852+
853+
it(
854+
'should select first tab if no tabs were provided originally (with a defaultIndex of 1)',
855+
suppressConsoleLogs(async () => {
856+
function Example({ defaultIndex = undefined }: { defaultIndex?: number } = {}) {
857+
let [tabs, setTabs] = useState<string[]>([])
858+
859+
return (
860+
<>
861+
<Tab.Group defaultIndex={defaultIndex}>
862+
<Tab.List>
863+
{tabs.map((tab, index) => (
864+
<Tab key={index}>{tab}</Tab>
865+
))}
866+
</Tab.List>
867+
<Tab.Panels>
868+
{tabs.map((tab, index) => (
869+
<Tab.Panel key={index}>content: {tab}</Tab.Panel>
870+
))}
871+
</Tab.Panels>
872+
</Tab.Group>
873+
874+
<button onClick={() => setTabs(['tab 1', 'tab 2', 'tab 3'])}>change</button>
875+
</>
876+
)
877+
}
878+
879+
render(<Example defaultIndex={1} />)
880+
881+
assertActiveElement(document.body)
882+
883+
// There are no tab initially
884+
assertTabs({ active: -1 })
885+
886+
// There are not tabs so this should not change anything
887+
await press(Keys.Tab)
888+
assertTabs({ active: -1 })
889+
890+
// Add some tabs
891+
await click(getByText('change'))
892+
893+
// When going from no tabs to some tabs, the tab based on defaultIndex should be selected
894+
assertTabs({ active: 1 })
895+
})
896+
)
897+
898+
it(
899+
'should select first tab if no tabs were provided originally (with a defaultIndex of 1)',
900+
suppressConsoleLogs(async () => {
901+
function Example({ defaultIndex = undefined }: { defaultIndex?: number } = {}) {
902+
let [tabs, setTabs] = useState<string[]>([])
903+
904+
return (
905+
<>
906+
<Tab.Group defaultIndex={defaultIndex}>
907+
<Tab.List>
908+
{tabs.map((tab, index) => (
909+
<Tab key={index}>{tab}</Tab>
910+
))}
911+
</Tab.List>
912+
<Tab.Panels>
913+
{tabs.map((tab, index) => (
914+
<Tab.Panel key={index}>content: {tab}</Tab.Panel>
915+
))}
916+
</Tab.Panels>
917+
</Tab.Group>
918+
919+
<button onClick={() => setTabs(['tab 1', 'tab 2', 'tab 3'])}>change 1</button>
920+
<button onClick={() => setTabs([])}>change 2</button>
921+
<button onClick={() => setTabs(['tab 1', 'tab 2', 'tab 3'])}>change 3</button>
922+
</>
923+
)
924+
}
925+
926+
render(<Example defaultIndex={1} />)
927+
928+
assertActiveElement(document.body)
929+
930+
// There are no tab initially
931+
assertTabs({ active: -1 })
932+
933+
// There are not tabs so this should not change anything
934+
await press(Keys.Tab)
935+
assertTabs({ active: -1 })
936+
937+
// Add some tabs
938+
await click(getByText('change 1'))
939+
await click(getByText('change 2'))
940+
await click(getByText('change 3'))
941+
942+
// When going from no tabs to some tabs, the tab based on defaultIndex should be selected
943+
assertTabs({ active: 1 })
944+
})
945+
)
807946
})
808947

809948
describe('`selectedIndex`', () => {

packages/@headlessui-react/src/components/tabs/tabs.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,15 @@ let reducers: {
9999
[Ordering.Greater]: () => Direction.Forwards,
100100
})
101101

102+
// If there are no focusable tabs then.
103+
// We won't change the selected index
104+
// because it's likely the user is
105+
// lazy loading tabs and there's
106+
// nothing to focus on yet
107+
if (focusableTabs.length === 0) {
108+
return nextState
109+
}
110+
102111
return {
103112
...nextState,
104113
selectedIndex: match(direction, {

packages/@headlessui-vue/src/components/tabs/tabs.test.ts

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,140 @@ describe('Rendering', () => {
721721
// Nothing should change...
722722
assertTabs({ active: 2 })
723723
})
724+
725+
it(
726+
'should select first tab if no tabs were provided originally',
727+
suppressConsoleLogs(async () => {
728+
renderTemplate({
729+
template: html`
730+
<TabGroup :defaultIndex="0">
731+
<TabList>
732+
<Tab v-for="tab in tabs">{{ tab }}</Tab>
733+
</TabList>
734+
<TabPanels>
735+
<TabPanel v-for="tab in tabs">content for: {{ tab }}</TabPanel>
736+
</TabPanels>
737+
</TabGroup>
738+
739+
<button>after</button>
740+
<button @click="tabs.value = ['tab 1', 'tab 2', 'tab 3']">change</button>
741+
`,
742+
setup() {
743+
let tabs = ref<string[]>([])
744+
return {
745+
tabs,
746+
}
747+
},
748+
})
749+
750+
assertActiveElement(document.body)
751+
752+
// There are no tab initially
753+
assertTabs({ active: -1 })
754+
755+
// There are not tabs so this should not change anything
756+
await press(Keys.Tab)
757+
assertTabs({ active: -1 })
758+
759+
// Add some tabs
760+
await click(getByText('change'))
761+
762+
// When going from no tabs to some tabs, the tab based on defaultIndex should be selected
763+
assertTabs({ active: 0 })
764+
})
765+
)
766+
767+
it(
768+
'should select first tab if no tabs were provided originally (with a defaultIndex of 1)',
769+
suppressConsoleLogs(async () => {
770+
renderTemplate({
771+
template: html`
772+
<TabGroup :defaultIndex="1">
773+
<TabList>
774+
<Tab v-for="tab in tabs">{{ tab }}</Tab>
775+
</TabList>
776+
<TabPanels>
777+
<TabPanel v-for="tab in tabs">content for: {{ tab }}</TabPanel>
778+
</TabPanels>
779+
</TabGroup>
780+
781+
<button>after</button>
782+
<button @click="tabs.value = ['tab 1', 'tab 2', 'tab 3']">change</button>
783+
`,
784+
setup() {
785+
let tabs = ref<string[]>([])
786+
return {
787+
tabs,
788+
}
789+
},
790+
})
791+
792+
assertActiveElement(document.body)
793+
794+
// There are no tab initially
795+
assertTabs({ active: -1 })
796+
797+
// There are not tabs so this should not change anything
798+
await press(Keys.Tab)
799+
assertTabs({ active: -1 })
800+
801+
// Add some tabs
802+
await click(getByText('change'))
803+
804+
// When going from no tabs to some tabs, the tab based on defaultIndex should be selected
805+
assertTabs({ active: 1 })
806+
})
807+
)
808+
809+
it(
810+
'should select first tab if no tabs were provided originally (with a defaultIndex of 1)',
811+
suppressConsoleLogs(async () => {
812+
renderTemplate({
813+
template: html`
814+
<TabGroup :defaultIndex="1">
815+
<TabList>
816+
<Tab v-for="tab in tabs">{{ tab }}</Tab>
817+
</TabList>
818+
<TabPanels>
819+
<TabPanel v-for="tab in tabs">content for: {{ tab }}</TabPanel>
820+
</TabPanels>
821+
</TabGroup>
822+
823+
<button>after</button>
824+
<button @click="tabs.value = ['tab 1', 'tab 2', 'tab 3']">change 1</button>
825+
<button @click="tabs.value = []">change 2</button>
826+
<button @click="tabs.value = ['tab 1', 'tab 2', 'tab 3']">change 3</button>
827+
`,
828+
setup() {
829+
let tabs = ref<string[]>([])
830+
return {
831+
tabs,
832+
}
833+
},
834+
})
835+
836+
assertActiveElement(document.body)
837+
838+
// There are no tab initially
839+
assertTabs({ active: -1 })
840+
841+
// There are not tabs so this should not change anything
842+
await press(Keys.Tab)
843+
assertTabs({ active: -1 })
844+
845+
// Add some tabs
846+
await click(getByText('change 1'))
847+
848+
// Add some tabs
849+
await click(getByText('change 2'))
850+
851+
// Add some tabs
852+
await click(getByText('change 3'))
853+
854+
// When going from no tabs to some tabs, the tab based on defaultIndex should be selected
855+
assertTabs({ active: 1 })
856+
})
857+
)
724858
})
725859
})
726860

0 commit comments

Comments
 (0)