Skip to content

Commit

Permalink
Add icon support to the <TabItem> component (#1568)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
HiDeoo and delucis committed Mar 1, 2024
1 parent 507bdcc commit 5f99a71
Show file tree
Hide file tree
Showing 6 changed files with 53 additions and 13 deletions.
5 changes: 5 additions & 0 deletions .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.
17 changes: 13 additions & 4 deletions docs/src/content/docs/guides/components.mdx
Expand Up @@ -59,23 +59,32 @@ 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鈥檚 built-in icons](#all-icons) next to the label.

```mdx
# src/content/docs/example.mdx

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
Expand Down
19 changes: 17 additions & 2 deletions 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) =>
Expand Down Expand Up @@ -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', () => {
Expand Down Expand Up @@ -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);
});
6 changes: 4 additions & 2 deletions 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>
8 changes: 6 additions & 2 deletions 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');
Expand All @@ -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"
Expand All @@ -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>
Expand Down Expand Up @@ -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);
Expand Down
11 changes: 8 additions & 3 deletions packages/starlight/user-components/rehype-tabs.ts
Expand Up @@ -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' {
Expand Down Expand Up @@ -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;
Expand Down

0 comments on commit 5f99a71

Please sign in to comment.