summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHiDeoo2024-03-01 18:20:26 +0100
committerGitHub2024-03-01 18:20:26 +0100
commit5f99a71ddfe92568b1cd3c0bfe5ebfd139797c1a (patch)
tree82252c42ed573d79161437d8eb603d949902f9c3
parent507bdcc760a478ba08fe68f2f51bffb33820b2c4 (diff)
downloadIT.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.md5
-rw-r--r--docs/src/content/docs/guides/components.mdx17
-rw-r--r--packages/starlight/__tests__/remark-rehype/rehype-tabs.test.ts19
-rw-r--r--packages/starlight/user-components/TabItem.astro6
-rw-r--r--packages/starlight/user-components/Tabs.astro8
-rw-r--r--packages/starlight/user-components/rehype-tabs.ts11
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;