UNPKG

@astrojs/starlight

Version:

Build beautiful, high-performance documentation websites with Astro

117 lines (104 loc) 3.1 kB
import type { Element } from 'hast'; import { select } from 'hast-util-select'; import { rehype } from 'rehype'; import { CONTINUE, SKIP, visit } from 'unist-util-visit'; import type { StarlightIcon } from '../components/Icons'; interface Panel { panelId: string; tabId: string; label: string; icon?: StarlightIcon; } declare module 'vfile' { interface DataMap { panels: Panel[]; } } export const TabItemTagname = 'starlight-tab-item'; // https://github.com/adobe/react-spectrum/blob/99ca82e87ba2d7fdd54f5b49326fd242320b4b51/packages/%40react-aria/focus/src/FocusScope.tsx#L256-L275 const focusableElementSelectors = [ 'input:not([disabled]):not([type=hidden])', 'select:not([disabled])', 'textarea:not([disabled])', 'button:not([disabled])', 'a[href]', 'area[href]', 'summary', 'iframe', 'object', 'embed', 'audio[controls]', 'video[controls]', '[contenteditable]', '[tabindex]:not([disabled])', ] .map((selector) => `${selector}:not([hidden]):not([tabindex="-1"])`) .join(','); let count = 0; const getIDs = () => { const id = count++; return { panelId: 'tab-panel-' + id, tabId: 'tab-' + id }; }; /** * Rehype processor to extract tab panel data and turn each * `<starlight-tab-item>` into a `<div>` with the necessary * attributes. */ const tabsProcessor = rehype() .data('settings', { fragment: true }) .use(function tabs() { return (tree: Element, file) => { file.data.panels = []; let isFirst = true; visit(tree, 'element', (node) => { if (node.tagName !== TabItemTagname || !node.properties) { return CONTINUE; } const { dataLabel, dataIcon } = node.properties; const ids = getIDs(); const panel: Panel = { ...ids, label: String(dataLabel), }; if (dataIcon) panel.icon = String(dataIcon) as StarlightIcon; file.data.panels?.push(panel); // Remove `<TabItem>` props delete node.properties.dataLabel; delete node.properties.dataIcon; // Turn into `<div>` with required attributes node.tagName = 'div'; node.properties.id = ids.panelId; node.properties['aria-labelledby'] = ids.tabId; node.properties.role = 'tabpanel'; const focusableChild = select(focusableElementSelectors, node); // If the panel does not contain any focusable elements, include it in // the tab sequence of the page. if (!focusableChild) { node.properties.tabindex = 0; } // Hide all panels except the first // TODO: make initially visible tab configurable if (isFirst) { isFirst = false; } else { node.properties.hidden = true; } // Skip over the tab panel’s children. return SKIP; }); }; }); /** * Process tab panel items to extract data for the tab links and format * each tab panel correctly. * @param html Inner HTML passed to the `<Tabs>` component. */ export const processPanels = (html: string) => { const file = tabsProcessor.processSync({ value: html }); return { /** Data for each tab panel. */ panels: file.data.panels, /** Processed HTML for the tab panels. */ html: file.toString(), }; };