@dsn-starter-kit/components-react 4.0.0 → 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"Popover2.mjs","names":["toggleEvent"],"sources":["../../src/Popover/Popover.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport './Popover.css';\n\n/*\n * React 18 ondersteunt het `popover`-attribuut en `onToggle` niet natively in de\n * JSX-types. Module-uitbreiding voegt de ontbrekende types toe.\n */\ndeclare module 'react' {\n // eslint-disable-next-line @typescript-eslint/no-unused-vars\n interface HTMLAttributes<T> {\n popover?: 'auto' | 'manual' | '' | undefined;\n popoverTarget?: string | undefined;\n popoverTargetAction?: 'hide' | 'show' | 'toggle' | undefined;\n }\n}\n\n/*\n * HTMLElement.showPopover / hidePopover zijn nog niet in alle TypeScript DOM-libs.\n * Declaratie toegevoegd voor veilig gebruik.\n */\ndeclare global {\n interface HTMLElement {\n showPopover(): void;\n hidePopover(): void;\n }\n}\n\n// =============================================================================\n// Types\n// =============================================================================\n\ntype Placement = 'top' | 'bottom' | 'start' | 'end';\n\n// =============================================================================\n// Positioneringshelper\n// =============================================================================\n\nconst GAP = 4; // px afstand tussen trigger en popover\n\nfunction positionPopover(\n popover: HTMLElement,\n trigger: HTMLElement,\n placement: Placement\n): void {\n const triggerRect = trigger.getBoundingClientRect();\n // offsetWidth/offsetHeight — onaangepast door CSS transforms (zoals @starting-style scale).\n // getBoundingClientRect geeft de getransformeerde afmetingen, wat leidt tot onjuiste clamping.\n const popoverWidth = popover.offsetWidth;\n const popoverHeight = popover.offsetHeight;\n const popoverRect = { width: popoverWidth, height: popoverHeight };\n const isRTL =\n document.documentElement.dir === 'rtl' ||\n getComputedStyle(trigger).direction === 'rtl';\n\n let top: number;\n let left: number;\n\n switch (placement) {\n case 'bottom':\n top = triggerRect.bottom + GAP;\n left = isRTL ? triggerRect.right - popoverRect.width : triggerRect.left;\n break;\n case 'top':\n top = triggerRect.top - popoverRect.height - GAP;\n left = isRTL ? triggerRect.right - popoverRect.width : triggerRect.left;\n break;\n case 'end':\n top = triggerRect.top;\n left = isRTL\n ? triggerRect.left - popoverRect.width - GAP\n : triggerRect.right + GAP;\n break;\n case 'start':\n top = triggerRect.top;\n left = isRTL\n ? triggerRect.right + GAP\n : triggerRect.left - popoverRect.width - GAP;\n break;\n default:\n top = triggerRect.bottom + GAP;\n left = triggerRect.left;\n }\n\n // Klamp binnen het viewport (8px marge)\n const margin = 8;\n left = Math.max(\n margin,\n Math.min(left, window.innerWidth - popoverRect.width - margin)\n );\n top = Math.max(\n margin,\n Math.min(top, window.innerHeight - popoverRect.height - margin)\n );\n\n popover.style.top = `${top}px`;\n popover.style.left = `${left}px`;\n popover.style.right = 'auto';\n popover.style.bottom = 'auto';\n}\n\n// Zet focus op het eerste interactieve element in de popover\nfunction focusFirstInteractive(container: HTMLElement): void {\n const selector = [\n 'button:not(:disabled)',\n 'a[href]',\n 'input:not(:disabled)',\n 'select:not(:disabled)',\n 'textarea:not(:disabled)',\n '[tabindex]:not([tabindex=\"-1\"])',\n ].join(', ');\n const first = container.querySelector<HTMLElement>(selector);\n if (first) {\n first.focus();\n } else {\n container.focus();\n }\n}\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface PopoverContextValue {\n headingId: string;\n onClose?: () => void;\n}\n\nconst PopoverContext = React.createContext<PopoverContextValue>({\n headingId: '',\n});\n\n// =============================================================================\n// Popover (root)\n// =============================================================================\n\nexport interface PopoverProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'children'\n> {\n /**\n * Bepaalt of de popover getoond wordt.\n */\n isOpen: boolean;\n\n /**\n * Callback bij sluiten (Escape, klik buiten, sluitknop in header).\n */\n onClose?: () => void;\n\n /**\n * Referentie naar het triggerelement voor positionering en focus-herstel.\n */\n triggerRef: React.RefObject<HTMLElement>;\n\n /**\n * Gewenste plaatsing relatief aan het triggerelement.\n * @default 'bottom'\n */\n placement?: Placement;\n\n /**\n * Toegankelijke naam voor popovers zonder `PopoverHeader`/`PopoverHeading`.\n * Gebruik `aria-labelledby` via context wanneer er wél een heading is.\n */\n label?: string;\n\n /**\n * Subcomponenten: `PopoverHeader`, `PopoverBody`, `PopoverFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * Popover component\n * Lichtgewicht, contextgebonden overlay verankerd aan een triggerelement.\n *\n * Gebaseerd op de HTML Popover API (`popover=\"auto\"`) voor ingebakken\n * light-dismiss en top-layer gedrag. Positionering via JavaScript.\n *\n * @example\n * ```tsx\n * const triggerRef = useRef<HTMLButtonElement>(null);\n * const [isOpen, setIsOpen] = useState(false);\n *\n * <Button ref={triggerRef} onClick={() => setIsOpen(true)}>Opties</Button>\n * <Popover isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef} label=\"Opties\">\n * <PopoverBody>\n * <Menu>...</Menu>\n * </PopoverBody>\n * </Popover>\n * ```\n */\nexport const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(\n (\n {\n className,\n isOpen,\n onClose,\n triggerRef,\n placement = 'bottom',\n label,\n children,\n ...props\n },\n ref\n ) => {\n const internalRef = React.useRef<HTMLDivElement>(null);\n const popoverRef = (ref as React.RefObject<HTMLDivElement>) ?? internalRef;\n const headingId = React.useId();\n\n // Toon/verberg popover via de Popover API\n React.useEffect(() => {\n const el = popoverRef.current;\n if (!el) return;\n\n if (isOpen) {\n el.showPopover();\n const trigger = triggerRef.current;\n if (trigger) {\n positionPopover(el, trigger, placement);\n }\n focusFirstInteractive(el);\n } else {\n // hidePopover gooit een fout als de popover al gesloten is\n try {\n el.hidePopover();\n } catch {\n // popover was al gesloten — geen actie nodig\n }\n }\n }, [isOpen, placement, triggerRef, popoverRef]);\n\n // Synchroniseer aria-expanded op het triggerelement\n React.useEffect(() => {\n const trigger = triggerRef.current;\n if (!trigger) return;\n trigger.setAttribute('aria-expanded', isOpen ? 'true' : 'false');\n return () => {\n trigger.setAttribute('aria-expanded', 'false');\n };\n }, [isOpen, triggerRef]);\n\n // Verwerk het toggle-event van de Popover API (Escape / klik buiten binnen hetzelfde document)\n // onToggle is niet beschikbaar als JSX-prop op div in React 18 — gebruik addEventListener.\n // Gebruik een ref voor onClose zodat de listener stabiel blijft en nooit stale is.\n const onCloseRef = React.useRef(onClose);\n onCloseRef.current = onClose;\n\n React.useEffect(() => {\n const el = popoverRef.current;\n if (!el) return;\n\n const handleToggle = (event: Event) => {\n const toggleEvent = event as Event & { newState?: string };\n if (toggleEvent.newState === 'closed') {\n onCloseRef.current?.();\n triggerRef.current?.focus();\n }\n };\n\n el.addEventListener('toggle', handleToggle);\n return () => {\n el.removeEventListener('toggle', handleToggle);\n };\n }, [triggerRef, popoverRef]);\n\n // Fallback light-dismiss via pointerdown — vangt klikken buiten het popover op\n // in contexten waar de Popover API's native light-dismiss niet bereikbaar is\n // (bijv. iframe-grenzen, of oudere browsers zonder Popover API).\n React.useEffect(() => {\n if (!isOpen) return;\n const el = popoverRef.current;\n if (!el) return;\n\n const handlePointerDown = (event: PointerEvent) => {\n const target = event.target as Node;\n // Sluit niet als er op de popover zelf of het triggerelement geklikt wordt\n if (el.contains(target) || triggerRef.current?.contains(target)) return;\n onCloseRef.current?.();\n triggerRef.current?.focus();\n };\n\n document.addEventListener('pointerdown', handlePointerDown);\n return () => {\n document.removeEventListener('pointerdown', handlePointerDown);\n };\n }, [isOpen, triggerRef, popoverRef]);\n\n const classes = classNames(\n 'dsn-popover',\n `dsn-popover--placement-${placement}`,\n className\n );\n\n const ariaProps = label\n ? { 'aria-label': label }\n : { 'aria-labelledby': headingId };\n\n return (\n <PopoverContext.Provider value={{ headingId, onClose }}>\n <div\n ref={popoverRef}\n popover=\"auto\"\n className={classes}\n role=\"dialog\"\n aria-modal=\"false\"\n tabIndex={-1}\n {...ariaProps}\n {...props}\n >\n {children}\n </div>\n </PopoverContext.Provider>\n );\n }\n);\n\nPopover.displayName = 'Popover';\n\n// =============================================================================\n// PopoverHeader\n// =============================================================================\n\nexport interface PopoverHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `PopoverHeading`.\n * De sluitknop wordt automatisch geïnjecteerd.\n */\n children?: React.ReactNode;\n}\n\n/**\n * PopoverHeader\n * De headerstrook van de popover met heading en sluitknop.\n */\nexport const PopoverHeader = React.forwardRef<\n HTMLDivElement,\n PopoverHeaderProps\n>(({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(PopoverContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-popover__header', className)}\n {...props}\n >\n {children}\n <Button\n variant=\"subtle\"\n size=\"small\"\n iconOnly\n onClick={onClose}\n iconStart={<Icon name=\"x\" aria-hidden />}\n >\n Sluiten\n </Button>\n </div>\n );\n});\n\nPopoverHeader.displayName = 'PopoverHeader';\n\n// =============================================================================\n// PopoverHeading\n// =============================================================================\n\nexport interface PopoverHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie.\n * @default 2\n */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n\n /**\n * De zichtbare heading-tekst.\n */\n children?: React.ReactNode;\n}\n\n/**\n * PopoverHeading\n * De heading van de popover. ID wordt automatisch gegenereerd voor aria-labelledby.\n */\nexport const PopoverHeading = React.forwardRef<\n HTMLHeadingElement,\n PopoverHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId } = React.useContext(PopoverContext);\n const Tag = `h${level}` as React.ElementType;\n\n return (\n <Tag\n ref={ref}\n id={headingId}\n className={classNames('dsn-popover-heading', className)}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nPopoverHeading.displayName = 'PopoverHeading';\n\n// =============================================================================\n// PopoverBody\n// =============================================================================\n\nexport interface PopoverBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van de popover — willekeurige slot-content.\n */\n children?: React.ReactNode;\n}\n\n/**\n * PopoverBody\n * De inhoudssectie van de popover.\n */\nexport const PopoverBody = React.forwardRef<HTMLDivElement, PopoverBodyProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-popover__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nPopoverBody.displayName = 'PopoverBody';\n\n// =============================================================================\n// PopoverFooter\n// =============================================================================\n\nexport interface PopoverFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Actieknoppen of slotzin van de popover.\n */\n children?: React.ReactNode;\n}\n\n/**\n * PopoverFooter\n * De voettekst van de popover met actieknoppen.\n */\nexport const PopoverFooter = React.forwardRef<\n HTMLDivElement,\n PopoverFooterProps\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-popover__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nPopoverFooter.displayName = 'PopoverFooter';\n"],"mappings":"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"}
1
+ {"version":3,"file":"Popover2.mjs","names":[],"sources":["../../src/Popover/Popover.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { usePopover, type PopoverPlacement } from '../utils/popover';\nimport './Popover.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface PopoverContextValue {\n headingId: string;\n headingRef: React.MutableRefObject<HTMLHeadingElement | null>;\n onClose?: () => void;\n}\n\nconst PopoverContext = React.createContext<PopoverContextValue>({\n headingId: '',\n headingRef: { current: null },\n});\n\n// =============================================================================\n// Popover (root)\n// =============================================================================\n\nexport interface PopoverProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'children'\n> {\n /**\n * Bepaalt of de popover getoond wordt.\n */\n isOpen: boolean;\n\n /**\n * Callback bij sluiten (Escape, klik buiten, sluitknop in header).\n */\n onClose?: () => void;\n\n /**\n * Referentie naar het triggerelement voor positionering en focus-herstel.\n */\n triggerRef: React.RefObject<HTMLElement>;\n\n /**\n * Gewenste plaatsing relatief aan het triggerelement.\n * @default 'bottom'\n */\n placement?: PopoverPlacement;\n\n /**\n * Toegankelijke naam voor een popover zonder `PopoverHeader`/`PopoverHeading`.\n * Een zichtbare heading heeft de voorkeur; die koppelt via `aria-labelledby`.\n */\n label?: string;\n\n /**\n * Subcomponenten: `PopoverHeader`, `PopoverBody`, `PopoverFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * Popover component\n * Niet-modaal dialoogvenster verankerd aan een triggerelement\n * (`role=\"dialog\"`, `aria-modal=\"false\"`).\n *\n * Gebaseerd op de HTML Popover API (`popover=\"auto\"`) voor ingebakken\n * light-dismiss en top-layer gedrag. Positionering via JavaScript.\n *\n * Voor een lijst met acties of links: gebruik `PopoverMenu`. Een lijst is\n * geen dialoogvenster.\n *\n * @example\n * ```tsx\n * const triggerRef = useRef<HTMLButtonElement>(null);\n * const [isOpen, setIsOpen] = useState(false);\n *\n * <Button ref={triggerRef} onClick={() => setIsOpen((v) => !v)}>Filters</Button>\n * <Popover isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}>\n * <PopoverHeader>\n * <PopoverHeading>Filters</PopoverHeading>\n * </PopoverHeader>\n * <PopoverBody>...</PopoverBody>\n * </Popover>\n * ```\n */\nexport const Popover = React.forwardRef<HTMLDivElement, PopoverProps>(\n (\n {\n className,\n isOpen,\n onClose,\n triggerRef,\n placement = 'bottom',\n label,\n children,\n ...props\n },\n ref\n ) => {\n const internalRef = React.useRef<HTMLDivElement>(null);\n const popoverRef = (ref as React.RefObject<HTMLDivElement>) ?? internalRef;\n const headingId = React.useId();\n const headingRef = React.useRef<HTMLHeadingElement | null>(null);\n\n /*\n * Bij openen krijgt de heading de focus, niet de sluitknop: zo wordt de\n * titel als eerste voorgelezen, net als bij ModalDialog en Drawer. Zonder\n * heading krijgt de popover zelf de focus (die heeft `tabindex=\"-1\"`).\n * Zie docs/decisions/DR-2026-12-dialogs-focus-bij-openen-op-de-heading.md.\n */\n usePopover({\n popoverRef,\n triggerRef,\n isOpen,\n onClose,\n placement,\n initialFocusRef: headingRef,\n });\n\n const classes = classNames(\n 'dsn-popover',\n `dsn-popover--placement-${placement}`,\n className\n );\n\n const ariaProps = label\n ? { 'aria-label': label }\n : { 'aria-labelledby': headingId };\n\n return (\n <PopoverContext.Provider value={{ headingId, headingRef, onClose }}>\n <div\n ref={popoverRef}\n popover=\"auto\"\n className={classes}\n role=\"dialog\"\n aria-modal=\"false\"\n tabIndex={-1}\n {...ariaProps}\n {...props}\n >\n {children}\n </div>\n </PopoverContext.Provider>\n );\n }\n);\n\nPopover.displayName = 'Popover';\n\n// =============================================================================\n// PopoverHeader\n// =============================================================================\n\nexport interface PopoverHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `PopoverHeading`.\n * De sluitknop wordt automatisch geïnjecteerd.\n */\n children?: React.ReactNode;\n}\n\n/**\n * PopoverHeader\n * De headerstrook van de popover met heading en sluitknop.\n */\nexport const PopoverHeader = React.forwardRef<\n HTMLDivElement,\n PopoverHeaderProps\n>(({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(PopoverContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-popover__header', className)}\n {...props}\n >\n {children}\n <Button\n variant=\"subtle\"\n size=\"small\"\n iconOnly\n onClick={onClose}\n iconStart={<Icon name=\"x\" aria-hidden />}\n >\n Sluiten\n </Button>\n </div>\n );\n});\n\nPopoverHeader.displayName = 'PopoverHeader';\n\n// =============================================================================\n// PopoverHeading\n// =============================================================================\n\nexport interface PopoverHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie.\n * @default 2\n */\n level?: 1 | 2 | 3 | 4 | 5 | 6;\n\n /**\n * De zichtbare heading-tekst.\n */\n children?: React.ReactNode;\n}\n\n/**\n * PopoverHeading\n * De heading van de popover. ID wordt automatisch gegenereerd voor aria-labelledby.\n * Krijgt `tabindex=\"-1\"` zodat de Popover er bij openen de focus op kan zetten.\n */\nexport const PopoverHeading = React.forwardRef<\n HTMLHeadingElement,\n PopoverHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId, headingRef } = React.useContext(PopoverContext);\n const Tag = `h${level}` as React.ElementType;\n\n // De Popover heeft de heading nodig om er bij openen de focus op te zetten;\n // een eventuele meegegeven ref blijft daarnaast gewoon werken.\n const setRef = React.useCallback(\n (node: HTMLHeadingElement | null) => {\n headingRef.current = node;\n if (typeof ref === 'function') {\n ref(node);\n } else if (ref) {\n (ref as React.MutableRefObject<HTMLHeadingElement | null>).current =\n node;\n }\n },\n [headingRef, ref]\n );\n\n return (\n <Tag\n ref={setRef}\n id={headingId}\n className={classNames('dsn-popover-heading', className)}\n tabIndex={-1}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nPopoverHeading.displayName = 'PopoverHeading';\n\n// =============================================================================\n// PopoverBody\n// =============================================================================\n\nexport interface PopoverBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van de popover — willekeurige slot-content.\n */\n children?: React.ReactNode;\n}\n\n/**\n * PopoverBody\n * De inhoudssectie van de popover.\n */\nexport const PopoverBody = React.forwardRef<HTMLDivElement, PopoverBodyProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-popover__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nPopoverBody.displayName = 'PopoverBody';\n\n// =============================================================================\n// PopoverFooter\n// =============================================================================\n\nexport interface PopoverFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Actieknoppen of slotzin van de popover.\n */\n children?: React.ReactNode;\n}\n\n/**\n * PopoverFooter\n * De voettekst van de popover met actieknoppen.\n */\nexport const PopoverFooter = React.forwardRef<\n HTMLDivElement,\n PopoverFooterProps\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-popover__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nPopoverFooter.displayName = 'PopoverFooter';\n"],"mappings":";;;;;;;;AAiBA,MAAM,iBAAiB,MAAM,cAAmC;CAC9D,WAAW;CACX,YAAY,EAAE,SAAS,KAAK;AAC9B,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;AAoED,MAAa,UAAU,MAAM,YAEzB,EACE,WACA,QACA,SACA,YACA,YAAY,UACZ,OACA,UACA,GAAG,SAEL,QACG;CACH,MAAM,cAAc,MAAM,OAAuB,IAAI;CACrD,MAAM,aAAc,OAA2C;CAC/D,MAAM,YAAY,MAAM,MAAM;CAC9B,MAAM,aAAa,MAAM,OAAkC,IAAI;CAQ/D,WAAW;EACT;EACA;EACA;EACA;EACA;EACA,iBAAiB;CACnB,CAAC;CAED,MAAM,UAAU,WACd,eACA,0BAA0B,aAC1B,SACF;CAEA,MAAM,YAAY,QACd,EAAE,cAAc,MAAM,IACtB,EAAE,mBAAmB,UAAU;CAEnC,OACE,oBAAC,eAAe,UAAhB;EAAyB,OAAO;GAAE;GAAW;GAAY;EAAQ;EAC/D,UAAA,oBAAC,OAAD;GACE,KAAK;GACL,SAAQ;GACR,WAAW;GACX,MAAK;GACL,cAAW;GACX,UAAU;GACV,GAAI;GACJ,GAAI;GAEH;EACE,CAAA;CACkB,CAAA;AAE7B,CACF;AAEA,QAAQ,cAAc;;;;;AAkBtB,MAAa,gBAAgB,MAAM,YAGhC,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAC5C,MAAM,EAAE,YAAY,MAAM,WAAW,cAAc;CAEnD,OACE,qBAAC,OAAD;EACO;EACL,WAAW,WAAW,uBAAuB,SAAS;EACtD,GAAI;EAHN,UAAA,CAKG,UACD,oBAAC,QAAD;GACE,SAAQ;GACR,MAAK;GACL,UAAA;GACA,SAAS;GACT,WAAW,oBAAC,MAAD;IAAM,MAAK;IAAI,eAAA;GAAa,CAAA;GACxC,UAAA;EAEO,CAAA,CACL;;AAET,CAAC;AAED,cAAc,cAAc;;;;;;AAwB5B,MAAa,iBAAiB,MAAM,YAGjC,EAAE,WAAW,QAAQ,GAAG,UAAU,GAAG,SAAS,QAAQ;CACvD,MAAM,EAAE,WAAW,eAAe,MAAM,WAAW,cAAc;CACjE,MAAM,MAAM,IAAI;CAIhB,MAAM,SAAS,MAAM,aAClB,SAAoC;EACnC,WAAW,UAAU;EACrB,IAAI,OAAO,QAAQ,YACjB,IAAI,IAAI;OACH,IAAI,KACT,IAA2D,UACzD;CAEN,GACA,CAAC,YAAY,GAAG,CAClB;CAEA,OACE,oBAAC,KAAD;EACE,KAAK;EACL,IAAI;EACJ,WAAW,WAAW,uBAAuB,SAAS;EACtD,UAAU;EACV,GAAI;EAEH;CACE,CAAA;AAET,CAAC;AAED,eAAe,cAAc;;;;;AAiB7B,MAAa,cAAc,MAAM,YAC9B,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAC1C,OACE,oBAAC,OAAD;EACO;EACL,WAAW,WAAW,qBAAqB,SAAS;EACpD,GAAI;EAEH;CACE,CAAA;AAET,CACF;AAEA,YAAY,cAAc;;;;;AAiB1B,MAAa,gBAAgB,MAAM,YAGhC,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAC5C,OACE,oBAAC,OAAD;EACO;EACL,WAAW,WAAW,uBAAuB,SAAS;EACtD,GAAI;EAEH;CACE,CAAA;AAET,CAAC;AAED,cAAc,cAAc"}
@@ -0,0 +1,62 @@
1
+ import { PopoverPlacement } from "../utils/popover.mjs";
2
+ import React from "react";
3
+ //#region src/PopoverMenu/PopoverMenu.d.ts
4
+ interface PopoverMenuProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
5
+ /**
6
+ * Bepaalt of het menu getoond wordt.
7
+ */
8
+ isOpen: boolean;
9
+ /**
10
+ * Callback bij sluiten: Escape, klik buiten het menu, of het activeren van
11
+ * een `MenuButton` of `MenuLink`.
12
+ */
13
+ onClose?: () => void;
14
+ /**
15
+ * Referentie naar het triggerelement voor positionering, `aria-expanded`
16
+ * en focusherstel.
17
+ */
18
+ triggerRef: React.RefObject<HTMLElement>;
19
+ /**
20
+ * Gewenste plaatsing relatief aan het triggerelement.
21
+ * @default 'bottom'
22
+ */
23
+ placement?: PopoverPlacement;
24
+ /**
25
+ * Doorgegeven aan `Menu`: geeft aan dat de items een `iconStart` hebben.
26
+ * @default false
27
+ */
28
+ iconStart?: boolean;
29
+ /**
30
+ * `MenuButton`- en/of `MenuLink`-items
31
+ */
32
+ children?: React.ReactNode;
33
+ }
34
+ /**
35
+ * PopoverMenu component
36
+ * Lijst met acties of links in een zwevend paneel, geopend vanuit een
37
+ * triggerknop.
38
+ *
39
+ * Anders dan `Popover` is dit géén dialoogvenster: het paneel heeft geen rol,
40
+ * de inhoud is een gewone lijst (`<ul role="list">`). Ook bewust geen
41
+ * `role="menu"`: die rol belooft pijltjesnavigatie en zet screenreaders in
42
+ * een applicatiemodus. De context komt uit het label van de trigger.
43
+ *
44
+ * Het menu sluit zichzelf zodra een item wordt geactiveerd, en zet de focus
45
+ * dan terug op de trigger.
46
+ *
47
+ * @example
48
+ * ```tsx
49
+ * const triggerRef = useRef<HTMLButtonElement>(null);
50
+ * const [isOpen, setIsOpen] = useState(false);
51
+ *
52
+ * <Button ref={triggerRef} onClick={() => setIsOpen((v) => !v)}>Acties</Button>
53
+ * <PopoverMenu isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}>
54
+ * <MenuButton onClick={edit}>Bewerken</MenuButton>
55
+ * <MenuButton onClick={duplicate}>Dupliceren</MenuButton>
56
+ * </PopoverMenu>
57
+ * ```
58
+ */
59
+ declare const PopoverMenu: React.ForwardRefExoticComponent<PopoverMenuProps & React.RefAttributes<HTMLDivElement>>;
60
+ //#endregion
61
+ export { PopoverMenu, PopoverMenuProps };
62
+ //# sourceMappingURL=PopoverMenu.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PopoverMenu.d.mts","names":[],"sources":["../../src/PopoverMenu/PopoverMenu.tsx"],"mappings":";;;UAaiB,yBAAyB,KACxC,MAAM,eAAe;;;;EAMrB;;;;;EAMA;;;;;EAMA,YAAY,MAAM,UAAU;;;;;EAM5B,YAAY;;;;;EAMZ;;;;EAKA,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4BN,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA"}
@@ -0,0 +1,66 @@
1
+ import { Menu } from "../Menu/Menu2.mjs";
2
+ import { usePopover } from "../utils/popover.mjs";
3
+ /* empty css */
4
+ import React from "react";
5
+ import { classNames } from "@dsn-starter-kit/core";
6
+ import { jsx } from "react/jsx-runtime";
7
+ //#region src/PopoverMenu/PopoverMenu.tsx
8
+ const ITEM_SELECTOR = ".dsn-menu-button__button, .dsn-menu-link__link";
9
+ /**
10
+ * PopoverMenu component
11
+ * Lijst met acties of links in een zwevend paneel, geopend vanuit een
12
+ * triggerknop.
13
+ *
14
+ * Anders dan `Popover` is dit géén dialoogvenster: het paneel heeft geen rol,
15
+ * de inhoud is een gewone lijst (`<ul role="list">`). Ook bewust geen
16
+ * `role="menu"`: die rol belooft pijltjesnavigatie en zet screenreaders in
17
+ * een applicatiemodus. De context komt uit het label van de trigger.
18
+ *
19
+ * Het menu sluit zichzelf zodra een item wordt geactiveerd, en zet de focus
20
+ * dan terug op de trigger.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const triggerRef = useRef<HTMLButtonElement>(null);
25
+ * const [isOpen, setIsOpen] = useState(false);
26
+ *
27
+ * <Button ref={triggerRef} onClick={() => setIsOpen((v) => !v)}>Acties</Button>
28
+ * <PopoverMenu isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}>
29
+ * <MenuButton onClick={edit}>Bewerken</MenuButton>
30
+ * <MenuButton onClick={duplicate}>Dupliceren</MenuButton>
31
+ * </PopoverMenu>
32
+ * ```
33
+ */
34
+ const PopoverMenu = React.forwardRef(({ className, isOpen, onClose, triggerRef, placement = "bottom", iconStart = false, onClick, children, ...props }, ref) => {
35
+ const internalRef = React.useRef(null);
36
+ const popoverRef = ref ?? internalRef;
37
+ usePopover({
38
+ popoverRef,
39
+ triggerRef,
40
+ isOpen,
41
+ onClose,
42
+ placement
43
+ });
44
+ const handleClick = (event) => {
45
+ onClick?.(event);
46
+ if (event.target.closest(ITEM_SELECTOR)) onClose?.();
47
+ };
48
+ const classes = classNames("dsn-popover-menu", `dsn-popover-menu--placement-${placement}`, className);
49
+ return /* @__PURE__ */ jsx("div", {
50
+ ref: popoverRef,
51
+ popover: "auto",
52
+ className: classes,
53
+ onClick: handleClick,
54
+ ...props,
55
+ children: /* @__PURE__ */ jsx(Menu, {
56
+ role: "list",
57
+ iconStart,
58
+ children
59
+ })
60
+ });
61
+ });
62
+ PopoverMenu.displayName = "PopoverMenu";
63
+ //#endregion
64
+ export { PopoverMenu };
65
+
66
+ //# sourceMappingURL=PopoverMenu2.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PopoverMenu2.mjs","names":[],"sources":["../../src/PopoverMenu/PopoverMenu.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Menu } from '../Menu';\nimport { usePopover, type PopoverPlacement } from '../utils/popover';\nimport './PopoverMenu.css';\n\n/*\n * Klikken op deze elementen activeert een item en sluit het menu. De\n * uitklapknop van een MenuLink met sub-items staat er bewust niet bij: die\n * toont meer items en is dus geen keuze.\n */\nconst ITEM_SELECTOR = '.dsn-menu-button__button, .dsn-menu-link__link';\n\nexport interface PopoverMenuProps extends Omit<\n React.HTMLAttributes<HTMLDivElement>,\n 'children'\n> {\n /**\n * Bepaalt of het menu getoond wordt.\n */\n isOpen: boolean;\n\n /**\n * Callback bij sluiten: Escape, klik buiten het menu, of het activeren van\n * een `MenuButton` of `MenuLink`.\n */\n onClose?: () => void;\n\n /**\n * Referentie naar het triggerelement voor positionering, `aria-expanded`\n * en focusherstel.\n */\n triggerRef: React.RefObject<HTMLElement>;\n\n /**\n * Gewenste plaatsing relatief aan het triggerelement.\n * @default 'bottom'\n */\n placement?: PopoverPlacement;\n\n /**\n * Doorgegeven aan `Menu`: geeft aan dat de items een `iconStart` hebben.\n * @default false\n */\n iconStart?: boolean;\n\n /**\n * `MenuButton`- en/of `MenuLink`-items\n */\n children?: React.ReactNode;\n}\n\n/**\n * PopoverMenu component\n * Lijst met acties of links in een zwevend paneel, geopend vanuit een\n * triggerknop.\n *\n * Anders dan `Popover` is dit géén dialoogvenster: het paneel heeft geen rol,\n * de inhoud is een gewone lijst (`<ul role=\"list\">`). Ook bewust geen\n * `role=\"menu\"`: die rol belooft pijltjesnavigatie en zet screenreaders in\n * een applicatiemodus. De context komt uit het label van de trigger.\n *\n * Het menu sluit zichzelf zodra een item wordt geactiveerd, en zet de focus\n * dan terug op de trigger.\n *\n * @example\n * ```tsx\n * const triggerRef = useRef<HTMLButtonElement>(null);\n * const [isOpen, setIsOpen] = useState(false);\n *\n * <Button ref={triggerRef} onClick={() => setIsOpen((v) => !v)}>Acties</Button>\n * <PopoverMenu isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}>\n * <MenuButton onClick={edit}>Bewerken</MenuButton>\n * <MenuButton onClick={duplicate}>Dupliceren</MenuButton>\n * </PopoverMenu>\n * ```\n */\nexport const PopoverMenu = React.forwardRef<HTMLDivElement, PopoverMenuProps>(\n (\n {\n className,\n isOpen,\n onClose,\n triggerRef,\n placement = 'bottom',\n iconStart = false,\n onClick,\n children,\n ...props\n },\n ref\n ) => {\n const internalRef = React.useRef<HTMLDivElement>(null);\n const popoverRef = (ref as React.RefObject<HTMLDivElement>) ?? internalRef;\n\n usePopover({ popoverRef, triggerRef, isOpen, onClose, placement });\n\n // Sluit na het activeren van een item. Het eigen onClick van het item is\n // dan al uitgevoerd, want dit event bubbelt vanaf het item omhoog.\n const handleClick = (event: React.MouseEvent<HTMLDivElement>) => {\n onClick?.(event);\n const target = event.target as Element;\n if (target.closest(ITEM_SELECTOR)) {\n onClose?.();\n }\n };\n\n const classes = classNames(\n 'dsn-popover-menu',\n `dsn-popover-menu--placement-${placement}`,\n className\n );\n\n return (\n <div\n ref={popoverRef}\n popover=\"auto\"\n className={classes}\n onClick={handleClick}\n {...props}\n >\n <Menu role=\"list\" iconStart={iconStart}>\n {children}\n </Menu>\n </div>\n );\n }\n);\n\nPopoverMenu.displayName = 'PopoverMenu';\n"],"mappings":";;;;;;;AAWA,MAAM,gBAAgB;;;;;;;;;;;;;;;;;;;;;;;;;;AAkEtB,MAAa,cAAc,MAAM,YAE7B,EACE,WACA,QACA,SACA,YACA,YAAY,UACZ,YAAY,OACZ,SACA,UACA,GAAG,SAEL,QACG;CACH,MAAM,cAAc,MAAM,OAAuB,IAAI;CACrD,MAAM,aAAc,OAA2C;CAE/D,WAAW;EAAE;EAAY;EAAY;EAAQ;EAAS;CAAU,CAAC;CAIjE,MAAM,eAAe,UAA4C;EAC/D,UAAU,KAAK;EAEf,IADe,MAAM,OACV,QAAQ,aAAa,GAC9B,UAAU;CAEd;CAEA,MAAM,UAAU,WACd,oBACA,+BAA+B,aAC/B,SACF;CAEA,OACE,oBAAC,OAAD;EACE,KAAK;EACL,SAAQ;EACR,WAAW;EACX,SAAS;EACT,GAAI;EAEJ,UAAA,oBAAC,MAAD;GAAM,MAAK;GAAkB;GAC1B;EACG,CAAA;CACH,CAAA;AAET,CACF;AAEA,YAAY,cAAc"}
@@ -0,0 +1,2 @@
1
+ import { PopoverMenu, PopoverMenuProps } from "./PopoverMenu.mjs";
2
+ export { PopoverMenu, type PopoverMenuProps };
package/dist/index.cjs CHANGED
@@ -3562,14 +3562,28 @@ const Drawer = react.default.forwardRef(({ className, isOpen, onClose, modal = t
3562
3562
  const dialogRef = ref ?? internalRef;
3563
3563
  const headingId = react.default.useId();
3564
3564
  const headingRef = react.default.useRef(null);
3565
+ const popoverShownRef = react.default.useRef(false);
3566
+ const previousFocusRef = react.default.useRef(null);
3565
3567
  react.default.useEffect(() => {
3566
3568
  const dialog = dialogRef.current;
3567
3569
  if (!dialog) return;
3568
- if (isOpen && !dialog.open) {
3570
+ const isShown = dialog.open || popoverShownRef.current;
3571
+ if (isOpen && !isShown) {
3569
3572
  if (modal) dialog.showModal();
3570
- else dialog.show();
3573
+ else {
3574
+ previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
3575
+ dialog.showPopover();
3576
+ popoverShownRef.current = true;
3577
+ }
3571
3578
  (headingRef.current ?? dialog).focus();
3572
- } else if (!isOpen && dialog.open) dialog.close();
3579
+ } else if (!isOpen && isShown) {
3580
+ if (popoverShownRef.current) {
3581
+ const focusWasInside = dialog.contains(document.activeElement);
3582
+ dialog.hidePopover();
3583
+ popoverShownRef.current = false;
3584
+ if (focusWasInside) previousFocusRef.current?.focus();
3585
+ } else dialog.close();
3586
+ }
3573
3587
  }, [
3574
3588
  isOpen,
3575
3589
  modal,
@@ -3616,6 +3630,7 @@ const Drawer = react.default.forwardRef(({ className, isOpen, onClose, modal = t
3616
3630
  "aria-labelledby": headingId,
3617
3631
  onCancel: handleCancel,
3618
3632
  tabIndex: -1,
3633
+ popover: modal ? void 0 : "manual",
3619
3634
  ...props,
3620
3635
  children
3621
3636
  })
@@ -4711,6 +4726,7 @@ CardGroup.displayName = "CardGroup";
4711
4726
  //#region src/ModalDialog/ModalDialog.tsx
4712
4727
  const ModalDialogContext = react.default.createContext({
4713
4728
  headingId: "",
4729
+ bodyId: "",
4714
4730
  headingRef: { current: null }
4715
4731
  });
4716
4732
  /**
@@ -4724,7 +4740,7 @@ const ModalDialogContext = react.default.createContext({
4724
4740
  *
4725
4741
  * @example
4726
4742
  * ```tsx
4727
- * <ModalDialog isOpen={isOpen} onClose={() => setIsOpen(false)}>
4743
+ * <ModalDialog alert isOpen={isOpen} onClose={() => setIsOpen(false)}>
4728
4744
  * <ModalDialogHeader>
4729
4745
  * <ModalDialogHeading>Bevestig verwijderen</ModalDialogHeading>
4730
4746
  * </ModalDialogHeader>
@@ -4733,17 +4749,18 @@ const ModalDialogContext = react.default.createContext({
4733
4749
  * </ModalDialogBody>
4734
4750
  * <ModalDialogFooter>
4735
4751
  * <ActionGroup>
4736
- * <Button variant="negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
4752
+ * <Button variant="strong-negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
4737
4753
  * <Button variant="default" onClick={() => setIsOpen(false)}>Annuleren</Button>
4738
4754
  * </ActionGroup>
4739
4755
  * </ModalDialogFooter>
4740
4756
  * </ModalDialog>
4741
4757
  * ```
4742
4758
  */
4743
- const ModalDialog = react.default.forwardRef(({ className, isOpen, onClose, triggerRef, children, ...props }, ref) => {
4759
+ const ModalDialog = react.default.forwardRef(({ className, isOpen, onClose, triggerRef, alert = false, children, ...props }, ref) => {
4744
4760
  const internalRef = react.default.useRef(null);
4745
4761
  const dialogRef = ref ?? internalRef;
4746
4762
  const headingId = react.default.useId();
4763
+ const bodyId = react.default.useId();
4747
4764
  const headingRef = react.default.useRef(null);
4748
4765
  react.default.useEffect(() => {
4749
4766
  const dialog = dialogRef.current;
@@ -4770,13 +4787,16 @@ const ModalDialog = react.default.forwardRef(({ className, isOpen, onClose, trig
4770
4787
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ModalDialogContext.Provider, {
4771
4788
  value: {
4772
4789
  headingId,
4790
+ bodyId,
4773
4791
  headingRef,
4774
4792
  onClose
4775
4793
  },
4776
4794
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("dialog", {
4777
4795
  ref: dialogRef,
4778
4796
  className: classes,
4797
+ role: alert ? "alertdialog" : void 0,
4779
4798
  "aria-labelledby": headingId,
4799
+ "aria-describedby": alert ? bodyId : void 0,
4780
4800
  onCancel: handleCancel,
4781
4801
  tabIndex: -1,
4782
4802
  ...props,
@@ -4837,8 +4857,10 @@ ModalDialogHeading.displayName = "ModalDialogHeading";
4837
4857
  * De scrollbare inhoudssectie van het dialoogvenster met scroll-affordance schaduw.
4838
4858
  */
4839
4859
  const ModalDialogBody = react.default.forwardRef(({ className, children, ...props }, ref) => {
4860
+ const { bodyId } = react.default.useContext(ModalDialogContext);
4840
4861
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
4841
4862
  ref,
4863
+ id: bodyId,
4842
4864
  className: (0, _dsn_starter_kit_core.classNames)("dsn-modal-dialog__body", className),
4843
4865
  ...props,
4844
4866
  children
@@ -4859,7 +4881,7 @@ const ModalDialogFooter = react.default.forwardRef(({ className, children, ...pr
4859
4881
  });
4860
4882
  ModalDialogFooter.displayName = "ModalDialogFooter";
4861
4883
  //#endregion
4862
- //#region src/Popover/Popover.tsx
4884
+ //#region src/utils/popover.ts
4863
4885
  const GAP = 4;
4864
4886
  function positionPopover(popover, trigger, placement) {
4865
4887
  const triggerRect = trigger.getBoundingClientRect();
@@ -4912,39 +4934,32 @@ function focusFirstInteractive(container) {
4912
4934
  if (first) first.focus();
4913
4935
  else container.focus();
4914
4936
  }
4915
- const PopoverContext = react.default.createContext({ headingId: "" });
4916
4937
  /**
4917
- * Popover component
4918
- * Lichtgewicht, contextgebonden overlay verankerd aan een triggerelement.
4919
- *
4920
- * Gebaseerd op de HTML Popover API (`popover="auto"`) voor ingebakken
4921
- * light-dismiss en top-layer gedrag. Positionering via JavaScript.
4922
- *
4923
- * @example
4924
- * ```tsx
4925
- * const triggerRef = useRef<HTMLButtonElement>(null);
4926
- * const [isOpen, setIsOpen] = useState(false);
4927
- *
4928
- * <Button ref={triggerRef} onClick={() => setIsOpen(true)}>Opties</Button>
4929
- * <Popover isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef} label="Opties">
4930
- * <PopoverBody>
4931
- * <Menu>...</Menu>
4932
- * </PopoverBody>
4933
- * </Popover>
4934
- * ```
4938
+ * Opent en sluit een element met `popover="auto"` synchroon met `isOpen`,
4939
+ * positioneert het bij de trigger en houdt `aria-expanded` op de trigger bij.
4940
+ *
4941
+ * Bij openen gaat de focus naar `initialFocusRef` (of de popover zelf als die
4942
+ * ref leeg is), en anders naar het eerste interactieve element. Bij sluiten
4943
+ * gaat de focus alleen terug naar de trigger wanneer dat zinvol is: als de
4944
+ * focus op dat moment nog in de popover stond (Escape, een sluitknop, een
4945
+ * actie in de popover). Klikt de gebruiker ergens anders, dan blijft de focus
4946
+ * daar; terugspringen naar de trigger zou die klik tenietdoen.
4935
4947
  */
4936
- const Popover = react.default.forwardRef(({ className, isOpen, onClose, triggerRef, placement = "bottom", label, children, ...props }, ref) => {
4937
- const internalRef = react.default.useRef(null);
4938
- const popoverRef = ref ?? internalRef;
4939
- const headingId = react.default.useId();
4948
+ function usePopover({ popoverRef, triggerRef, isOpen, onClose, placement, initialFocusRef }) {
4949
+ const restoreFocusRef = react.default.useRef(false);
4950
+ const dismissedByPointerRef = react.default.useRef(false);
4951
+ const onCloseRef = react.default.useRef(onClose);
4952
+ onCloseRef.current = onClose;
4940
4953
  react.default.useEffect(() => {
4941
4954
  const el = popoverRef.current;
4942
4955
  if (!el) return;
4943
4956
  if (isOpen) {
4957
+ dismissedByPointerRef.current = false;
4944
4958
  el.showPopover();
4945
4959
  const trigger = triggerRef.current;
4946
4960
  if (trigger) positionPopover(el, trigger, placement);
4947
- focusFirstInteractive(el);
4961
+ if (initialFocusRef) (initialFocusRef.current ?? el).focus();
4962
+ else focusFirstInteractive(el);
4948
4963
  } else try {
4949
4964
  el.hidePopover();
4950
4965
  } catch {}
@@ -4952,7 +4967,8 @@ const Popover = react.default.forwardRef(({ className, isOpen, onClose, triggerR
4952
4967
  isOpen,
4953
4968
  placement,
4954
4969
  triggerRef,
4955
- popoverRef
4970
+ popoverRef,
4971
+ initialFocusRef
4956
4972
  ]);
4957
4973
  react.default.useEffect(() => {
4958
4974
  const trigger = triggerRef.current;
@@ -4962,19 +4978,23 @@ const Popover = react.default.forwardRef(({ className, isOpen, onClose, triggerR
4962
4978
  trigger.setAttribute("aria-expanded", "false");
4963
4979
  };
4964
4980
  }, [isOpen, triggerRef]);
4965
- const onCloseRef = react.default.useRef(onClose);
4966
- onCloseRef.current = onClose;
4967
4981
  react.default.useEffect(() => {
4968
4982
  const el = popoverRef.current;
4969
4983
  if (!el) return;
4984
+ const handleBeforeToggle = (event) => {
4985
+ if (event.newState === "closed") restoreFocusRef.current = !dismissedByPointerRef.current && el.contains(document.activeElement);
4986
+ };
4970
4987
  const handleToggle = (event) => {
4971
4988
  if (event.newState === "closed") {
4972
4989
  onCloseRef.current?.();
4973
- triggerRef.current?.focus();
4990
+ if (restoreFocusRef.current) triggerRef.current?.focus();
4991
+ restoreFocusRef.current = false;
4974
4992
  }
4975
4993
  };
4994
+ el.addEventListener("beforetoggle", handleBeforeToggle);
4976
4995
  el.addEventListener("toggle", handleToggle);
4977
4996
  return () => {
4997
+ el.removeEventListener("beforetoggle", handleBeforeToggle);
4978
4998
  el.removeEventListener("toggle", handleToggle);
4979
4999
  };
4980
5000
  }, [triggerRef, popoverRef]);
@@ -4985,8 +5005,8 @@ const Popover = react.default.forwardRef(({ className, isOpen, onClose, triggerR
4985
5005
  const handlePointerDown = (event) => {
4986
5006
  const target = event.target;
4987
5007
  if (el.contains(target) || triggerRef.current?.contains(target)) return;
5008
+ dismissedByPointerRef.current = true;
4988
5009
  onCloseRef.current?.();
4989
- triggerRef.current?.focus();
4990
5010
  };
4991
5011
  document.addEventListener("pointerdown", handlePointerDown);
4992
5012
  return () => {
@@ -4997,11 +5017,57 @@ const Popover = react.default.forwardRef(({ className, isOpen, onClose, triggerR
4997
5017
  triggerRef,
4998
5018
  popoverRef
4999
5019
  ]);
5020
+ }
5021
+ //#endregion
5022
+ //#region src/Popover/Popover.tsx
5023
+ const PopoverContext = react.default.createContext({
5024
+ headingId: "",
5025
+ headingRef: { current: null }
5026
+ });
5027
+ /**
5028
+ * Popover component
5029
+ * Niet-modaal dialoogvenster verankerd aan een triggerelement
5030
+ * (`role="dialog"`, `aria-modal="false"`).
5031
+ *
5032
+ * Gebaseerd op de HTML Popover API (`popover="auto"`) voor ingebakken
5033
+ * light-dismiss en top-layer gedrag. Positionering via JavaScript.
5034
+ *
5035
+ * Voor een lijst met acties of links: gebruik `PopoverMenu`. Een lijst is
5036
+ * geen dialoogvenster.
5037
+ *
5038
+ * @example
5039
+ * ```tsx
5040
+ * const triggerRef = useRef<HTMLButtonElement>(null);
5041
+ * const [isOpen, setIsOpen] = useState(false);
5042
+ *
5043
+ * <Button ref={triggerRef} onClick={() => setIsOpen((v) => !v)}>Filters</Button>
5044
+ * <Popover isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}>
5045
+ * <PopoverHeader>
5046
+ * <PopoverHeading>Filters</PopoverHeading>
5047
+ * </PopoverHeader>
5048
+ * <PopoverBody>...</PopoverBody>
5049
+ * </Popover>
5050
+ * ```
5051
+ */
5052
+ const Popover = react.default.forwardRef(({ className, isOpen, onClose, triggerRef, placement = "bottom", label, children, ...props }, ref) => {
5053
+ const internalRef = react.default.useRef(null);
5054
+ const popoverRef = ref ?? internalRef;
5055
+ const headingId = react.default.useId();
5056
+ const headingRef = react.default.useRef(null);
5057
+ usePopover({
5058
+ popoverRef,
5059
+ triggerRef,
5060
+ isOpen,
5061
+ onClose,
5062
+ placement,
5063
+ initialFocusRef: headingRef
5064
+ });
5000
5065
  const classes = (0, _dsn_starter_kit_core.classNames)("dsn-popover", `dsn-popover--placement-${placement}`, className);
5001
5066
  const ariaProps = label ? { "aria-label": label } : { "aria-labelledby": headingId };
5002
5067
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(PopoverContext.Provider, {
5003
5068
  value: {
5004
5069
  headingId,
5070
+ headingRef,
5005
5071
  onClose
5006
5072
  },
5007
5073
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
@@ -5045,14 +5111,21 @@ PopoverHeader.displayName = "PopoverHeader";
5045
5111
  /**
5046
5112
  * PopoverHeading
5047
5113
  * De heading van de popover. ID wordt automatisch gegenereerd voor aria-labelledby.
5114
+ * Krijgt `tabindex="-1"` zodat de Popover er bij openen de focus op kan zetten.
5048
5115
  */
5049
5116
  const PopoverHeading = react.default.forwardRef(({ className, level = 2, children, ...props }, ref) => {
5050
- const { headingId } = react.default.useContext(PopoverContext);
5117
+ const { headingId, headingRef } = react.default.useContext(PopoverContext);
5051
5118
  const Tag = `h${level}`;
5119
+ const setRef = react.default.useCallback((node) => {
5120
+ headingRef.current = node;
5121
+ if (typeof ref === "function") ref(node);
5122
+ else if (ref) ref.current = node;
5123
+ }, [headingRef, ref]);
5052
5124
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Tag, {
5053
- ref,
5125
+ ref: setRef,
5054
5126
  id: headingId,
5055
5127
  className: (0, _dsn_starter_kit_core.classNames)("dsn-popover-heading", className),
5128
+ tabIndex: -1,
5056
5129
  ...props,
5057
5130
  children
5058
5131
  });
@@ -5085,6 +5158,63 @@ const PopoverFooter = react.default.forwardRef(({ className, children, ...props
5085
5158
  });
5086
5159
  PopoverFooter.displayName = "PopoverFooter";
5087
5160
  //#endregion
5161
+ //#region src/PopoverMenu/PopoverMenu.tsx
5162
+ const ITEM_SELECTOR = ".dsn-menu-button__button, .dsn-menu-link__link";
5163
+ /**
5164
+ * PopoverMenu component
5165
+ * Lijst met acties of links in een zwevend paneel, geopend vanuit een
5166
+ * triggerknop.
5167
+ *
5168
+ * Anders dan `Popover` is dit géén dialoogvenster: het paneel heeft geen rol,
5169
+ * de inhoud is een gewone lijst (`<ul role="list">`). Ook bewust geen
5170
+ * `role="menu"`: die rol belooft pijltjesnavigatie en zet screenreaders in
5171
+ * een applicatiemodus. De context komt uit het label van de trigger.
5172
+ *
5173
+ * Het menu sluit zichzelf zodra een item wordt geactiveerd, en zet de focus
5174
+ * dan terug op de trigger.
5175
+ *
5176
+ * @example
5177
+ * ```tsx
5178
+ * const triggerRef = useRef<HTMLButtonElement>(null);
5179
+ * const [isOpen, setIsOpen] = useState(false);
5180
+ *
5181
+ * <Button ref={triggerRef} onClick={() => setIsOpen((v) => !v)}>Acties</Button>
5182
+ * <PopoverMenu isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}>
5183
+ * <MenuButton onClick={edit}>Bewerken</MenuButton>
5184
+ * <MenuButton onClick={duplicate}>Dupliceren</MenuButton>
5185
+ * </PopoverMenu>
5186
+ * ```
5187
+ */
5188
+ const PopoverMenu = react.default.forwardRef(({ className, isOpen, onClose, triggerRef, placement = "bottom", iconStart = false, onClick, children, ...props }, ref) => {
5189
+ const internalRef = react.default.useRef(null);
5190
+ const popoverRef = ref ?? internalRef;
5191
+ usePopover({
5192
+ popoverRef,
5193
+ triggerRef,
5194
+ isOpen,
5195
+ onClose,
5196
+ placement
5197
+ });
5198
+ const handleClick = (event) => {
5199
+ onClick?.(event);
5200
+ if (event.target.closest(ITEM_SELECTOR)) onClose?.();
5201
+ };
5202
+ const classes = (0, _dsn_starter_kit_core.classNames)("dsn-popover-menu", `dsn-popover-menu--placement-${placement}`, className);
5203
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
5204
+ ref: popoverRef,
5205
+ popover: "auto",
5206
+ className: classes,
5207
+ onClick: handleClick,
5208
+ ...props,
5209
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)(Menu, {
5210
+ role: "list",
5211
+ iconStart,
5212
+ children
5213
+ })
5214
+ });
5215
+ });
5216
+ PopoverMenu.displayName = "PopoverMenu";
5217
+ //#endregion
5088
5218
  //#region src/File/File.tsx
5089
5219
  /**
5090
5220
  * File component
@@ -5351,6 +5481,7 @@ exports.PopoverBody = PopoverBody;
5351
5481
  exports.PopoverFooter = PopoverFooter;
5352
5482
  exports.PopoverHeader = PopoverHeader;
5353
5483
  exports.PopoverHeading = PopoverHeading;
5484
+ exports.PopoverMenu = PopoverMenu;
5354
5485
  exports.PreHeading = PreHeading;
5355
5486
  exports.ProgressBar = ProgressBar;
5356
5487
  exports.Radio = Radio;