Skip to content
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
5 changes: 5 additions & 0 deletions .changeset/beige-knives-visit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@astrojs/starlight': patch
---

Fixes an issue with synced `<Tabs>` components containing nested `<Tabs>` causing tab panels to not render correctly.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
---
title: Tabs
---

import { Tabs, TabItem } from '@astrojs/starlight/components';

A set of tabs using the `pkg` sync key with some nested tabs using an `os` sync key.

<Tabs syncKey="pkg">

<TabItem label="npm">

npm content

<Tabs syncKey="os">
<TabItem label="macos">npm macOS</TabItem>
<TabItem label="windows">npm Windows</TabItem>
<TabItem label="linux">npm GNU/Linux</TabItem>
</Tabs>

</TabItem>

<TabItem label="pnpm">

pnpm content

<Tabs syncKey="os">
<TabItem label="macos">pnpm macOS</TabItem>
<TabItem label="windows">pnpm Windows</TabItem>
<TabItem label="linux">pnpm GNU/Linux</TabItem>
</Tabs>

</TabItem>

<TabItem label="yarn">

yarn content

<Tabs syncKey="os">
<TabItem label="macos">yarn macOS</TabItem>
<TabItem label="windows">yarn Windows</TabItem>
<TabItem label="linux">yarn GNU/Linux</TabItem>
</Tabs>

</TabItem>

</Tabs>
40 changes: 37 additions & 3 deletions packages/starlight/__e2e__/tabs.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,7 +312,41 @@ test('gracefully handles invalid persisted state for synced tabs', async ({
);
});

async function expectSelectedTab(tabs: Locator, label: string, panel: string) {
expect((await tabs.getByRole('tab', { selected: true }).textContent())?.trim()).toBe(label);
expect((await tabs.getByRole('tabpanel').textContent())?.trim()).toBe(panel);
test('syncs and restores nested tabs', async ({ page, getProdServer }) => {
const starlight = await getProdServer();
await starlight.goto('/tabs-nested');

const tabs = page.locator('starlight-tabs');
const pkgTabs = tabs.nth(0);
const osTabsA = tabs.nth(1);
const osTabsB = tabs.nth(2);

// Select the linux tab in the npm tab.
await osTabsA.getByRole('tab').filter({ hasText: 'linux' }).click();

await expectSelectedTab(osTabsA, 'linux', 'npm GNU/Linux');

// Select the pnpm tab.
await pkgTabs.getByRole('tab').filter({ hasText: 'pnpm' }).click();

await expectSelectedTab(pkgTabs, 'pnpm');
await expectSelectedTab(osTabsB, 'linux', 'pnpm GNU/Linux');

page.reload();

// The synced tabs should be restored.
await expectSelectedTab(pkgTabs, 'pnpm');
await expectSelectedTab(osTabsB, 'linux', 'pnpm GNU/Linux');
});

async function expectSelectedTab(tabs: Locator, label: string, panel?: string) {
expect(
(await tabs.locator(':scope > div [role=tab][aria-selected=true]').textContent())?.trim()
).toBe(label);

if (panel) {
const tabPanel = tabs.locator(':scope > [role=tabpanel]:not([hidden])');
await expect(tabPanel).toBeVisible();
expect((await tabPanel.textContent())?.trim()).toBe(panel);
}
}
2 changes: 1 addition & 1 deletion packages/starlight/user-components/Tabs.astro
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ if (isSynced) {
const tabIndexToRestore = tabs.findIndex(
(tab) => tab instanceof HTMLAnchorElement && tab.textContent?.trim() === label
);
const panels = starlightTabs?.querySelectorAll('[role="tabpanel"]');
const panels = starlightTabs?.querySelectorAll(':scope > [role="tabpanel"]');
const newTab = tabs[tabIndexToRestore];
const newPanel = panels[tabIndexToRestore];
if (tabIndexToRestore < 1 || !newTab || !newPanel) return;
Expand Down