import type { Meta, StoryObj } from '@storybook/html'; import { Accordion, type AccordionOptions } from './Accordion'; import './accordion.css'; // The component's README, imported as raw Markdown (Vite `?raw`). It is surfaced // on the autodocs "Docs" page below so the reference documentation and the live // stories stay in one place. // // NOTE: the project ships `storybook-readme` for this purpose, but that addon // (v5, React/Vue era) is incompatible with Storybook 8 — it depends on the // removed `@storybook/addons` global-decorator API and on React, neither of // which exists here. We therefore use Storybook 8's native docs mechanism // (`autodocs` + `parameters.docs.description.component`) to render the README. import readme from './README.md?raw'; /** * Storybook stories for the Accordion component. * * The library is framework-agnostic vanilla TS, so each story's `render` * builds real DOM, then enhances it with `new Accordion(...)` — exactly how a * consumer would use it. The `@storybook/addon-a11y` panel runs axe-core * against the rendered output so regressions in the ARIA wiring surface here. */ interface Section { title: string; body: string; /** Start this section expanded (maps to `data-expanded` on the trigger). */ open?: boolean; } const SAMPLE_SECTIONS: readonly Section[] = [ { title: 'Personal information', body: 'Name, date of birth, and contact details.', open: true, }, { title: 'Billing address', body: 'Where invoices are sent.' }, { title: 'Shipping address', body: 'Where orders are delivered.' }, ]; /** * Build the semantic accordion markup and activate the component. * Returns the root element Storybook mounts. */ function renderAccordion( sections: readonly Section[], options: AccordionOptions = {}, ): HTMLElement { const root = document.createElement('div'); root.className = 'accordion'; root.setAttribute('data-accordion', ''); for (const section of sections) { const heading = document.createElement('h3'); heading.className = 'accordion__heading'; const trigger = document.createElement('button'); trigger.type = 'button'; trigger.className = 'accordion__trigger'; trigger.textContent = section.title; if (section.open) trigger.setAttribute('data-expanded', ''); heading.append(trigger); const panel = document.createElement('div'); panel.className = 'accordion__panel'; const p = document.createElement('p'); p.textContent = section.body; panel.append(p); root.append(heading, panel); } new Accordion(root, options); return root; } const meta: Meta = { title: 'Components/Accordion', // `autodocs` (see `.storybook/main.ts` → `docs.autodocs: "tag"`) generates a // "Docs" page for this component; the README is rendered at the top of it. tags: ['autodocs'], parameters: { docs: { description: { component: readme, }, }, }, argTypes: { // `table.defaultValue.summary` fills the Controls/Docs "Default" column. // `@storybook/html` cannot read these from the TS interface, so they are // kept in sync with the defaults in `AccordionOptions` by hand. allowMultiple: { control: 'boolean', description: 'Allow more than one panel open at once.', table: { defaultValue: { summary: 'false' } }, }, allowToggle: { control: 'boolean', description: 'In single-open mode, allow the open panel to be collapsed. Ignored when allowMultiple is true.', table: { defaultValue: { summary: 'true' } }, }, arrowNavigation: { control: 'boolean', description: 'Enable optional Up/Down/Home/End focus movement between headers.', table: { defaultValue: { summary: 'true' } }, }, wrapFocus: { control: 'boolean', description: 'Wrap arrow-key focus around the first/last header.', table: { defaultValue: { summary: 'true' } }, }, useRegionRole: { control: 'boolean', description: 'Expose each panel as an ARIA landmark region.', table: { defaultValue: { summary: 'true' } }, }, // No `control`: this is a callback, not something a user edits via the // Controls panel. `action: 'accordion:toggle'` tells the Actions addon // (part of `@storybook/addon-essentials`, see `.storybook/main.ts`) to // inject a spy function as this arg and log every call — index + expanded // state — to the Actions panel, mirroring the `accordion:toggle` event. onToggle: { action: 'accordion:toggle', description: 'Called on every expand/collapse with the same detail as the accordion:toggle event.', table: { category: 'Events', defaultValue: { summary: '() => {}' } }, }, }, render: (args) => renderAccordion(SAMPLE_SECTIONS, args), }; export default meta; type Story = StoryObj; /** * Default configuration: single panel open at a time, and the open panel can be * collapsed (nothing open) by clicking its own header. */ export const Default: Story = { args: { allowMultiple: false, allowToggle: true, arrowNavigation: true, wrapFocus: true, useRegionRole: true, }, };