import type { ReactNode } from "react" import { useEffect } from "react" import { FOCUS_RING_CLASS } from "../intentStyles.ts" import { toClassName } from "../toClassName.ts" import type { AccordionHeadingLevel, AccordionItem, } from "./Accordion.tsx" /** * Written out rather than interpolated into a tag name. React * accepts `` `h${level}` `` perfectly well; a reader and a grep do * not, and "which heading levels does this component emit" should be * answerable by looking. */ const HEADING_TAG: Record< AccordionHeadingLevel, "h2" | "h3" | "h4" | "h5" | "h6" > = { 2: "h2", 3: "h3", 4: "h4", 5: "h5", 6: "h6", } export type AccordionSectionProps = { headingLevel: AccordionHeadingLevel id: string isExpanded: boolean item: AccordionItem onToggle: (key: string) => void panelId: string /** * `VisibilityGroup.register` or `MultiplePicker.register`, * whichever kind the accordion is running. Both have this shape, * which is the point of the kinds being separate rather than one * configurable one. */ register: (key: string) => () => void } /** * Its own file for the same reason `TabTrigger` is: **registration * is an effect**, and an effect cannot run in a loop. * * A disabled section stays out of the group entirely — it is not a * member, so nothing can expand it, including a consumer passing its * key in `expandedKeys`. That is registration-as-membership again, * and it is the behaviour a `` cannot have at all. */ export const AccordionSection = ({ headingLevel, id, isExpanded, item, onToggle, panelId, register, }: AccordionSectionProps): ReactNode => { const { isDisabled = false, key, label } = item const Heading = HEADING_TAG[headingLevel] useEffect(() => { if (isDisabled) { return } return register(key) }, [isDisabled, key, register]) return (
{/* `group`, not `region`, and the difference is a landmark. The APG's accordion pattern says a panel *may* take `role="region"` and adds a caveat in the same breath: avoid it "in circumstances that create landmark region proliferation". A component library has to assume the worst case, and the worst case arrived on the first board — four accordions produced four landmarks named "Disc", and axe's `landmark-unique` failed the story. A real page with a job list does exactly that. Dropping the role entirely was the first fix and it was wrong: `aria-labelledby` on a roleless `
` is **inert**, so the panel would have lost its name rather than just its landmark — caught by `useAriaPropsSupportedByRole`, which is the one linter rule in this file that was right. `group` is the role the caveat is reaching for: "a set of UI objects **not** intended to be included in a page summary". It keeps `aria-labelledby` meaningful and stays out of the landmark list. */} {/* biome-ignore lint/a11y/useSemanticElements: the semantic element for `group` is `
`, which is a form-control grouping — it drags `` semantics and form-reset behaviour onto an accordion panel that contains prose. */}
) }