diff options
author | HiDeoo | 2024-03-01 18:20:26 +0100 |
---|---|---|
committer | GitHub | 2024-03-01 18:20:26 +0100 |
commit | 5f99a71ddfe92568b1cd3c0bfe5ebfd139797c1a (patch) | |
tree | 82252c42ed573d79161437d8eb603d949902f9c3 | |
parent | 507bdcc760a478ba08fe68f2f51bffb33820b2c4 (diff) | |
download | IT.starlight-5f99a71ddfe92568b1cd3c0bfe5ebfd139797c1a.tar.gz IT.starlight-5f99a71ddfe92568b1cd3c0bfe5ebfd139797c1a.tar.bz2 IT.starlight-5f99a71ddfe92568b1cd3c0bfe5ebfd139797c1a.zip |
Add icon support to the `<TabItem>` component (#1568)
* feat: add `icon` prop to the `<TabItem>` component
* docs: split `icon` attribute in a new sentence
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
---------
Co-authored-by: Chris Swithinbank <swithinbank@gmail.com>
-rw-r--r-- | .changeset/curly-taxis-destroy.md | 5 | ||||
-rw-r--r-- | docs/src/content/docs/guides/components.mdx | 17 | ||||
-rw-r--r-- | packages/starlight/__tests__/remark-rehype/rehype-tabs.test.ts | 19 | ||||
-rw-r--r-- | packages/starlight/user-components/TabItem.astro | 6 | ||||
-rw-r--r-- | packages/starlight/user-components/Tabs.astro | 8 | ||||
-rw-r--r-- | packages/starlight/user-components/rehype-tabs.ts | 11 |
6 files changed, 53 insertions, 13 deletions
diff --git a/.changeset/curly-taxis-destroy.md b/.changeset/curly-taxis-destroy.md new file mode 100644 index 00000000..60eecb25 --- /dev/null +++ b/.changeset/curly-taxis-destroy.md @@ -0,0 +1,5 @@ +--- +'@astrojs/starlight': minor +--- + +Adds support for optionally setting an icon on a `<TabItem>` component to make it easier to visually distinguish between tabs. diff --git a/docs/src/content/docs/guides/components.mdx b/docs/src/content/docs/guides/components.mdx index e1737838..8db1a553 100644 --- a/docs/src/content/docs/guides/components.mdx +++ b/docs/src/content/docs/guides/components.mdx @@ -59,6 +59,7 @@ import { Tabs, TabItem } from '@astrojs/starlight/components'; You can display a tabbed interface using the `<Tabs>` and `<TabItem>` components. Each `<TabItem>` must have a `label` to display to users. +Use the optional `icon` attribute to include one of [Starlight’s built-in icons](#all-icons) next to the label. ```mdx # src/content/docs/example.mdx @@ -66,16 +67,24 @@ Each `<TabItem>` must have a `label` to display to users. import { Tabs, TabItem } from '@astrojs/starlight/components'; <Tabs> - <TabItem label="Stars">Sirius, Vega, Betelgeuse</TabItem> - <TabItem label="Moons">Io, Europa, Ganymede</TabItem> + <TabItem label="Stars" icon="star"> + Sirius, Vega, Betelgeuse + </TabItem> + <TabItem label="Moons" icon="moon"> + Io, Europa, Ganymede + </TabItem> </Tabs> ``` The code above generates the following tabs on the page: <Tabs> - <TabItem label="Stars">Sirius, Vega, Betelgeuse</TabItem> - <TabItem label="Moons">Io, Europa, Ganymede</TabItem> + <TabItem label="Stars" icon="star"> + Sirius, Vega, Betelgeuse + </TabItem> + <TabItem label="Moons" icon="moon"> + Io, Europa, Ganymede + </TabItem> </Tabs> ### Cards diff --git a/packages/starlight/__tests__/remark-rehype/rehype-tabs.test.ts b/packages/starlight/__tests__/remark-rehype/rehype-tabs.test.ts index 868f250d..85e50d7c 100644 --- a/packages/starlight/__tests__/remark-rehype/rehype-tabs.test.ts +++ b/packages/starlight/__tests__/remark-rehype/rehype-tabs.test.ts @@ -1,8 +1,10 @@ import { expect, test } from 'vitest'; import { processPanels, TabItemTagname } from '../../user-components/rehype-tabs'; -const TabItem = ({ label, slot }: { label: string; slot: string }) => - `<${TabItemTagname} data-label="${label}">${slot}</${TabItemTagname}>`; +const TabItem = ({ label, slot, icon }: { label: string; slot: string; icon?: string }) => { + const iconAttr = icon ? ` data-icon="${icon}"` : ''; + return `<${TabItemTagname} data-label="${label}"${iconAttr}>${slot}</${TabItemTagname}>`; +}; /** Get an array of HTML strings, one for each `<section>` created by rehype-tabs for each tab item. */ const extractSections = (html: string) => @@ -34,6 +36,7 @@ test('tab items are processed', () => { expect(panels?.[0]?.label).toBe(label); expect(panels?.[0]?.panelId).toMatchInlineSnapshot('"tab-panel-0"'); expect(panels?.[0]?.tabId).toMatchInlineSnapshot('"tab-0"'); + expect(panels?.[0]?.icon).not.toBeDefined(); }); test('only first item is not hidden', () => { @@ -89,3 +92,15 @@ test('applies tabindex="0" to tab items without focusable content', () => { expect(sections[1]).includes('tabindex="0"'); expect(sections[2]).not.includes('tabindex="0"'); }); + +test('processes a tab item icon', () => { + const icon = 'star'; + const input = TabItem({ label: 'Test', slot: '<p>Random paragraph</p>', icon }); + const { panels, html } = processPanels(input); + + expect(html).toMatchInlineSnapshot( + `"<section id="tab-panel-10" aria-labelledby="tab-10" role="tabpanel" tabindex="0"><p>Random paragraph</p></section>"` + ); + expect(panels).toHaveLength(1); + expect(panels?.[0]?.icon).toBe(icon); +}); diff --git a/packages/starlight/user-components/TabItem.astro b/packages/starlight/user-components/TabItem.astro index bc08c86e..a7dfe634 100644 --- a/packages/starlight/user-components/TabItem.astro +++ b/packages/starlight/user-components/TabItem.astro @@ -1,17 +1,19 @@ --- import { TabItemTagname } from './rehype-tabs'; +import type { Icons } from '../components/Icons'; interface Props { + icon?: keyof typeof Icons; label: string; } -const { label } = Astro.props; +const { icon, label } = Astro.props; if (!label) { throw new Error('Missing prop `label` on `<TabItem>` component.'); } --- -<TabItemTagname data-label={label}> +<TabItemTagname data-label={label} data-icon={icon}> <slot /> </TabItemTagname> diff --git a/packages/starlight/user-components/Tabs.astro b/packages/starlight/user-components/Tabs.astro index 82b3af17..554be0e4 100644 --- a/packages/starlight/user-components/Tabs.astro +++ b/packages/starlight/user-components/Tabs.astro @@ -1,4 +1,5 @@ --- +import Icon from './Icon.astro'; import { processPanels } from './rehype-tabs'; const panelHtml = await Astro.slots.render('default'); @@ -10,7 +11,7 @@ const { html, panels } = processPanels(panelHtml); panels && ( <div class="tablist-wrapper not-content"> <ul role="tablist"> - {panels.map(({ label, panelId, tabId }, idx) => ( + {panels.map(({ icon, label, panelId, tabId }, idx) => ( <li role="presentation" class="tab"> <a role="tab" @@ -19,6 +20,7 @@ const { html, panels } = processPanels(panelHtml); aria-selected={idx === 0 && 'true'} tabindex={idx !== 0 ? -1 : 0} > + {icon && <Icon name={icon} />} {label} </a> </li> @@ -50,7 +52,9 @@ const { html, panels } = processPanels(panelHtml); margin-bottom: -2px; } .tab > [role='tab'] { - display: block; + display: flex; + align-items: center; + gap: 0.5rem; padding: 0 1.25rem; text-decoration: none; border-bottom: 2px solid var(--sl-color-gray-5); diff --git a/packages/starlight/user-components/rehype-tabs.ts b/packages/starlight/user-components/rehype-tabs.ts index 245297c6..759a551b 100644 --- a/packages/starlight/user-components/rehype-tabs.ts +++ b/packages/starlight/user-components/rehype-tabs.ts @@ -2,11 +2,13 @@ import type { Element } from 'hast'; import { select } from 'hast-util-select'; import { rehype } from 'rehype'; import { CONTINUE, SKIP, visit } from 'unist-util-visit'; +import { Icons } from '../components/Icons'; interface Panel { panelId: string; tabId: string; label: string; + icon?: keyof typeof Icons; } declare module 'vfile' { @@ -59,15 +61,18 @@ const tabsProcessor = rehype() return CONTINUE; } - const { dataLabel } = node.properties; + const { dataLabel, dataIcon } = node.properties; const ids = getIDs(); - file.data.panels?.push({ + const panel: Panel = { ...ids, label: String(dataLabel), - }); + }; + if (dataIcon) panel.icon = String(dataIcon) as keyof typeof Icons; + file.data.panels?.push(panel); // Remove `<TabItem>` props delete node.properties.dataLabel; + delete node.properties.dataIcon; // Turn into `<section>` with required attributes node.tagName = 'section'; node.properties.id = ids.panelId; |