@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.
- package/dist/Drawer/Drawer.d.mts +5 -2
- package/dist/Drawer/Drawer.d.mts.map +1 -1
- package/dist/Drawer/Drawer2.mjs +18 -3
- package/dist/Drawer/Drawer2.mjs.map +1 -1
- package/dist/ModalDialog/ModalDialog.d.mts +13 -3
- package/dist/ModalDialog/ModalDialog.d.mts.map +1 -1
- package/dist/ModalDialog/ModalDialog2.mjs +10 -3
- package/dist/ModalDialog/ModalDialog2.mjs.map +1 -1
- package/dist/Popover/Popover.d.mts +16 -23
- package/dist/Popover/Popover.d.mts.map +1 -1
- package/dist/Popover/Popover2.mjs +33 -119
- package/dist/Popover/Popover2.mjs.map +1 -1
- package/dist/PopoverMenu/PopoverMenu.d.mts +62 -0
- package/dist/PopoverMenu/PopoverMenu.d.mts.map +1 -0
- package/dist/PopoverMenu/PopoverMenu2.mjs +66 -0
- package/dist/PopoverMenu/PopoverMenu2.mjs.map +1 -0
- package/dist/PopoverMenu/index.d.mts +2 -0
- package/dist/index.cjs +169 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +5147 -4522
- package/dist/index.d.cts +104 -17
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +3 -1
- package/dist/index.mjs +2 -1
- package/dist/utils/popover.d.mts +27 -0
- package/dist/utils/popover.d.mts.map +1 -0
- package/dist/utils/popover.mjs +142 -0
- package/dist/utils/popover.mjs.map +1 -0
- package/package.json +3 -3
package/dist/Drawer/Drawer.d.mts
CHANGED
|
@@ -14,8 +14,11 @@ interface DrawerProps extends Omit<React.HTMLAttributes<HTMLDialogElement>, 'chi
|
|
|
14
14
|
* Modaal of non-modaal gedrag.
|
|
15
15
|
* - `true` (standaard): opent via `.showModal()` — achtergrond geblokkeerd,
|
|
16
16
|
* focus blijft binnen het paneel, Escape sluit via `cancel`-event.
|
|
17
|
-
* - `false`: opent via `.
|
|
18
|
-
* gebruiker kan er met Tab naartoe,
|
|
17
|
+
* - `false`: `<dialog popover="manual">`, opent via `.showPopover()` —
|
|
18
|
+
* achtergrond blijft interactief en de gebruiker kan er met Tab naartoe,
|
|
19
|
+
* Escape sluit via `keydown`-listener. Het paneel staat in de top layer,
|
|
20
|
+
* dus een sticky header of een voorouder met `transform` valt er niet
|
|
21
|
+
* overheen.
|
|
19
22
|
* @default true
|
|
20
23
|
*/
|
|
21
24
|
modal?: boolean;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Drawer.d.mts","names":[],"sources":["../../src/Drawer/Drawer.tsx"],"mappings":";;UA0BiB,oBAAoB,KACnC,MAAM,eAAe;;;;EAMrB;;;;;EAMA
|
|
1
|
+
{"version":3,"file":"Drawer.d.mts","names":[],"sources":["../../src/Drawer/Drawer.tsx"],"mappings":";;UA0BiB,oBAAoB,KACnC,MAAM,eAAe;;;;EAMrB;;;;;EAMA;;;;;;;;;;;;EAaA;;;;;;;EAQA,aAAa,MAAM,UAAU;;;;;EAM7B;;;;;EAMA,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA6BN,QAAM,MAAA,0BAAA,cAAA,MAAA,cAAA;UA4JF,0BAA0B,MAAM,eAAe;;;;EAI9D,WAAW,MAAM;;;;;;cAON,cAAY,MAAA,0BAAA,oBAAA,MAAA,cAAA;UA+BR,2BAA2B,MAAM,eAAe;;;;;EAK/D;;;;EAKA,WAAW,MAAM;;;;;;;cAQN,eAAa,MAAA,0BAAA,qBAAA,MAAA,cAAA;UAyCT,wBAAwB,MAAM,eAAe;;;;EAI5D,WAAW,MAAM;;;;;;cAON,YAAU,MAAA,0BAAA,kBAAA,MAAA,cAAA;UAoBN,0BAA0B,MAAM,eAAe;;;;EAI9D,WAAW,MAAM;;;;;;cAON,cAAY,MAAA,0BAAA,oBAAA,MAAA,cAAA"}
|
package/dist/Drawer/Drawer2.mjs
CHANGED
|
@@ -41,14 +41,28 @@ const Drawer = React.forwardRef(({ className, isOpen, onClose, modal = true, tri
|
|
|
41
41
|
const dialogRef = ref ?? internalRef;
|
|
42
42
|
const headingId = React.useId();
|
|
43
43
|
const headingRef = React.useRef(null);
|
|
44
|
+
const popoverShownRef = React.useRef(false);
|
|
45
|
+
const previousFocusRef = React.useRef(null);
|
|
44
46
|
React.useEffect(() => {
|
|
45
47
|
const dialog = dialogRef.current;
|
|
46
48
|
if (!dialog) return;
|
|
47
|
-
|
|
49
|
+
const isShown = dialog.open || popoverShownRef.current;
|
|
50
|
+
if (isOpen && !isShown) {
|
|
48
51
|
if (modal) dialog.showModal();
|
|
49
|
-
else
|
|
52
|
+
else {
|
|
53
|
+
previousFocusRef.current = document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
54
|
+
dialog.showPopover();
|
|
55
|
+
popoverShownRef.current = true;
|
|
56
|
+
}
|
|
50
57
|
(headingRef.current ?? dialog).focus();
|
|
51
|
-
} else if (!isOpen &&
|
|
58
|
+
} else if (!isOpen && isShown) {
|
|
59
|
+
if (popoverShownRef.current) {
|
|
60
|
+
const focusWasInside = dialog.contains(document.activeElement);
|
|
61
|
+
dialog.hidePopover();
|
|
62
|
+
popoverShownRef.current = false;
|
|
63
|
+
if (focusWasInside) previousFocusRef.current?.focus();
|
|
64
|
+
} else dialog.close();
|
|
65
|
+
}
|
|
52
66
|
}, [
|
|
53
67
|
isOpen,
|
|
54
68
|
modal,
|
|
@@ -95,6 +109,7 @@ const Drawer = React.forwardRef(({ className, isOpen, onClose, modal = true, tri
|
|
|
95
109
|
"aria-labelledby": headingId,
|
|
96
110
|
onCancel: handleCancel,
|
|
97
111
|
tabIndex: -1,
|
|
112
|
+
popover: modal ? void 0 : "manual",
|
|
98
113
|
...props,
|
|
99
114
|
children
|
|
100
115
|
})
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Drawer2.mjs","names":[],"sources":["../../src/Drawer/Drawer.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { useFocusTrap } from '../utils/focusTrap';\nimport './Drawer.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface DrawerContextValue {\n headingId: string;\n headingRef: React.MutableRefObject<HTMLHeadingElement | null>;\n onClose?: () => void;\n}\n\nconst DrawerContext = React.createContext<DrawerContextValue>({\n headingId: '',\n headingRef: { current: null },\n});\n\n// =============================================================================\n// Drawer (root)\n// =============================================================================\n\nexport interface DrawerProps extends Omit<\n React.HTMLAttributes<HTMLDialogElement>,\n 'children'\n> {\n /**\n * Bepaalt of het zijpaneel getoond wordt.\n */\n isOpen: boolean;\n\n /**\n * Callback die wordt aangeroepen wanneer het zijpaneel sluit\n * (sluitknop, Escape-toets).\n */\n onClose?: () => void;\n\n /**\n * Modaal of non-modaal gedrag.\n * - `true` (standaard): opent via `.showModal()` — achtergrond geblokkeerd,\n * focus blijft binnen het paneel, Escape sluit via `cancel`-event.\n * - `false`: opent via `.show()` — achtergrond blijft interactief en de\n * gebruiker kan er met Tab naartoe, Escape sluit via `keydown`-listener.\n * @default true\n */\n modal?: boolean;\n\n /**\n * Ref naar het element dat het zijpaneel opent. Wanneer je die meegeeft,\n * houdt het zijpaneel `aria-expanded` op dat element synchroon met de\n * open-staat: `false` wanneer het paneel gesloten is, `true` wanneer het\n * open staat.\n */\n triggerRef?: React.RefObject<HTMLElement | null>;\n\n /**\n * De kant van de viewport vanwaar het zijpaneel inschuift.\n * @default \"right\"\n */\n side?: 'right' | 'left';\n\n /**\n * De subcomponenten van het zijpaneel:\n * `DrawerHeader`, `DrawerBody`, `DrawerFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * Drawer component\n * Zijpaneel dat vanuit links of rechts de viewport inschuift.\n *\n * Gebruik `modal={true}` (standaard) voor gefocuste taken waarbij de achtergrond\n * geblokkeerd moet worden. Gebruik `modal={false}` als de gebruiker de achtergrondpagina\n * nodig heeft voor context.\n *\n * @example\n * ```tsx\n * <Drawer isOpen={isOpen} onClose={() => setIsOpen(false)}>\n * <DrawerHeader>\n * <DrawerHeading>Filteropties</DrawerHeading>\n * </DrawerHeader>\n * <DrawerBody>\n * <Paragraph>Inhoud van het zijpaneel</Paragraph>\n * </DrawerBody>\n * <DrawerFooter>\n * <ActionGroup>\n * <Button variant=\"strong\" onClick={() => setIsOpen(false)}>Toepassen</Button>\n * <Button variant=\"default\" onClick={() => setIsOpen(false)}>Annuleren</Button>\n * </ActionGroup>\n * </DrawerFooter>\n * </Drawer>\n * ```\n */\nexport const Drawer = React.forwardRef<HTMLDialogElement, DrawerProps>(\n (\n {\n className,\n isOpen,\n onClose,\n modal = true,\n triggerRef,\n side = 'right',\n children,\n ...props\n },\n ref\n ) => {\n const internalRef = React.useRef<HTMLDialogElement>(null);\n const dialogRef =\n (ref as React.RefObject<HTMLDialogElement>) ?? internalRef;\n const headingId = React.useId();\n const headingRef = React.useRef<HTMLHeadingElement | null>(null);\n\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (isOpen && !dialog.open) {\n if (modal) {\n dialog.showModal();\n } else {\n dialog.show();\n }\n\n /*\n * Zet de focus op de heading, niet op de sluitknop. De browser zet de\n * focus bij het openen zelf op het eerste focusbare element, en dat is\n * de sluitknop. VoiceOver in Safari leest die knop dan wel voor, maar\n * niet de titel uit `aria-labelledby`, en omdat de sluitknop na de\n * titel staat kom je de titel bij verder lezen ook niet meer tegen.\n *\n * Met de focus op de heading (die `tabindex=\"-1\"` heeft) wordt de\n * titel als eerste voorgelezen en loopt de leesvolgorde daarna door\n * naar de sluitknop en de inhoud. Dit is het patroon uit de ARIA\n * Authoring Practices, en het houdt DOM-volgorde en visuele volgorde\n * gelijk.\n */\n (headingRef.current ?? dialog).focus();\n } else if (!isOpen && dialog.open) {\n dialog.close();\n }\n }, [isOpen, modal, dialogRef]);\n\n /*\n * Expliciete focus-trap bovenop de native trap van `.showModal()`.\n * Zie `utils/focusTrap.ts` voor waarom de native trap niet volstaat.\n * Non-modaal krijgt bewust geen trap: daar hoort de gebruiker juist\n * tussen paneel en achtergrondpagina te kunnen tabben.\n */\n useFocusTrap(dialogRef, isOpen && modal);\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 // Non-modaal: handmatige Escape-afhandeling via keydown\n React.useEffect(() => {\n if (modal) return;\n const dialog = dialogRef.current;\n if (!dialog || !isOpen) return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n onClose?.();\n }\n };\n\n dialog.addEventListener('keydown', handleKeyDown);\n return () => dialog.removeEventListener('keydown', handleKeyDown);\n }, [modal, isOpen, onClose, dialogRef]);\n\n // Modaal: native cancel-event (Escape via browser)\n const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n onClose?.();\n };\n\n const classes = classNames(\n 'dsn-drawer',\n side === 'left' ? 'dsn-drawer--side-left' : 'dsn-drawer--side-right',\n className\n );\n\n return (\n <DrawerContext.Provider value={{ headingId, headingRef, onClose }}>\n <dialog\n ref={dialogRef}\n className={classes}\n aria-labelledby={headingId}\n onCancel={handleCancel}\n tabIndex={-1}\n {...props}\n >\n {children}\n </dialog>\n </DrawerContext.Provider>\n );\n }\n);\n\nDrawer.displayName = 'Drawer';\n\n// =============================================================================\n// DrawerHeader\n// =============================================================================\n\nexport interface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `DrawerHeading`\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerHeader\n * De headerstrook van het zijpaneel met heading en sluitknop.\n */\nexport const DrawerHeader = React.forwardRef<HTMLDivElement, DrawerHeaderProps>(\n ({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(DrawerContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__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);\n\nDrawerHeader.displayName = 'DrawerHeader';\n\n// =============================================================================\n// DrawerHeading\n// =============================================================================\n\nexport interface DrawerHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie, niet visuele grootte.\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 * DrawerHeading\n * De heading van het zijpaneel. ID wordt automatisch gegenereerd voor aria-labelledby.\n * Krijgt `tabindex=\"-1\"` zodat de Drawer er bij openen de focus op kan zetten.\n */\nexport const DrawerHeading = React.forwardRef<\n HTMLHeadingElement,\n DrawerHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId, headingRef } = React.useContext(DrawerContext);\n const Tag = `h${level}` as React.ElementType;\n\n // De Drawer 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-drawer-heading', className)}\n tabIndex={-1}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nDrawerHeading.displayName = 'DrawerHeading';\n\n// =============================================================================\n// DrawerBody\n// =============================================================================\n\nexport interface DrawerBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van het zijpaneel\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerBody\n * De scrollbare inhoudssectie van het zijpaneel met scroll-affordance schaduw.\n */\nexport const DrawerBody = React.forwardRef<HTMLDivElement, DrawerBodyProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nDrawerBody.displayName = 'DrawerBody';\n\n// =============================================================================\n// DrawerFooter\n// =============================================================================\n\nexport interface DrawerFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De acties van het zijpaneel — doorgaans een `ActionGroup` met knoppen\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerFooter\n * De voettekst van het zijpaneel met actieknoppen.\n */\nexport const DrawerFooter = React.forwardRef<HTMLDivElement, DrawerFooterProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nDrawerFooter.displayName = 'DrawerFooter';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Drawer2.mjs","names":[],"sources":["../../src/Drawer/Drawer.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { useFocusTrap } from '../utils/focusTrap';\nimport './Drawer.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface DrawerContextValue {\n headingId: string;\n headingRef: React.MutableRefObject<HTMLHeadingElement | null>;\n onClose?: () => void;\n}\n\nconst DrawerContext = React.createContext<DrawerContextValue>({\n headingId: '',\n headingRef: { current: null },\n});\n\n// =============================================================================\n// Drawer (root)\n// =============================================================================\n\nexport interface DrawerProps extends Omit<\n React.HTMLAttributes<HTMLDialogElement>,\n 'children'\n> {\n /**\n * Bepaalt of het zijpaneel getoond wordt.\n */\n isOpen: boolean;\n\n /**\n * Callback die wordt aangeroepen wanneer het zijpaneel sluit\n * (sluitknop, Escape-toets).\n */\n onClose?: () => void;\n\n /**\n * Modaal of non-modaal gedrag.\n * - `true` (standaard): opent via `.showModal()` — achtergrond geblokkeerd,\n * focus blijft binnen het paneel, Escape sluit via `cancel`-event.\n * - `false`: `<dialog popover=\"manual\">`, opent via `.showPopover()` —\n * achtergrond blijft interactief en de gebruiker kan er met Tab naartoe,\n * Escape sluit via `keydown`-listener. Het paneel staat in de top layer,\n * dus een sticky header of een voorouder met `transform` valt er niet\n * overheen.\n * @default true\n */\n modal?: boolean;\n\n /**\n * Ref naar het element dat het zijpaneel opent. Wanneer je die meegeeft,\n * houdt het zijpaneel `aria-expanded` op dat element synchroon met de\n * open-staat: `false` wanneer het paneel gesloten is, `true` wanneer het\n * open staat.\n */\n triggerRef?: React.RefObject<HTMLElement | null>;\n\n /**\n * De kant van de viewport vanwaar het zijpaneel inschuift.\n * @default \"right\"\n */\n side?: 'right' | 'left';\n\n /**\n * De subcomponenten van het zijpaneel:\n * `DrawerHeader`, `DrawerBody`, `DrawerFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * Drawer component\n * Zijpaneel dat vanuit links of rechts de viewport inschuift.\n *\n * Gebruik `modal={true}` (standaard) voor gefocuste taken waarbij de achtergrond\n * geblokkeerd moet worden. Gebruik `modal={false}` als de gebruiker de achtergrondpagina\n * nodig heeft voor context.\n *\n * @example\n * ```tsx\n * <Drawer isOpen={isOpen} onClose={() => setIsOpen(false)}>\n * <DrawerHeader>\n * <DrawerHeading>Filteropties</DrawerHeading>\n * </DrawerHeader>\n * <DrawerBody>\n * <Paragraph>Inhoud van het zijpaneel</Paragraph>\n * </DrawerBody>\n * <DrawerFooter>\n * <ActionGroup>\n * <Button variant=\"strong\" onClick={() => setIsOpen(false)}>Toepassen</Button>\n * <Button variant=\"default\" onClick={() => setIsOpen(false)}>Annuleren</Button>\n * </ActionGroup>\n * </DrawerFooter>\n * </Drawer>\n * ```\n */\nexport const Drawer = React.forwardRef<HTMLDialogElement, DrawerProps>(\n (\n {\n className,\n isOpen,\n onClose,\n modal = true,\n triggerRef,\n side = 'right',\n children,\n ...props\n },\n ref\n ) => {\n const internalRef = React.useRef<HTMLDialogElement>(null);\n const dialogRef =\n (ref as React.RefObject<HTMLDialogElement>) ?? internalRef;\n const headingId = React.useId();\n const headingRef = React.useRef<HTMLHeadingElement | null>(null);\n\n /*\n * De non-modale variant is een popover (`popover=\"manual\"`) en geen via\n * `.show()` geopende dialog. `.show()` laat het paneel in de gewone\n * stapelvolgorde staan, waar een sticky PageHeader of een voorouder met\n * `transform` het kan afdekken; `.showPopover()` zet het in de top layer\n * zonder de rest van de pagina inert te maken.\n *\n * Een popover zet geen `open`-attribuut, dus die staat houden we zelf bij.\n * En anders dan `dialog.close()` herstelt `hidePopover()` de focus hier\n * niet: die blijft op een verborgen element staan. Daarom onthouden we bij\n * openen waar de focus stond, en zetten hem daar bij sluiten terug.\n */\n const popoverShownRef = React.useRef(false);\n const previousFocusRef = React.useRef<HTMLElement | null>(null);\n\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n const isShown = dialog.open || popoverShownRef.current;\n\n if (isOpen && !isShown) {\n if (modal) {\n dialog.showModal();\n } else {\n previousFocusRef.current =\n document.activeElement instanceof HTMLElement\n ? document.activeElement\n : null;\n dialog.showPopover();\n popoverShownRef.current = true;\n }\n\n /*\n * Zet de focus op de heading, niet op de sluitknop. De browser zet de\n * focus bij het openen zelf op het eerste focusbare element, en dat is\n * de sluitknop. VoiceOver in Safari leest die knop dan wel voor, maar\n * niet de titel uit `aria-labelledby`, en omdat de sluitknop na de\n * titel staat kom je de titel bij verder lezen ook niet meer tegen.\n *\n * Met de focus op de heading (die `tabindex=\"-1\"` heeft) wordt de\n * titel als eerste voorgelezen en loopt de leesvolgorde daarna door\n * naar de sluitknop en de inhoud. Dit is het patroon uit de ARIA\n * Authoring Practices, en het houdt DOM-volgorde en visuele volgorde\n * gelijk.\n *\n * Waarom niet het eerste veld of de minst destructieve knop: zie\n * docs/decisions/DR-2026-12-dialogs-focus-bij-openen-op-de-heading.md.\n */\n (headingRef.current ?? dialog).focus();\n } else if (!isOpen && isShown) {\n if (popoverShownRef.current) {\n // Alleen terugzetten als de focus nog in het paneel stond; heeft de\n // gebruiker intussen op de pagina ernaast geklikt, dan blijft hij daar.\n const focusWasInside = dialog.contains(document.activeElement);\n dialog.hidePopover();\n popoverShownRef.current = false;\n if (focusWasInside) {\n previousFocusRef.current?.focus();\n }\n } else {\n dialog.close();\n }\n }\n }, [isOpen, modal, dialogRef]);\n\n /*\n * Expliciete focus-trap bovenop de native trap van `.showModal()`.\n * Zie `utils/focusTrap.ts` voor waarom de native trap niet volstaat.\n * Non-modaal krijgt bewust geen trap: daar hoort de gebruiker juist\n * tussen paneel en achtergrondpagina te kunnen tabben.\n */\n useFocusTrap(dialogRef, isOpen && modal);\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 // Non-modaal: handmatige Escape-afhandeling via keydown\n React.useEffect(() => {\n if (modal) return;\n const dialog = dialogRef.current;\n if (!dialog || !isOpen) return;\n\n const handleKeyDown = (event: KeyboardEvent) => {\n if (event.key === 'Escape') {\n onClose?.();\n }\n };\n\n dialog.addEventListener('keydown', handleKeyDown);\n return () => dialog.removeEventListener('keydown', handleKeyDown);\n }, [modal, isOpen, onClose, dialogRef]);\n\n // Modaal: native cancel-event (Escape via browser)\n const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n onClose?.();\n };\n\n const classes = classNames(\n 'dsn-drawer',\n side === 'left' ? 'dsn-drawer--side-left' : 'dsn-drawer--side-right',\n className\n );\n\n return (\n <DrawerContext.Provider value={{ headingId, headingRef, onClose }}>\n <dialog\n ref={dialogRef}\n className={classes}\n aria-labelledby={headingId}\n onCancel={handleCancel}\n tabIndex={-1}\n popover={modal ? undefined : 'manual'}\n {...props}\n >\n {children}\n </dialog>\n </DrawerContext.Provider>\n );\n }\n);\n\nDrawer.displayName = 'Drawer';\n\n// =============================================================================\n// DrawerHeader\n// =============================================================================\n\nexport interface DrawerHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `DrawerHeading`\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerHeader\n * De headerstrook van het zijpaneel met heading en sluitknop.\n */\nexport const DrawerHeader = React.forwardRef<HTMLDivElement, DrawerHeaderProps>(\n ({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(DrawerContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__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);\n\nDrawerHeader.displayName = 'DrawerHeader';\n\n// =============================================================================\n// DrawerHeading\n// =============================================================================\n\nexport interface DrawerHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie, niet visuele grootte.\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 * DrawerHeading\n * De heading van het zijpaneel. ID wordt automatisch gegenereerd voor aria-labelledby.\n * Krijgt `tabindex=\"-1\"` zodat de Drawer er bij openen de focus op kan zetten.\n */\nexport const DrawerHeading = React.forwardRef<\n HTMLHeadingElement,\n DrawerHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId, headingRef } = React.useContext(DrawerContext);\n const Tag = `h${level}` as React.ElementType;\n\n // De Drawer 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-drawer-heading', className)}\n tabIndex={-1}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nDrawerHeading.displayName = 'DrawerHeading';\n\n// =============================================================================\n// DrawerBody\n// =============================================================================\n\nexport interface DrawerBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van het zijpaneel\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerBody\n * De scrollbare inhoudssectie van het zijpaneel met scroll-affordance schaduw.\n */\nexport const DrawerBody = React.forwardRef<HTMLDivElement, DrawerBodyProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nDrawerBody.displayName = 'DrawerBody';\n\n// =============================================================================\n// DrawerFooter\n// =============================================================================\n\nexport interface DrawerFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De acties van het zijpaneel — doorgaans een `ActionGroup` met knoppen\n */\n children?: React.ReactNode;\n}\n\n/**\n * DrawerFooter\n * De voettekst van het zijpaneel met actieknoppen.\n */\nexport const DrawerFooter = React.forwardRef<HTMLDivElement, DrawerFooterProps>(\n ({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-drawer__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n }\n);\n\nDrawerFooter.displayName = 'DrawerFooter';\n"],"mappings":"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"}
|
|
@@ -8,7 +8,8 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
|
|
|
8
8
|
isOpen: boolean;
|
|
9
9
|
/**
|
|
10
10
|
* Callback die wordt aangeroepen wanneer het dialoogvenster sluit
|
|
11
|
-
* (sluitknop
|
|
11
|
+
* (sluitknop of Escape-toets). Een klik op de backdrop sluit het venster
|
|
12
|
+
* bewust niet.
|
|
12
13
|
*/
|
|
13
14
|
onClose?: () => void;
|
|
14
15
|
/**
|
|
@@ -18,6 +19,15 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
|
|
|
18
19
|
* open staat.
|
|
19
20
|
*/
|
|
20
21
|
triggerRef?: React.RefObject<HTMLElement | null>;
|
|
22
|
+
/**
|
|
23
|
+
* Markeert het venster als alertdialog: een dringende melding die een
|
|
24
|
+
* reactie vraagt, zoals de bevestiging van een destructieve actie. Zet
|
|
25
|
+
* `role="alertdialog"` op het `<dialog>` en koppelt de `ModalDialogBody`
|
|
26
|
+
* via `aria-describedby`, zodat de boodschap bij openen wordt voorgelezen.
|
|
27
|
+
* De focus komt net als anders op de heading.
|
|
28
|
+
* @default false
|
|
29
|
+
*/
|
|
30
|
+
alert?: boolean;
|
|
21
31
|
/**
|
|
22
32
|
* De subcomponenten van het dialoogvenster:
|
|
23
33
|
* `ModalDialogHeader`, `ModalDialogBody`, `ModalDialogFooter`
|
|
@@ -35,7 +45,7 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
|
|
|
35
45
|
*
|
|
36
46
|
* @example
|
|
37
47
|
* ```tsx
|
|
38
|
-
* <ModalDialog isOpen={isOpen} onClose={() => setIsOpen(false)}>
|
|
48
|
+
* <ModalDialog alert isOpen={isOpen} onClose={() => setIsOpen(false)}>
|
|
39
49
|
* <ModalDialogHeader>
|
|
40
50
|
* <ModalDialogHeading>Bevestig verwijderen</ModalDialogHeading>
|
|
41
51
|
* </ModalDialogHeader>
|
|
@@ -44,7 +54,7 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
|
|
|
44
54
|
* </ModalDialogBody>
|
|
45
55
|
* <ModalDialogFooter>
|
|
46
56
|
* <ActionGroup>
|
|
47
|
-
* <Button variant="negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
|
|
57
|
+
* <Button variant="strong-negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
|
|
48
58
|
* <Button variant="default" onClick={() => setIsOpen(false)}>Annuleren</Button>
|
|
49
59
|
* </ActionGroup>
|
|
50
60
|
* </ModalDialogFooter>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModalDialog.d.mts","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"ModalDialog.d.mts","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"mappings":";;UA4BiB,yBAAyB,KACxC,MAAM,eAAe;;;;;EAOrB;;;;;;EAOA;;;;;;;EAQA,aAAa,MAAM,UAAU;;;;;;;;;EAU7B;;;;;EAMA,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8BN,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA;UAqGP,+BAA+B,MAAM,eAAe;;;;EAInE,WAAW,MAAM;;;;;;cAON,mBAAiB,MAAA,0BAAA,yBAAA,MAAA,cAAA;UAgCb,gCAAgC,MAAM,eAAe;;;;;EAKpE;;;;EAKA,WAAW,MAAM;;;;;;;cAQN,oBAAkB,MAAA,0BAAA,0BAAA,MAAA,cAAA;UAyCd,6BAA6B,MAAM,eAAe;;;;EAIjE,WAAW,MAAM;;;;;;cAON,iBAAe,MAAA,0BAAA,uBAAA,MAAA,cAAA;UAwBX,+BAA+B,MAAM,eAAe;;;;EAInE,WAAW,MAAM;;;;;;cAON,mBAAiB,MAAA,0BAAA,yBAAA,MAAA,cAAA"}
|
|
@@ -8,6 +8,7 @@ import { jsx, jsxs } from "react/jsx-runtime";
|
|
|
8
8
|
//#region src/ModalDialog/ModalDialog.tsx
|
|
9
9
|
const ModalDialogContext = React.createContext({
|
|
10
10
|
headingId: "",
|
|
11
|
+
bodyId: "",
|
|
11
12
|
headingRef: { current: null }
|
|
12
13
|
});
|
|
13
14
|
/**
|
|
@@ -21,7 +22,7 @@ const ModalDialogContext = React.createContext({
|
|
|
21
22
|
*
|
|
22
23
|
* @example
|
|
23
24
|
* ```tsx
|
|
24
|
-
* <ModalDialog isOpen={isOpen} onClose={() => setIsOpen(false)}>
|
|
25
|
+
* <ModalDialog alert isOpen={isOpen} onClose={() => setIsOpen(false)}>
|
|
25
26
|
* <ModalDialogHeader>
|
|
26
27
|
* <ModalDialogHeading>Bevestig verwijderen</ModalDialogHeading>
|
|
27
28
|
* </ModalDialogHeader>
|
|
@@ -30,17 +31,18 @@ const ModalDialogContext = React.createContext({
|
|
|
30
31
|
* </ModalDialogBody>
|
|
31
32
|
* <ModalDialogFooter>
|
|
32
33
|
* <ActionGroup>
|
|
33
|
-
* <Button variant="negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
|
|
34
|
+
* <Button variant="strong-negative" onClick={() => setIsOpen(false)}>Verwijderen</Button>
|
|
34
35
|
* <Button variant="default" onClick={() => setIsOpen(false)}>Annuleren</Button>
|
|
35
36
|
* </ActionGroup>
|
|
36
37
|
* </ModalDialogFooter>
|
|
37
38
|
* </ModalDialog>
|
|
38
39
|
* ```
|
|
39
40
|
*/
|
|
40
|
-
const ModalDialog = React.forwardRef(({ className, isOpen, onClose, triggerRef, children, ...props }, ref) => {
|
|
41
|
+
const ModalDialog = React.forwardRef(({ className, isOpen, onClose, triggerRef, alert = false, children, ...props }, ref) => {
|
|
41
42
|
const internalRef = React.useRef(null);
|
|
42
43
|
const dialogRef = ref ?? internalRef;
|
|
43
44
|
const headingId = React.useId();
|
|
45
|
+
const bodyId = React.useId();
|
|
44
46
|
const headingRef = React.useRef(null);
|
|
45
47
|
React.useEffect(() => {
|
|
46
48
|
const dialog = dialogRef.current;
|
|
@@ -67,13 +69,16 @@ const ModalDialog = React.forwardRef(({ className, isOpen, onClose, triggerRef,
|
|
|
67
69
|
return /* @__PURE__ */ jsx(ModalDialogContext.Provider, {
|
|
68
70
|
value: {
|
|
69
71
|
headingId,
|
|
72
|
+
bodyId,
|
|
70
73
|
headingRef,
|
|
71
74
|
onClose
|
|
72
75
|
},
|
|
73
76
|
children: /* @__PURE__ */ jsx("dialog", {
|
|
74
77
|
ref: dialogRef,
|
|
75
78
|
className: classes,
|
|
79
|
+
role: alert ? "alertdialog" : void 0,
|
|
76
80
|
"aria-labelledby": headingId,
|
|
81
|
+
"aria-describedby": alert ? bodyId : void 0,
|
|
77
82
|
onCancel: handleCancel,
|
|
78
83
|
tabIndex: -1,
|
|
79
84
|
...props,
|
|
@@ -134,8 +139,10 @@ ModalDialogHeading.displayName = "ModalDialogHeading";
|
|
|
134
139
|
* De scrollbare inhoudssectie van het dialoogvenster met scroll-affordance schaduw.
|
|
135
140
|
*/
|
|
136
141
|
const ModalDialogBody = React.forwardRef(({ className, children, ...props }, ref) => {
|
|
142
|
+
const { bodyId } = React.useContext(ModalDialogContext);
|
|
137
143
|
return /* @__PURE__ */ jsx("div", {
|
|
138
144
|
ref,
|
|
145
|
+
id: bodyId,
|
|
139
146
|
className: classNames("dsn-modal-dialog__body", className),
|
|
140
147
|
...props,
|
|
141
148
|
children
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ModalDialog2.mjs","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { useFocusTrap } from '../utils/focusTrap';\nimport './ModalDialog.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface ModalDialogContextValue {\n headingId: string;\n headingRef: React.MutableRefObject<HTMLHeadingElement | null>;\n onClose?: () => void;\n}\n\nconst ModalDialogContext = React.createContext<ModalDialogContextValue>({\n headingId: '',\n headingRef: { current: null },\n});\n\n// =============================================================================\n// ModalDialog (root)\n// =============================================================================\n\nexport interface ModalDialogProps extends Omit<\n React.HTMLAttributes<HTMLDialogElement>,\n 'children'\n> {\n /**\n * Bepaalt of het dialoogvenster getoond wordt.\n * Het dialoogvenster wordt via `.showModal()` geopend — nooit via `.show()`.\n */\n isOpen: boolean;\n\n /**\n * Callback die wordt aangeroepen wanneer het dialoogvenster sluit\n * (sluitknop, Escape-toets of klik buiten het venster).\n */\n onClose?: () => void;\n\n /**\n * Ref naar het element dat het dialoogvenster opent. Wanneer je die meegeeft,\n * houdt het dialoogvenster `aria-expanded` op dat element synchroon met de\n * open-staat: `false` wanneer het venster gesloten is, `true` wanneer het\n * open staat.\n */\n triggerRef?: React.RefObject<HTMLElement | null>;\n\n /**\n * De subcomponenten van het dialoogvenster:\n * `ModalDialogHeader`, `ModalDialogBody`, `ModalDialogFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialog component\n * Modaal dialoogvenster gebaseerd op het native `<dialog>` element.\n *\n * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop), nooit `.show()`.\n * Dit garandeert aria-modal semantiek en het inert-attribuut op de achtergrond.\n * De focus blijft binnen het venster via de native focus-trap plus een\n * expliciete trap, en komt bij openen op de heading te staan.\n *\n * @example\n * ```tsx\n * <ModalDialog isOpen={isOpen} onClose={() => setIsOpen(false)}>\n * <ModalDialogHeader>\n * <ModalDialogHeading>Bevestig verwijderen</ModalDialogHeading>\n * </ModalDialogHeader>\n * <ModalDialogBody>\n * <Paragraph>Weet u zeker dat u dit item wilt verwijderen?</Paragraph>\n * </ModalDialogBody>\n * <ModalDialogFooter>\n * <ActionGroup>\n * <Button variant=\"negative\" onClick={() => setIsOpen(false)}>Verwijderen</Button>\n * <Button variant=\"default\" onClick={() => setIsOpen(false)}>Annuleren</Button>\n * </ActionGroup>\n * </ModalDialogFooter>\n * </ModalDialog>\n * ```\n */\nexport const ModalDialog = React.forwardRef<\n HTMLDialogElement,\n ModalDialogProps\n>(({ className, isOpen, onClose, triggerRef, children, ...props }, ref) => {\n const internalRef = React.useRef<HTMLDialogElement>(null);\n const dialogRef = (ref as React.RefObject<HTMLDialogElement>) ?? internalRef;\n const headingId = React.useId();\n const headingRef = React.useRef<HTMLHeadingElement | null>(null);\n\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (isOpen && !dialog.open) {\n dialog.showModal();\n\n /*\n * Zet de focus op de heading, niet op de sluitknop. De browser zet de\n * focus bij het openen zelf op het eerste focusbare element, en dat is\n * de sluitknop. VoiceOver in Safari leest die knop dan wel voor, maar\n * niet de titel uit `aria-labelledby`, en omdat de sluitknop na de\n * titel staat kom je de titel bij verder lezen ook niet meer tegen.\n *\n * Met de focus op de heading (die `tabindex=\"-1\"` heeft) wordt de titel\n * als eerste voorgelezen en loopt de leesvolgorde daarna door naar de\n * sluitknop en de inhoud. Dit is het patroon uit de ARIA Authoring\n * Practices, en het houdt DOM-volgorde en visuele volgorde gelijk.\n */\n (headingRef.current ?? dialog).focus();\n } else if (!isOpen && dialog.open) {\n dialog.close();\n }\n }, [isOpen, dialogRef]);\n\n /*\n * Expliciete focus-trap bovenop de native trap van `.showModal()`.\n * Zie `utils/focusTrap.ts` voor waarom de native trap niet volstaat.\n */\n useFocusTrap(dialogRef, isOpen);\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 const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n onClose?.();\n };\n\n const classes = classNames('dsn-modal-dialog', className);\n\n return (\n <ModalDialogContext.Provider value={{ headingId, headingRef, onClose }}>\n <dialog\n ref={dialogRef}\n className={classes}\n aria-labelledby={headingId}\n onCancel={handleCancel}\n tabIndex={-1}\n {...props}\n >\n {children}\n </dialog>\n </ModalDialogContext.Provider>\n );\n});\n\nModalDialog.displayName = 'ModalDialog';\n\n// =============================================================================\n// ModalDialogHeader\n// =============================================================================\n\nexport interface ModalDialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `ModalDialogHeading`\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogHeader\n * De headerstrook van het dialoogvenster met heading en sluitknop.\n */\nexport const ModalDialogHeader = React.forwardRef<\n HTMLDivElement,\n ModalDialogHeaderProps\n>(({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(ModalDialogContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__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\nModalDialogHeader.displayName = 'ModalDialogHeader';\n\n// =============================================================================\n// ModalDialogHeading\n// =============================================================================\n\nexport interface ModalDialogHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie, niet visuele grootte.\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 * ModalDialogHeading\n * De heading van het dialoogvenster. ID wordt automatisch gegenereerd voor aria-labelledby.\n * Krijgt `tabindex=\"-1\"` zodat het ModalDialog er bij openen de focus op kan zetten.\n */\nexport const ModalDialogHeading = React.forwardRef<\n HTMLHeadingElement,\n ModalDialogHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId, headingRef } = React.useContext(ModalDialogContext);\n const Tag = `h${level}` as React.ElementType;\n\n // Het ModalDialog heeft de heading nodig om er bij openen de focus op te\n // zetten; 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-modal-dialog-heading', className)}\n tabIndex={-1}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nModalDialogHeading.displayName = 'ModalDialogHeading';\n\n// =============================================================================\n// ModalDialogBody\n// =============================================================================\n\nexport interface ModalDialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van het dialoogvenster\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogBody\n * De scrollbare inhoudssectie van het dialoogvenster met scroll-affordance schaduw.\n */\nexport const ModalDialogBody = React.forwardRef<\n HTMLDivElement,\n ModalDialogBodyProps\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nModalDialogBody.displayName = 'ModalDialogBody';\n\n// =============================================================================\n// ModalDialogFooter\n// =============================================================================\n\nexport interface ModalDialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De acties van het dialoogvenster — doorgaans een `ActionGroup` met knoppen\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogFooter\n * De voettekst van het dialoogvenster met actieknoppen.\n */\nexport const ModalDialogFooter = React.forwardRef<\n HTMLDivElement,\n ModalDialogFooterProps\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nModalDialogFooter.displayName = 'ModalDialogFooter';\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ModalDialog2.mjs","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames } from '@dsn-starter-kit/core';\nimport { Button } from '../Button';\nimport { Icon } from '../Icon';\nimport { useFocusTrap } from '../utils/focusTrap';\nimport './ModalDialog.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface ModalDialogContextValue {\n headingId: string;\n bodyId: string;\n headingRef: React.MutableRefObject<HTMLHeadingElement | null>;\n onClose?: () => void;\n}\n\nconst ModalDialogContext = React.createContext<ModalDialogContextValue>({\n headingId: '',\n bodyId: '',\n headingRef: { current: null },\n});\n\n// =============================================================================\n// ModalDialog (root)\n// =============================================================================\n\nexport interface ModalDialogProps extends Omit<\n React.HTMLAttributes<HTMLDialogElement>,\n 'children'\n> {\n /**\n * Bepaalt of het dialoogvenster getoond wordt.\n * Het dialoogvenster wordt via `.showModal()` geopend — nooit via `.show()`.\n */\n isOpen: boolean;\n\n /**\n * Callback die wordt aangeroepen wanneer het dialoogvenster sluit\n * (sluitknop of Escape-toets). Een klik op de backdrop sluit het venster\n * bewust niet.\n */\n onClose?: () => void;\n\n /**\n * Ref naar het element dat het dialoogvenster opent. Wanneer je die meegeeft,\n * houdt het dialoogvenster `aria-expanded` op dat element synchroon met de\n * open-staat: `false` wanneer het venster gesloten is, `true` wanneer het\n * open staat.\n */\n triggerRef?: React.RefObject<HTMLElement | null>;\n\n /**\n * Markeert het venster als alertdialog: een dringende melding die een\n * reactie vraagt, zoals de bevestiging van een destructieve actie. Zet\n * `role=\"alertdialog\"` op het `<dialog>` en koppelt de `ModalDialogBody`\n * via `aria-describedby`, zodat de boodschap bij openen wordt voorgelezen.\n * De focus komt net als anders op de heading.\n * @default false\n */\n alert?: boolean;\n\n /**\n * De subcomponenten van het dialoogvenster:\n * `ModalDialogHeader`, `ModalDialogBody`, `ModalDialogFooter`\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialog component\n * Modaal dialoogvenster gebaseerd op het native `<dialog>` element.\n *\n * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop), nooit `.show()`.\n * Dit garandeert aria-modal semantiek en het inert-attribuut op de achtergrond.\n * De focus blijft binnen het venster via de native focus-trap plus een\n * expliciete trap, en komt bij openen op de heading te staan.\n *\n * @example\n * ```tsx\n * <ModalDialog alert isOpen={isOpen} onClose={() => setIsOpen(false)}>\n * <ModalDialogHeader>\n * <ModalDialogHeading>Bevestig verwijderen</ModalDialogHeading>\n * </ModalDialogHeader>\n * <ModalDialogBody>\n * <Paragraph>Weet u zeker dat u dit item wilt verwijderen?</Paragraph>\n * </ModalDialogBody>\n * <ModalDialogFooter>\n * <ActionGroup>\n * <Button variant=\"strong-negative\" onClick={() => setIsOpen(false)}>Verwijderen</Button>\n * <Button variant=\"default\" onClick={() => setIsOpen(false)}>Annuleren</Button>\n * </ActionGroup>\n * </ModalDialogFooter>\n * </ModalDialog>\n * ```\n */\nexport const ModalDialog = React.forwardRef<\n HTMLDialogElement,\n ModalDialogProps\n>(\n (\n {\n className,\n isOpen,\n onClose,\n triggerRef,\n alert = false,\n children,\n ...props\n },\n ref\n ) => {\n const internalRef = React.useRef<HTMLDialogElement>(null);\n const dialogRef =\n (ref as React.RefObject<HTMLDialogElement>) ?? internalRef;\n const headingId = React.useId();\n const bodyId = React.useId();\n const headingRef = React.useRef<HTMLHeadingElement | null>(null);\n\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (isOpen && !dialog.open) {\n dialog.showModal();\n\n /*\n * Zet de focus op de heading, niet op de sluitknop. De browser zet de\n * focus bij het openen zelf op het eerste focusbare element, en dat is\n * de sluitknop. VoiceOver in Safari leest die knop dan wel voor, maar\n * niet de titel uit `aria-labelledby`, en omdat de sluitknop na de\n * titel staat kom je de titel bij verder lezen ook niet meer tegen.\n *\n * Met de focus op de heading (die `tabindex=\"-1\"` heeft) wordt de titel\n * als eerste voorgelezen en loopt de leesvolgorde daarna door naar de\n * sluitknop en de inhoud. Dit is het patroon uit de ARIA Authoring\n * Practices, en het houdt DOM-volgorde en visuele volgorde gelijk.\n *\n * Waarom niet het eerste veld of de minst destructieve knop: zie\n * docs/decisions/DR-2026-12-dialogs-focus-bij-openen-op-de-heading.md.\n */\n (headingRef.current ?? dialog).focus();\n } else if (!isOpen && dialog.open) {\n dialog.close();\n }\n }, [isOpen, dialogRef]);\n\n /*\n * Expliciete focus-trap bovenop de native trap van `.showModal()`.\n * Zie `utils/focusTrap.ts` voor waarom de native trap niet volstaat.\n */\n useFocusTrap(dialogRef, isOpen);\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 const handleCancel = (event: React.SyntheticEvent<HTMLDialogElement>) => {\n event.preventDefault();\n onClose?.();\n };\n\n const classes = classNames('dsn-modal-dialog', className);\n\n return (\n <ModalDialogContext.Provider\n value={{ headingId, bodyId, headingRef, onClose }}\n >\n <dialog\n ref={dialogRef}\n className={classes}\n role={alert ? 'alertdialog' : undefined}\n aria-labelledby={headingId}\n aria-describedby={alert ? bodyId : undefined}\n onCancel={handleCancel}\n tabIndex={-1}\n {...props}\n >\n {children}\n </dialog>\n </ModalDialogContext.Provider>\n );\n }\n);\n\nModalDialog.displayName = 'ModalDialog';\n\n// =============================================================================\n// ModalDialogHeader\n// =============================================================================\n\nexport interface ModalDialogHeaderProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * Inhoud van de header — doorgaans een `ModalDialogHeading`\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogHeader\n * De headerstrook van het dialoogvenster met heading en sluitknop.\n */\nexport const ModalDialogHeader = React.forwardRef<\n HTMLDivElement,\n ModalDialogHeaderProps\n>(({ className, children, ...props }, ref) => {\n const { onClose } = React.useContext(ModalDialogContext);\n\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__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\nModalDialogHeader.displayName = 'ModalDialogHeader';\n\n// =============================================================================\n// ModalDialogHeading\n// =============================================================================\n\nexport interface ModalDialogHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {\n /**\n * Semantisch heading-niveau. Kies op basis van documenthiërarchie, niet visuele grootte.\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 * ModalDialogHeading\n * De heading van het dialoogvenster. ID wordt automatisch gegenereerd voor aria-labelledby.\n * Krijgt `tabindex=\"-1\"` zodat het ModalDialog er bij openen de focus op kan zetten.\n */\nexport const ModalDialogHeading = React.forwardRef<\n HTMLHeadingElement,\n ModalDialogHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId, headingRef } = React.useContext(ModalDialogContext);\n const Tag = `h${level}` as React.ElementType;\n\n // Het ModalDialog heeft de heading nodig om er bij openen de focus op te\n // zetten; 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-modal-dialog-heading', className)}\n tabIndex={-1}\n {...props}\n >\n {children}\n </Tag>\n );\n});\n\nModalDialogHeading.displayName = 'ModalDialogHeading';\n\n// =============================================================================\n// ModalDialogBody\n// =============================================================================\n\nexport interface ModalDialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De hoofdinhoud van het dialoogvenster\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogBody\n * De scrollbare inhoudssectie van het dialoogvenster met scroll-affordance schaduw.\n */\nexport const ModalDialogBody = React.forwardRef<\n HTMLDivElement,\n ModalDialogBodyProps\n>(({ className, children, ...props }, ref) => {\n const { bodyId } = React.useContext(ModalDialogContext);\n\n return (\n <div\n ref={ref}\n id={bodyId}\n className={classNames('dsn-modal-dialog__body', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nModalDialogBody.displayName = 'ModalDialogBody';\n\n// =============================================================================\n// ModalDialogFooter\n// =============================================================================\n\nexport interface ModalDialogFooterProps extends React.HTMLAttributes<HTMLDivElement> {\n /**\n * De acties van het dialoogvenster — doorgaans een `ActionGroup` met knoppen\n */\n children?: React.ReactNode;\n}\n\n/**\n * ModalDialogFooter\n * De voettekst van het dialoogvenster met actieknoppen.\n */\nexport const ModalDialogFooter = React.forwardRef<\n HTMLDivElement,\n ModalDialogFooterProps\n>(({ className, children, ...props }, ref) => {\n return (\n <div\n ref={ref}\n className={classNames('dsn-modal-dialog__footer', className)}\n {...props}\n >\n {children}\n </div>\n );\n});\n\nModalDialogFooter.displayName = 'ModalDialogFooter';\n"],"mappings":"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"}
|
|
@@ -1,19 +1,6 @@
|
|
|
1
|
+
import { PopoverPlacement } from "../utils/popover.mjs";
|
|
1
2
|
import React from "react";
|
|
2
3
|
//#region src/Popover/Popover.d.ts
|
|
3
|
-
declare module 'react' {
|
|
4
|
-
interface HTMLAttributes<T> {
|
|
5
|
-
popover?: 'auto' | 'manual' | '' | undefined;
|
|
6
|
-
popoverTarget?: string | undefined;
|
|
7
|
-
popoverTargetAction?: 'hide' | 'show' | 'toggle' | undefined;
|
|
8
|
-
}
|
|
9
|
-
}
|
|
10
|
-
declare global {
|
|
11
|
-
interface HTMLElement {
|
|
12
|
-
showPopover(): void;
|
|
13
|
-
hidePopover(): void;
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
type Placement = 'top' | 'bottom' | 'start' | 'end';
|
|
17
4
|
interface PopoverProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
18
5
|
/**
|
|
19
6
|
* Bepaalt of de popover getoond wordt.
|
|
@@ -31,10 +18,10 @@ interface PopoverProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'child
|
|
|
31
18
|
* Gewenste plaatsing relatief aan het triggerelement.
|
|
32
19
|
* @default 'bottom'
|
|
33
20
|
*/
|
|
34
|
-
placement?:
|
|
21
|
+
placement?: PopoverPlacement;
|
|
35
22
|
/**
|
|
36
|
-
* Toegankelijke naam voor
|
|
37
|
-
*
|
|
23
|
+
* Toegankelijke naam voor een popover zonder `PopoverHeader`/`PopoverHeading`.
|
|
24
|
+
* Een zichtbare heading heeft de voorkeur; die koppelt via `aria-labelledby`.
|
|
38
25
|
*/
|
|
39
26
|
label?: string;
|
|
40
27
|
/**
|
|
@@ -44,21 +31,26 @@ interface PopoverProps extends Omit<React.HTMLAttributes<HTMLDivElement>, 'child
|
|
|
44
31
|
}
|
|
45
32
|
/**
|
|
46
33
|
* Popover component
|
|
47
|
-
*
|
|
34
|
+
* Niet-modaal dialoogvenster verankerd aan een triggerelement
|
|
35
|
+
* (`role="dialog"`, `aria-modal="false"`).
|
|
48
36
|
*
|
|
49
37
|
* Gebaseerd op de HTML Popover API (`popover="auto"`) voor ingebakken
|
|
50
38
|
* light-dismiss en top-layer gedrag. Positionering via JavaScript.
|
|
51
39
|
*
|
|
40
|
+
* Voor een lijst met acties of links: gebruik `PopoverMenu`. Een lijst is
|
|
41
|
+
* geen dialoogvenster.
|
|
42
|
+
*
|
|
52
43
|
* @example
|
|
53
44
|
* ```tsx
|
|
54
45
|
* const triggerRef = useRef<HTMLButtonElement>(null);
|
|
55
46
|
* const [isOpen, setIsOpen] = useState(false);
|
|
56
47
|
*
|
|
57
|
-
* <Button ref={triggerRef} onClick={() => setIsOpen(
|
|
58
|
-
* <Popover isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}
|
|
59
|
-
* <
|
|
60
|
-
* <
|
|
61
|
-
* </
|
|
48
|
+
* <Button ref={triggerRef} onClick={() => setIsOpen((v) => !v)}>Filters</Button>
|
|
49
|
+
* <Popover isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}>
|
|
50
|
+
* <PopoverHeader>
|
|
51
|
+
* <PopoverHeading>Filters</PopoverHeading>
|
|
52
|
+
* </PopoverHeader>
|
|
53
|
+
* <PopoverBody>...</PopoverBody>
|
|
62
54
|
* </Popover>
|
|
63
55
|
* ```
|
|
64
56
|
*/
|
|
@@ -89,6 +81,7 @@ interface PopoverHeadingProps extends React.HTMLAttributes<HTMLHeadingElement> {
|
|
|
89
81
|
/**
|
|
90
82
|
* PopoverHeading
|
|
91
83
|
* De heading van de popover. ID wordt automatisch gegenereerd voor aria-labelledby.
|
|
84
|
+
* Krijgt `tabindex="-1"` zodat de Popover er bij openen de focus op kan zetten.
|
|
92
85
|
*/
|
|
93
86
|
declare const PopoverHeading: React.ForwardRefExoticComponent<PopoverHeadingProps & React.RefAttributes<HTMLHeadingElement>>;
|
|
94
87
|
interface PopoverBodyProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Popover.d.mts","names":[],"sources":["../../src/Popover/Popover.tsx"],"mappings":";;;
|
|
1
|
+
{"version":3,"file":"Popover.d.mts","names":[],"sources":["../../src/Popover/Popover.tsx"],"mappings":";;;UA0BiB,qBAAqB,KACpC,MAAM,eAAe;;;;EAMrB;;;;EAKA;;;;EAKA,YAAY,MAAM,UAAU;;;;;EAM5B,YAAY;;;;;EAMZ;;;;EAKA,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4BN,SAAO,MAAA,0BAAA,eAAA,MAAA,cAAA;UAqEH,2BAA2B,MAAM,eAAe;;;;;EAK/D,WAAW,MAAM;;;;;;cAON,eAAa,MAAA,0BAAA,qBAAA,MAAA,cAAA;UAgCT,4BAA4B,MAAM,eAAe;;;;;EAKhE;;;;EAKA,WAAW,MAAM;;;;;;;cAQN,gBAAc,MAAA,0BAAA,sBAAA,MAAA,cAAA;UAyCV,yBAAyB,MAAM,eAAe;;;;EAI7D,WAAW,MAAM;;;;;;cAON,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA;UAoBP,2BAA2B,MAAM,eAAe;;;;EAI/D,WAAW,MAAM;;;;;;cAON,eAAa,MAAA,0BAAA,qBAAA,MAAA,cAAA"}
|
|
@@ -1,80 +1,37 @@
|
|
|
1
1
|
import { Icon } from "../Icon/Icon2.mjs";
|
|
2
2
|
import { Button } from "../Button/Button2.mjs";
|
|
3
|
+
import { usePopover } from "../utils/popover.mjs";
|
|
3
4
|
/* empty css */
|
|
4
5
|
import React from "react";
|
|
5
6
|
import { classNames } from "@dsn-starter-kit/core";
|
|
6
7
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
8
|
//#region src/Popover/Popover.tsx
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
width: popover.offsetWidth,
|
|
13
|
-
height: popover.offsetHeight
|
|
14
|
-
};
|
|
15
|
-
const isRTL = document.documentElement.dir === "rtl" || getComputedStyle(trigger).direction === "rtl";
|
|
16
|
-
let top;
|
|
17
|
-
let left;
|
|
18
|
-
switch (placement) {
|
|
19
|
-
case "bottom":
|
|
20
|
-
top = triggerRect.bottom + GAP;
|
|
21
|
-
left = isRTL ? triggerRect.right - popoverRect.width : triggerRect.left;
|
|
22
|
-
break;
|
|
23
|
-
case "top":
|
|
24
|
-
top = triggerRect.top - popoverRect.height - GAP;
|
|
25
|
-
left = isRTL ? triggerRect.right - popoverRect.width : triggerRect.left;
|
|
26
|
-
break;
|
|
27
|
-
case "end":
|
|
28
|
-
top = triggerRect.top;
|
|
29
|
-
left = isRTL ? triggerRect.left - popoverRect.width - GAP : triggerRect.right + GAP;
|
|
30
|
-
break;
|
|
31
|
-
case "start":
|
|
32
|
-
top = triggerRect.top;
|
|
33
|
-
left = isRTL ? triggerRect.right + GAP : triggerRect.left - popoverRect.width - GAP;
|
|
34
|
-
break;
|
|
35
|
-
default:
|
|
36
|
-
top = triggerRect.bottom + GAP;
|
|
37
|
-
left = triggerRect.left;
|
|
38
|
-
}
|
|
39
|
-
const margin = 8;
|
|
40
|
-
left = Math.max(margin, Math.min(left, window.innerWidth - popoverRect.width - margin));
|
|
41
|
-
top = Math.max(margin, Math.min(top, window.innerHeight - popoverRect.height - margin));
|
|
42
|
-
popover.style.top = `${top}px`;
|
|
43
|
-
popover.style.left = `${left}px`;
|
|
44
|
-
popover.style.right = "auto";
|
|
45
|
-
popover.style.bottom = "auto";
|
|
46
|
-
}
|
|
47
|
-
function focusFirstInteractive(container) {
|
|
48
|
-
const selector = [
|
|
49
|
-
"button:not(:disabled)",
|
|
50
|
-
"a[href]",
|
|
51
|
-
"input:not(:disabled)",
|
|
52
|
-
"select:not(:disabled)",
|
|
53
|
-
"textarea:not(:disabled)",
|
|
54
|
-
"[tabindex]:not([tabindex=\"-1\"])"
|
|
55
|
-
].join(", ");
|
|
56
|
-
const first = container.querySelector(selector);
|
|
57
|
-
if (first) first.focus();
|
|
58
|
-
else container.focus();
|
|
59
|
-
}
|
|
60
|
-
const PopoverContext = React.createContext({ headingId: "" });
|
|
9
|
+
const PopoverContext = React.createContext({
|
|
10
|
+
headingId: "",
|
|
11
|
+
headingRef: { current: null }
|
|
12
|
+
});
|
|
61
13
|
/**
|
|
62
14
|
* Popover component
|
|
63
|
-
*
|
|
15
|
+
* Niet-modaal dialoogvenster verankerd aan een triggerelement
|
|
16
|
+
* (`role="dialog"`, `aria-modal="false"`).
|
|
64
17
|
*
|
|
65
18
|
* Gebaseerd op de HTML Popover API (`popover="auto"`) voor ingebakken
|
|
66
19
|
* light-dismiss en top-layer gedrag. Positionering via JavaScript.
|
|
67
20
|
*
|
|
21
|
+
* Voor een lijst met acties of links: gebruik `PopoverMenu`. Een lijst is
|
|
22
|
+
* geen dialoogvenster.
|
|
23
|
+
*
|
|
68
24
|
* @example
|
|
69
25
|
* ```tsx
|
|
70
26
|
* const triggerRef = useRef<HTMLButtonElement>(null);
|
|
71
27
|
* const [isOpen, setIsOpen] = useState(false);
|
|
72
28
|
*
|
|
73
|
-
* <Button ref={triggerRef} onClick={() => setIsOpen(
|
|
74
|
-
* <Popover isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}
|
|
75
|
-
* <
|
|
76
|
-
* <
|
|
77
|
-
* </
|
|
29
|
+
* <Button ref={triggerRef} onClick={() => setIsOpen((v) => !v)}>Filters</Button>
|
|
30
|
+
* <Popover isOpen={isOpen} onClose={() => setIsOpen(false)} triggerRef={triggerRef}>
|
|
31
|
+
* <PopoverHeader>
|
|
32
|
+
* <PopoverHeading>Filters</PopoverHeading>
|
|
33
|
+
* </PopoverHeader>
|
|
34
|
+
* <PopoverBody>...</PopoverBody>
|
|
78
35
|
* </Popover>
|
|
79
36
|
* ```
|
|
80
37
|
*/
|
|
@@ -82,71 +39,21 @@ const Popover = React.forwardRef(({ className, isOpen, onClose, triggerRef, plac
|
|
|
82
39
|
const internalRef = React.useRef(null);
|
|
83
40
|
const popoverRef = ref ?? internalRef;
|
|
84
41
|
const headingId = React.useId();
|
|
85
|
-
React.
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (isOpen) {
|
|
89
|
-
el.showPopover();
|
|
90
|
-
const trigger = triggerRef.current;
|
|
91
|
-
if (trigger) positionPopover(el, trigger, placement);
|
|
92
|
-
focusFirstInteractive(el);
|
|
93
|
-
} else try {
|
|
94
|
-
el.hidePopover();
|
|
95
|
-
} catch {}
|
|
96
|
-
}, [
|
|
97
|
-
isOpen,
|
|
98
|
-
placement,
|
|
42
|
+
const headingRef = React.useRef(null);
|
|
43
|
+
usePopover({
|
|
44
|
+
popoverRef,
|
|
99
45
|
triggerRef,
|
|
100
|
-
popoverRef
|
|
101
|
-
]);
|
|
102
|
-
React.useEffect(() => {
|
|
103
|
-
const trigger = triggerRef.current;
|
|
104
|
-
if (!trigger) return;
|
|
105
|
-
trigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
|
|
106
|
-
return () => {
|
|
107
|
-
trigger.setAttribute("aria-expanded", "false");
|
|
108
|
-
};
|
|
109
|
-
}, [isOpen, triggerRef]);
|
|
110
|
-
const onCloseRef = React.useRef(onClose);
|
|
111
|
-
onCloseRef.current = onClose;
|
|
112
|
-
React.useEffect(() => {
|
|
113
|
-
const el = popoverRef.current;
|
|
114
|
-
if (!el) return;
|
|
115
|
-
const handleToggle = (event) => {
|
|
116
|
-
if (event.newState === "closed") {
|
|
117
|
-
onCloseRef.current?.();
|
|
118
|
-
triggerRef.current?.focus();
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
el.addEventListener("toggle", handleToggle);
|
|
122
|
-
return () => {
|
|
123
|
-
el.removeEventListener("toggle", handleToggle);
|
|
124
|
-
};
|
|
125
|
-
}, [triggerRef, popoverRef]);
|
|
126
|
-
React.useEffect(() => {
|
|
127
|
-
if (!isOpen) return;
|
|
128
|
-
const el = popoverRef.current;
|
|
129
|
-
if (!el) return;
|
|
130
|
-
const handlePointerDown = (event) => {
|
|
131
|
-
const target = event.target;
|
|
132
|
-
if (el.contains(target) || triggerRef.current?.contains(target)) return;
|
|
133
|
-
onCloseRef.current?.();
|
|
134
|
-
triggerRef.current?.focus();
|
|
135
|
-
};
|
|
136
|
-
document.addEventListener("pointerdown", handlePointerDown);
|
|
137
|
-
return () => {
|
|
138
|
-
document.removeEventListener("pointerdown", handlePointerDown);
|
|
139
|
-
};
|
|
140
|
-
}, [
|
|
141
46
|
isOpen,
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
47
|
+
onClose,
|
|
48
|
+
placement,
|
|
49
|
+
initialFocusRef: headingRef
|
|
50
|
+
});
|
|
145
51
|
const classes = classNames("dsn-popover", `dsn-popover--placement-${placement}`, className);
|
|
146
52
|
const ariaProps = label ? { "aria-label": label } : { "aria-labelledby": headingId };
|
|
147
53
|
return /* @__PURE__ */ jsx(PopoverContext.Provider, {
|
|
148
54
|
value: {
|
|
149
55
|
headingId,
|
|
56
|
+
headingRef,
|
|
150
57
|
onClose
|
|
151
58
|
},
|
|
152
59
|
children: /* @__PURE__ */ jsx("div", {
|
|
@@ -190,14 +97,21 @@ PopoverHeader.displayName = "PopoverHeader";
|
|
|
190
97
|
/**
|
|
191
98
|
* PopoverHeading
|
|
192
99
|
* De heading van de popover. ID wordt automatisch gegenereerd voor aria-labelledby.
|
|
100
|
+
* Krijgt `tabindex="-1"` zodat de Popover er bij openen de focus op kan zetten.
|
|
193
101
|
*/
|
|
194
102
|
const PopoverHeading = React.forwardRef(({ className, level = 2, children, ...props }, ref) => {
|
|
195
|
-
const { headingId } = React.useContext(PopoverContext);
|
|
103
|
+
const { headingId, headingRef } = React.useContext(PopoverContext);
|
|
196
104
|
const Tag = `h${level}`;
|
|
105
|
+
const setRef = React.useCallback((node) => {
|
|
106
|
+
headingRef.current = node;
|
|
107
|
+
if (typeof ref === "function") ref(node);
|
|
108
|
+
else if (ref) ref.current = node;
|
|
109
|
+
}, [headingRef, ref]);
|
|
197
110
|
return /* @__PURE__ */ jsx(Tag, {
|
|
198
|
-
ref,
|
|
111
|
+
ref: setRef,
|
|
199
112
|
id: headingId,
|
|
200
113
|
className: classNames("dsn-popover-heading", className),
|
|
114
|
+
tabIndex: -1,
|
|
201
115
|
...props,
|
|
202
116
|
children
|
|
203
117
|
});
|