From 5f99a71ddfe92568b1cd3c0bfe5ebfd139797c1a Mon Sep 17 00:00:00 2001 From: HiDeoo Date: Fri, 1 Mar 2024 18:20:26 +0100 Subject: Add icon support to the `` component (#1568) * feat: add `icon` prop to the `` component * docs: split `icon` attribute in a new sentence Co-authored-by: Chris Swithinbank --------- Co-authored-by: Chris Swithinbank --- .changeset/curly-taxis-destroy.md | 5 +++++ docs/src/content/docs/guides/components.mdx | 17 +++++++++++++---- .../__tests__/remark-rehype/rehype-tabs.test.ts | 19 +++++++++++++++++-- packages/starlight/user-components/TabItem.astro | 6 ++++-- packages/starlight/user-components/Tabs.astro | 8 ++++++-- packages/starlight/user-components/rehype-tabs.ts | 11 ++++++++--- 6 files changed, 53 insertions(+), 13 deletions(-) create mode 100644 .changeset/curly-taxis-destroy.md 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 `` 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 `` and `` components. Each `` 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 `` must have a `label` to display to users. import { Tabs, TabItem } from '@astrojs/starlight/components'; - Sirius, Vega, Betelgeuse - Io, Europa, Ganymede + + Sirius, Vega, Betelgeuse + + + Io, Europa, Ganymede + ``` The code above generates the following tabs on the page: - Sirius, Vega, Betelgeuse - Io, Europa, Ganymede + + Sirius, Vega, Betelgeuse + + + Io, Europa, Ganymede + ### 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}`; +const TabItem = ({ label, slot, icon }: { label: string; slot: string; icon?: string }) => { + const iconAttr = icon ? ` data-icon="${icon}"` : ''; + return `<${TabItemTagname} data-label="${label}"${iconAttr}>${slot}`; +}; /** Get an array of HTML strings, one for each `
` 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: '

Random paragraph

', icon }); + const { panels, html } = processPanels(input); + + expect(html).toMatchInlineSnapshot( + `"

Random paragraph

"` + ); + 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 `` component.'); } --- - + 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 && (
    - {panels.map(({ label, panelId, tabId }, idx) => ( + {panels.map(({ icon, label, panelId, tabId }, idx) => ( @@ -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 `` props delete node.properties.dataLabel; + delete node.properties.dataIcon; // Turn into `
    ` with required attributes node.tagName = 'section'; node.properties.id = ids.panelId; -- cgit