@dsn-starter-kit/components-react 3.4.0 → 4.0.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.
Files changed (68) hide show
  1. package/dist/Card/Card.d.mts +86 -21
  2. package/dist/Card/Card.d.mts.map +1 -1
  3. package/dist/Card/Card2.mjs +100 -17
  4. package/dist/Card/Card2.mjs.map +1 -1
  5. package/dist/Card/index.d.mts +2 -2
  6. package/dist/DateInput/DateInput.d.mts +1 -1
  7. package/dist/DateInput/DateInput.d.mts.map +1 -1
  8. package/dist/DateInput/DateInput2.mjs +1 -2
  9. package/dist/DateInput/DateInput2.mjs.map +1 -1
  10. package/dist/DateInputGroup/DateInputGroup2.mjs +3 -3
  11. package/dist/DateInputGroup/DateInputGroup2.mjs.map +1 -1
  12. package/dist/Drawer/Drawer.d.mts +11 -3
  13. package/dist/Drawer/Drawer.d.mts.map +1 -1
  14. package/dist/Drawer/Drawer2.mjs +28 -4
  15. package/dist/Drawer/Drawer2.mjs.map +1 -1
  16. package/dist/EmailInput/EmailInput.d.mts +10 -5
  17. package/dist/EmailInput/EmailInput.d.mts.map +1 -1
  18. package/dist/EmailInput/EmailInput2.mjs +2 -2
  19. package/dist/EmailInput/EmailInput2.mjs.map +1 -1
  20. package/dist/ModalDialog/ModalDialog.d.mts +12 -2
  21. package/dist/ModalDialog/ModalDialog.d.mts.map +1 -1
  22. package/dist/ModalDialog/ModalDialog2.mjs +35 -8
  23. package/dist/ModalDialog/ModalDialog2.mjs.map +1 -1
  24. package/dist/NumberInput/NumberInput.d.mts +4 -4
  25. package/dist/NumberInput/NumberInput.d.mts.map +1 -1
  26. package/dist/NumberInput/NumberInput2.mjs +2 -2
  27. package/dist/NumberInput/NumberInput2.mjs.map +1 -1
  28. package/dist/PageHeader/PageHeader.d.mts.map +1 -1
  29. package/dist/PageHeader/PageHeader2.mjs +20 -2
  30. package/dist/PageHeader/PageHeader2.mjs.map +1 -1
  31. package/dist/PasswordInput/PasswordInput.d.mts +10 -5
  32. package/dist/PasswordInput/PasswordInput.d.mts.map +1 -1
  33. package/dist/PasswordInput/PasswordInput2.mjs +2 -2
  34. package/dist/PasswordInput/PasswordInput2.mjs.map +1 -1
  35. package/dist/SearchInput/SearchInput.d.mts +10 -5
  36. package/dist/SearchInput/SearchInput.d.mts.map +1 -1
  37. package/dist/SearchInput/SearchInput2.mjs +2 -3
  38. package/dist/SearchInput/SearchInput2.mjs.map +1 -1
  39. package/dist/Select/Select.d.mts +6 -6
  40. package/dist/Select/Select.d.mts.map +1 -1
  41. package/dist/Select/Select2.mjs +4 -5
  42. package/dist/Select/Select2.mjs.map +1 -1
  43. package/dist/TelephoneInput/TelephoneInput.d.mts +10 -5
  44. package/dist/TelephoneInput/TelephoneInput.d.mts.map +1 -1
  45. package/dist/TelephoneInput/TelephoneInput2.mjs +2 -2
  46. package/dist/TelephoneInput/TelephoneInput2.mjs.map +1 -1
  47. package/dist/TextArea/TextArea.d.mts +10 -5
  48. package/dist/TextArea/TextArea.d.mts.map +1 -1
  49. package/dist/TextArea/TextArea2.mjs +2 -2
  50. package/dist/TextArea/TextArea2.mjs.map +1 -1
  51. package/dist/TextInput/TextInput.d.mts +4 -4
  52. package/dist/TextInput/TextInput.d.mts.map +1 -1
  53. package/dist/TextInput/TextInput2.mjs +2 -2
  54. package/dist/TextInput/TextInput2.mjs.map +1 -1
  55. package/dist/TimeInput/TimeInput.d.mts +1 -1
  56. package/dist/TimeInput/TimeInput.d.mts.map +1 -1
  57. package/dist/TimeInput/TimeInput2.mjs +1 -2
  58. package/dist/TimeInput/TimeInput2.mjs.map +1 -1
  59. package/dist/index.cjs +303 -53
  60. package/dist/index.cjs.map +1 -1
  61. package/dist/index.css +5439 -3838
  62. package/dist/index.d.cts +163 -55
  63. package/dist/index.d.cts.map +1 -1
  64. package/dist/index.d.mts +2 -2
  65. package/dist/index.mjs +2 -2
  66. package/dist/utils/focusTrap.mjs +99 -0
  67. package/dist/utils/focusTrap.mjs.map +1 -0
  68. package/package.json +3 -3
@@ -1,11 +1,15 @@
1
1
  import { Icon } from "../Icon/Icon2.mjs";
2
2
  import { Button } from "../Button/Button2.mjs";
3
+ import { useFocusTrap } from "../utils/focusTrap.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/Drawer/Drawer.tsx
8
- const DrawerContext = React.createContext({ headingId: "" });
9
+ const DrawerContext = React.createContext({
10
+ headingId: "",
11
+ headingRef: { current: null }
12
+ });
9
13
  /**
10
14
  * Drawer component
11
15
  * Zijpaneel dat vanuit links of rechts de viewport inschuift.
@@ -32,22 +36,33 @@ const DrawerContext = React.createContext({ headingId: "" });
32
36
  * </Drawer>
33
37
  * ```
34
38
  */
35
- const Drawer = React.forwardRef(({ className, isOpen, onClose, modal = true, side = "right", children, ...props }, ref) => {
39
+ const Drawer = React.forwardRef(({ className, isOpen, onClose, modal = true, triggerRef, side = "right", children, ...props }, ref) => {
36
40
  const internalRef = React.useRef(null);
37
41
  const dialogRef = ref ?? internalRef;
38
42
  const headingId = React.useId();
43
+ const headingRef = React.useRef(null);
39
44
  React.useEffect(() => {
40
45
  const dialog = dialogRef.current;
41
46
  if (!dialog) return;
42
47
  if (isOpen && !dialog.open) {
43
48
  if (modal) dialog.showModal();
44
49
  else dialog.show();
50
+ (headingRef.current ?? dialog).focus();
45
51
  } else if (!isOpen && dialog.open) dialog.close();
46
52
  }, [
47
53
  isOpen,
48
54
  modal,
49
55
  dialogRef
50
56
  ]);
57
+ useFocusTrap(dialogRef, isOpen && modal);
58
+ React.useEffect(() => {
59
+ const trigger = triggerRef?.current;
60
+ if (!trigger) return;
61
+ trigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
62
+ return () => {
63
+ trigger.setAttribute("aria-expanded", "false");
64
+ };
65
+ }, [isOpen, triggerRef]);
51
66
  React.useEffect(() => {
52
67
  if (modal) return;
53
68
  const dialog = dialogRef.current;
@@ -71,6 +86,7 @@ const Drawer = React.forwardRef(({ className, isOpen, onClose, modal = true, sid
71
86
  return /* @__PURE__ */ jsx(DrawerContext.Provider, {
72
87
  value: {
73
88
  headingId,
89
+ headingRef,
74
90
  onClose
75
91
  },
76
92
  children: /* @__PURE__ */ jsx("dialog", {
@@ -78,6 +94,7 @@ const Drawer = React.forwardRef(({ className, isOpen, onClose, modal = true, sid
78
94
  className: classes,
79
95
  "aria-labelledby": headingId,
80
96
  onCancel: handleCancel,
97
+ tabIndex: -1,
81
98
  ...props,
82
99
  children
83
100
  })
@@ -111,14 +128,21 @@ DrawerHeader.displayName = "DrawerHeader";
111
128
  /**
112
129
  * DrawerHeading
113
130
  * De heading van het zijpaneel. ID wordt automatisch gegenereerd voor aria-labelledby.
131
+ * Krijgt `tabindex="-1"` zodat de Drawer er bij openen de focus op kan zetten.
114
132
  */
115
133
  const DrawerHeading = React.forwardRef(({ className, level = 2, children, ...props }, ref) => {
116
- const { headingId } = React.useContext(DrawerContext);
134
+ const { headingId, headingRef } = React.useContext(DrawerContext);
117
135
  const Tag = `h${level}`;
136
+ const setRef = React.useCallback((node) => {
137
+ headingRef.current = node;
138
+ if (typeof ref === "function") ref(node);
139
+ else if (ref) ref.current = node;
140
+ }, [headingRef, ref]);
118
141
  return /* @__PURE__ */ jsx(Tag, {
119
- ref,
142
+ ref: setRef,
120
143
  id: headingId,
121
144
  className: classNames("dsn-drawer-heading", className),
145
+ tabIndex: -1,
122
146
  ...props,
123
147
  children
124
148
  });
@@ -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 './Drawer.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface DrawerContextValue {\n headingId: string;\n onClose?: () => void;\n}\n\nconst DrawerContext = React.createContext<DrawerContextValue>({\n headingId: '',\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 * natieve focus-trap, Escape sluit via `cancel`-event.\n * - `false`: opent via `.show()` — achtergrond blijft interactief,\n * Escape sluit via `keydown`-listener.\n * @default true\n */\n modal?: boolean;\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 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\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 } else if (!isOpen && dialog.open) {\n dialog.close();\n }\n }, [isOpen, modal, dialogRef]);\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, onClose }}>\n <dialog\n ref={dialogRef}\n className={classes}\n aria-labelledby={headingId}\n onCancel={handleCancel}\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 */\nexport const DrawerHeading = React.forwardRef<\n HTMLHeadingElement,\n DrawerHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId } = React.useContext(DrawerContext);\n const Tag = `h${level}` as React.ElementType;\n\n return (\n <Tag\n ref={ref}\n id={headingId}\n className={classNames('dsn-drawer-heading', className)}\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":";;;;;;;AAeA,MAAM,gBAAgB,MAAM,cAAkC,EAC5D,WAAW,GACb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsED,MAAa,SAAS,MAAM,YAExB,EACE,WACA,QACA,SACA,QAAQ,MACR,OAAO,SACP,UACA,GAAG,SAEL,QACG;CACH,MAAM,cAAc,MAAM,OAA0B,IAAI;CACxD,MAAM,YACH,OAA8C;CACjD,MAAM,YAAY,MAAM,MAAM;CAE9B,MAAM,gBAAgB;EACpB,MAAM,SAAS,UAAU;EACzB,IAAI,CAAC,QAAQ;EAEb,IAAI,UAAU,CAAC,OAAO,MAAM;GAC1B,IAAI,OACF,OAAO,UAAU;QAEjB,OAAO,KAAK;EAEhB,OAAO,IAAI,CAAC,UAAU,OAAO,MAC3B,OAAO,MAAM;CAEjB,GAAG;EAAC;EAAQ;EAAO;CAAS,CAAC;CAG7B,MAAM,gBAAgB;EACpB,IAAI,OAAO;EACX,MAAM,SAAS,UAAU;EACzB,IAAI,CAAC,UAAU,CAAC,QAAQ;EAExB,MAAM,iBAAiB,UAAyB;GAC9C,IAAI,MAAM,QAAQ,UAChB,UAAU;EAEd;EAEA,OAAO,iBAAiB,WAAW,aAAa;EAChD,aAAa,OAAO,oBAAoB,WAAW,aAAa;CAClE,GAAG;EAAC;EAAO;EAAQ;EAAS;CAAS,CAAC;CAGtC,MAAM,gBAAgB,UAAmD;EACvE,MAAM,eAAe;EACrB,UAAU;CACZ;CAEA,MAAM,UAAU,WACd,cACA,SAAS,SAAS,0BAA0B,0BAC5C,SACF;CAEA,OACE,oBAAC,cAAc,UAAf;EAAwB,OAAO;GAAE;GAAW;EAAQ;EAClD,UAAA,oBAAC,UAAD;GACE,KAAK;GACL,WAAW;GACX,mBAAiB;GACjB,UAAU;GACV,GAAI;GAEH;EACK,CAAA;CACc,CAAA;AAE5B,CACF;AAEA,OAAO,cAAc;;;;;AAiBrB,MAAa,eAAe,MAAM,YAC/B,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAC1C,MAAM,EAAE,YAAY,MAAM,WAAW,aAAa;CAElD,OACE,qBAAC,OAAD;EACO;EACL,WAAW,WAAW,sBAAsB,SAAS;EACrD,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,CACF;AAEA,aAAa,cAAc;;;;;AAuB3B,MAAa,gBAAgB,MAAM,YAGhC,EAAE,WAAW,QAAQ,GAAG,UAAU,GAAG,SAAS,QAAQ;CACvD,MAAM,EAAE,cAAc,MAAM,WAAW,aAAa;CACpD,MAAM,MAAM,IAAI;CAEhB,OACE,oBAAC,KAAD;EACO;EACL,IAAI;EACJ,WAAW,WAAW,sBAAsB,SAAS;EACrD,GAAI;EAEH;CACE,CAAA;AAET,CAAC;AAED,cAAc,cAAc;;;;;AAiB5B,MAAa,aAAa,MAAM,YAC7B,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAC1C,OACE,oBAAC,OAAD;EACO;EACL,WAAW,WAAW,oBAAoB,SAAS;EACnD,GAAI;EAEH;CACE,CAAA;AAET,CACF;AAEA,WAAW,cAAc;;;;;AAiBzB,MAAa,eAAe,MAAM,YAC/B,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAC1C,OACE,oBAAC,OAAD;EACO;EACL,WAAW,WAAW,sBAAsB,SAAS;EACrD,GAAI;EAEH;CACE,CAAA;AAET,CACF;AAEA,aAAa,cAAc"}
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,6 +1,11 @@
1
1
  import React from "react";
2
- import { FormControlWidth } from "@dsn-starter-kit/core";
2
+ import { FormControlInlineSize } from "@dsn-starter-kit/core";
3
3
  //#region src/EmailInput/EmailInput.d.ts
4
+ /**
5
+ * Inline-size variants offered by EmailInput. xs and sm are left out: they
6
+ * are too narrow for the content this field is meant for.
7
+ */
8
+ type EmailInputInlineSize = Exclude<FormControlInlineSize, 'xs' | 'sm'>;
4
9
  interface EmailInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
5
10
  /**
6
11
  * Whether the input is in an invalid state
@@ -8,10 +13,10 @@ interface EmailInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElemen
8
13
  */
9
14
  invalid?: boolean;
10
15
  /**
11
- * Width variant for the input
12
- * @default undefined (uses default max-width from form-control)
16
+ * Inline-size variant for the input (md, lg, xl or full)
17
+ * @default undefined (uses default max-inline-size from form-control)
13
18
  */
14
- width?: FormControlWidth;
19
+ inlineSize?: EmailInputInlineSize;
15
20
  /**
16
21
  * Additional CSS class names
17
22
  */
@@ -36,5 +41,5 @@ interface EmailInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElemen
36
41
  */
37
42
  declare const EmailInput: React.ForwardRefExoticComponent<EmailInputProps & React.RefAttributes<HTMLInputElement>>;
38
43
  //#endregion
39
- export { EmailInput, EmailInputProps };
44
+ export { EmailInput, EmailInputInlineSize, EmailInputProps };
40
45
  //# sourceMappingURL=EmailInput.d.mts.map
@@ -1 +1 @@
1
- {"version":3,"file":"EmailInput.d.mts","names":[],"sources":["../../src/EmailInput/EmailInput.tsx"],"mappings":";;;UAIiB,wBAAwB,KACvC,MAAM,oBAAoB;;;;;EAO1B;;;;;EAMA,QAAQ;;;;EAKR;;;;;;;;;;;;;;;;;;;cAoBW,YAAU,MAAA,0BAAA,kBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"EmailInput.d.mts","names":[],"sources":["../../src/EmailInput/EmailInput.tsx"],"mappings":";;;;;;;KAQY,uBAAuB,QAAQ;UAE1B,wBAAwB,KACvC,MAAM,oBAAoB;;;;;EAO1B;;;;;EAMA,aAAa;;;;EAKb;;;;;;;;;;;;;;;;;;;cAoBW,YAAU,MAAA,0BAAA,kBAAA,MAAA,cAAA"}
@@ -20,8 +20,8 @@ import { jsx } from "react/jsx-runtime";
20
20
  * <EmailInput invalid aria-invalid="true" aria-describedby="error" />
21
21
  * ```
22
22
  */
23
- const EmailInput = React.forwardRef(({ className, invalid, width, autoComplete, ...props }, ref) => {
24
- const classes = classNames("dsn-text-input", width && `dsn-text-input--width-${width}`, className);
23
+ const EmailInput = React.forwardRef(({ className, invalid, inlineSize, autoComplete, ...props }, ref) => {
24
+ const classes = classNames("dsn-text-input", inlineSize && `dsn-text-input--inline-size-${inlineSize}`, className);
25
25
  return /* @__PURE__ */ jsx("input", {
26
26
  ref,
27
27
  type: "email",
@@ -1 +1 @@
1
- {"version":3,"file":"EmailInput2.mjs","names":[],"sources":["../../src/EmailInput/EmailInput.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, FormControlWidth } from '@dsn-starter-kit/core';\nimport './EmailInput.css';\n\nexport interface EmailInputProps extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n 'type'\n> {\n /**\n * Whether the input is in an invalid state\n * @default false\n */\n invalid?: boolean;\n\n /**\n * Width variant for the input\n * @default undefined (uses default max-width from form-control)\n */\n width?: FormControlWidth;\n\n /**\n * Additional CSS class names\n */\n className?: string;\n}\n\n/**\n * Email Input component\n * Email input with appropriate inputmode and autocomplete\n *\n * @example\n * ```tsx\n * // Basic usage\n * <EmailInput placeholder=\"naam@voorbeeld.nl\" />\n *\n * // With label\n * <FormFieldLabel htmlFor=\"email\">E-mailadres</FormFieldLabel>\n * <EmailInput id=\"email\" />\n *\n * // Invalid state\n * <EmailInput invalid aria-invalid=\"true\" aria-describedby=\"error\" />\n * ```\n */\nexport const EmailInput = React.forwardRef<HTMLInputElement, EmailInputProps>(\n ({ className, invalid, width, autoComplete, ...props }, ref) => {\n const classes = classNames(\n 'dsn-text-input',\n width && `dsn-text-input--width-${width}`,\n className\n );\n\n return (\n <input\n ref={ref}\n type=\"email\"\n inputMode=\"email\"\n className={classes}\n autoComplete={autoComplete || 'email'}\n aria-invalid={invalid || undefined}\n {...props}\n />\n );\n }\n);\n\nEmailInput.displayName = 'EmailInput';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AA2CA,MAAa,aAAa,MAAM,YAC7B,EAAE,WAAW,SAAS,OAAO,cAAc,GAAG,SAAS,QAAQ;CAC9D,MAAM,UAAU,WACd,kBACA,SAAS,yBAAyB,SAClC,SACF;CAEA,OACE,oBAAC,SAAD;EACO;EACL,MAAK;EACL,WAAU;EACV,WAAW;EACX,cAAc,gBAAgB;EAC9B,gBAAc,WAAW,KAAA;EACzB,GAAI;CACL,CAAA;AAEL,CACF;AAEA,WAAW,cAAc"}
1
+ {"version":3,"file":"EmailInput2.mjs","names":[],"sources":["../../src/EmailInput/EmailInput.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, FormControlInlineSize } from '@dsn-starter-kit/core';\nimport './EmailInput.css';\n\n/**\n * Inline-size variants offered by EmailInput. xs and sm are left out: they\n * are too narrow for the content this field is meant for.\n */\nexport type EmailInputInlineSize = Exclude<FormControlInlineSize, 'xs' | 'sm'>;\n\nexport interface EmailInputProps extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n 'type'\n> {\n /**\n * Whether the input is in an invalid state\n * @default false\n */\n invalid?: boolean;\n\n /**\n * Inline-size variant for the input (md, lg, xl or full)\n * @default undefined (uses default max-inline-size from form-control)\n */\n inlineSize?: EmailInputInlineSize;\n\n /**\n * Additional CSS class names\n */\n className?: string;\n}\n\n/**\n * Email Input component\n * Email input with appropriate inputmode and autocomplete\n *\n * @example\n * ```tsx\n * // Basic usage\n * <EmailInput placeholder=\"naam@voorbeeld.nl\" />\n *\n * // With label\n * <FormFieldLabel htmlFor=\"email\">E-mailadres</FormFieldLabel>\n * <EmailInput id=\"email\" />\n *\n * // Invalid state\n * <EmailInput invalid aria-invalid=\"true\" aria-describedby=\"error\" />\n * ```\n */\nexport const EmailInput = React.forwardRef<HTMLInputElement, EmailInputProps>(\n ({ className, invalid, inlineSize, autoComplete, ...props }, ref) => {\n const classes = classNames(\n 'dsn-text-input',\n inlineSize && `dsn-text-input--inline-size-${inlineSize}`,\n className\n );\n\n return (\n <input\n ref={ref}\n type=\"email\"\n inputMode=\"email\"\n className={classes}\n autoComplete={autoComplete || 'email'}\n aria-invalid={invalid || undefined}\n {...props}\n />\n );\n }\n);\n\nEmailInput.displayName = 'EmailInput';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAiDA,MAAa,aAAa,MAAM,YAC7B,EAAE,WAAW,SAAS,YAAY,cAAc,GAAG,SAAS,QAAQ;CACnE,MAAM,UAAU,WACd,kBACA,cAAc,+BAA+B,cAC7C,SACF;CAEA,OACE,oBAAC,SAAD;EACO;EACL,MAAK;EACL,WAAU;EACV,WAAW;EACX,cAAc,gBAAgB;EAC9B,gBAAc,WAAW,KAAA;EACzB,GAAI;CACL,CAAA;AAEL,CACF;AAEA,WAAW,cAAc"}
@@ -11,6 +11,13 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
11
11
  * (sluitknop, Escape-toets of klik buiten het venster).
12
12
  */
13
13
  onClose?: () => void;
14
+ /**
15
+ * Ref naar het element dat het dialoogvenster opent. Wanneer je die meegeeft,
16
+ * houdt het dialoogvenster `aria-expanded` op dat element synchroon met de
17
+ * open-staat: `false` wanneer het venster gesloten is, `true` wanneer het
18
+ * open staat.
19
+ */
20
+ triggerRef?: React.RefObject<HTMLElement | null>;
14
21
  /**
15
22
  * De subcomponenten van het dialoogvenster:
16
23
  * `ModalDialogHeader`, `ModalDialogBody`, `ModalDialogFooter`
@@ -21,8 +28,10 @@ interface ModalDialogProps extends Omit<React.HTMLAttributes<HTMLDialogElement>,
21
28
  * ModalDialog component
22
29
  * Modaal dialoogvenster gebaseerd op het native `<dialog>` element.
23
30
  *
24
- * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop) — nooit `.show()`.
25
- * Dit garandeert natieve focus-trap, aria-modal semantiek en inert-attribuut op de achtergrond.
31
+ * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop), nooit `.show()`.
32
+ * Dit garandeert aria-modal semantiek en het inert-attribuut op de achtergrond.
33
+ * De focus blijft binnen het venster via de native focus-trap plus een
34
+ * expliciete trap, en komt bij openen op de heading te staan.
26
35
  *
27
36
  * @example
28
37
  * ```tsx
@@ -68,6 +77,7 @@ interface ModalDialogHeadingProps extends React.HTMLAttributes<HTMLHeadingElemen
68
77
  /**
69
78
  * ModalDialogHeading
70
79
  * De heading van het dialoogvenster. ID wordt automatisch gegenereerd voor aria-labelledby.
80
+ * Krijgt `tabindex="-1"` zodat het ModalDialog er bij openen de focus op kan zetten.
71
81
  */
72
82
  declare const ModalDialogHeading: React.ForwardRefExoticComponent<ModalDialogHeadingProps & React.RefAttributes<HTMLHeadingElement>>;
73
83
  interface ModalDialogBodyProps extends React.HTMLAttributes<HTMLDivElement> {
@@ -1 +1 @@
1
- {"version":3,"file":"ModalDialog.d.mts","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"mappings":";;UAuBiB,yBAAyB,KACxC,MAAM,eAAe;;;;;EAOrB;;;;;EAMA;;;;;EAMA,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;cA4BN,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA;UA+CP,+BAA+B,MAAM,eAAe;;;;EAInE,WAAW,MAAM;;;;;;cAON,mBAAiB,MAAA,0BAAA,yBAAA,MAAA,cAAA;UAgCb,gCAAgC,MAAM,eAAe;;;;;EAKpE;;;;EAKA,WAAW,MAAM;;;;;;cAON,oBAAkB,MAAA,0BAAA,0BAAA,MAAA,cAAA;UAyBd,6BAA6B,MAAM,eAAe;;;;EAIjE,WAAW,MAAM;;;;;;cAON,iBAAe,MAAA,0BAAA,uBAAA,MAAA,cAAA;UAqBX,+BAA+B,MAAM,eAAe;;;;EAInE,WAAW,MAAM;;;;;;cAON,mBAAiB,MAAA,0BAAA,yBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"ModalDialog.d.mts","names":[],"sources":["../../src/ModalDialog/ModalDialog.tsx"],"mappings":";;UA0BiB,yBAAyB,KACxC,MAAM,eAAe;;;;;EAOrB;;;;;EAMA;;;;;;;EAQA,aAAa,MAAM,UAAU;;;;;EAM7B,WAAW,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cA8BN,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA;UA+EP,+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;UAqBX,+BAA+B,MAAM,eAAe;;;;EAInE,WAAW,MAAM;;;;;;cAON,mBAAiB,MAAA,0BAAA,yBAAA,MAAA,cAAA"}
@@ -1,17 +1,23 @@
1
1
  import { Icon } from "../Icon/Icon2.mjs";
2
2
  import { Button } from "../Button/Button2.mjs";
3
+ import { useFocusTrap } from "../utils/focusTrap.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/ModalDialog/ModalDialog.tsx
8
- const ModalDialogContext = React.createContext({ headingId: "" });
9
+ const ModalDialogContext = React.createContext({
10
+ headingId: "",
11
+ headingRef: { current: null }
12
+ });
9
13
  /**
10
14
  * ModalDialog component
11
15
  * Modaal dialoogvenster gebaseerd op het native `<dialog>` element.
12
16
  *
13
- * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop) — nooit `.show()`.
14
- * Dit garandeert natieve focus-trap, aria-modal semantiek en inert-attribuut op de achtergrond.
17
+ * Gebruik altijd `.showModal()` (intern afgehandeld via `isOpen` prop), nooit `.show()`.
18
+ * Dit garandeert aria-modal semantiek en het inert-attribuut op de achtergrond.
19
+ * De focus blijft binnen het venster via de native focus-trap plus een
20
+ * expliciete trap, en komt bij openen op de heading te staan.
15
21
  *
16
22
  * @example
17
23
  * ```tsx
@@ -31,16 +37,28 @@ const ModalDialogContext = React.createContext({ headingId: "" });
31
37
  * </ModalDialog>
32
38
  * ```
33
39
  */
34
- const ModalDialog = React.forwardRef(({ className, isOpen, onClose, children, ...props }, ref) => {
40
+ const ModalDialog = React.forwardRef(({ className, isOpen, onClose, triggerRef, children, ...props }, ref) => {
35
41
  const internalRef = React.useRef(null);
36
42
  const dialogRef = ref ?? internalRef;
37
43
  const headingId = React.useId();
44
+ const headingRef = React.useRef(null);
38
45
  React.useEffect(() => {
39
46
  const dialog = dialogRef.current;
40
47
  if (!dialog) return;
41
- if (isOpen && !dialog.open) dialog.showModal();
42
- else if (!isOpen && dialog.open) dialog.close();
48
+ if (isOpen && !dialog.open) {
49
+ dialog.showModal();
50
+ (headingRef.current ?? dialog).focus();
51
+ } else if (!isOpen && dialog.open) dialog.close();
43
52
  }, [isOpen, dialogRef]);
53
+ useFocusTrap(dialogRef, isOpen);
54
+ React.useEffect(() => {
55
+ const trigger = triggerRef?.current;
56
+ if (!trigger) return;
57
+ trigger.setAttribute("aria-expanded", isOpen ? "true" : "false");
58
+ return () => {
59
+ trigger.setAttribute("aria-expanded", "false");
60
+ };
61
+ }, [isOpen, triggerRef]);
44
62
  const handleCancel = (event) => {
45
63
  event.preventDefault();
46
64
  onClose?.();
@@ -49,6 +67,7 @@ const ModalDialog = React.forwardRef(({ className, isOpen, onClose, children, ..
49
67
  return /* @__PURE__ */ jsx(ModalDialogContext.Provider, {
50
68
  value: {
51
69
  headingId,
70
+ headingRef,
52
71
  onClose
53
72
  },
54
73
  children: /* @__PURE__ */ jsx("dialog", {
@@ -56,6 +75,7 @@ const ModalDialog = React.forwardRef(({ className, isOpen, onClose, children, ..
56
75
  className: classes,
57
76
  "aria-labelledby": headingId,
58
77
  onCancel: handleCancel,
78
+ tabIndex: -1,
59
79
  ...props,
60
80
  children
61
81
  })
@@ -89,14 +109,21 @@ ModalDialogHeader.displayName = "ModalDialogHeader";
89
109
  /**
90
110
  * ModalDialogHeading
91
111
  * De heading van het dialoogvenster. ID wordt automatisch gegenereerd voor aria-labelledby.
112
+ * Krijgt `tabindex="-1"` zodat het ModalDialog er bij openen de focus op kan zetten.
92
113
  */
93
114
  const ModalDialogHeading = React.forwardRef(({ className, level = 2, children, ...props }, ref) => {
94
- const { headingId } = React.useContext(ModalDialogContext);
115
+ const { headingId, headingRef } = React.useContext(ModalDialogContext);
95
116
  const Tag = `h${level}`;
117
+ const setRef = React.useCallback((node) => {
118
+ headingRef.current = node;
119
+ if (typeof ref === "function") ref(node);
120
+ else if (ref) ref.current = node;
121
+ }, [headingRef, ref]);
96
122
  return /* @__PURE__ */ jsx(Tag, {
97
- ref,
123
+ ref: setRef,
98
124
  id: headingId,
99
125
  className: classNames("dsn-modal-dialog-heading", className),
126
+ tabIndex: -1,
100
127
  ...props,
101
128
  children
102
129
  });
@@ -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 './ModalDialog.css';\n\n// =============================================================================\n// Context\n// =============================================================================\n\ninterface ModalDialogContextValue {\n headingId: string;\n onClose?: () => void;\n}\n\nconst ModalDialogContext = React.createContext<ModalDialogContextValue>({\n headingId: '',\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 * 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 natieve focus-trap, aria-modal semantiek en inert-attribuut op de achtergrond.\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, 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\n React.useEffect(() => {\n const dialog = dialogRef.current;\n if (!dialog) return;\n\n if (isOpen && !dialog.open) {\n dialog.showModal();\n } else if (!isOpen && dialog.open) {\n dialog.close();\n }\n }, [isOpen, dialogRef]);\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, onClose }}>\n <dialog\n ref={dialogRef}\n className={classes}\n aria-labelledby={headingId}\n onCancel={handleCancel}\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 */\nexport const ModalDialogHeading = React.forwardRef<\n HTMLHeadingElement,\n ModalDialogHeadingProps\n>(({ className, level = 2, children, ...props }, ref) => {\n const { headingId } = React.useContext(ModalDialogContext);\n const Tag = `h${level}` as React.ElementType;\n\n return (\n <Tag\n ref={ref}\n id={headingId}\n className={classNames('dsn-modal-dialog-heading', className)}\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":";;;;;;;AAeA,MAAM,qBAAqB,MAAM,cAAuC,EACtE,WAAW,GACb,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;AAsDD,MAAa,cAAc,MAAM,YAG9B,EAAE,WAAW,QAAQ,SAAS,UAAU,GAAG,SAAS,QAAQ;CAC7D,MAAM,cAAc,MAAM,OAA0B,IAAI;CACxD,MAAM,YAAa,OAA8C;CACjE,MAAM,YAAY,MAAM,MAAM;CAE9B,MAAM,gBAAgB;EACpB,MAAM,SAAS,UAAU;EACzB,IAAI,CAAC,QAAQ;EAEb,IAAI,UAAU,CAAC,OAAO,MACpB,OAAO,UAAU;OACZ,IAAI,CAAC,UAAU,OAAO,MAC3B,OAAO,MAAM;CAEjB,GAAG,CAAC,QAAQ,SAAS,CAAC;CAEtB,MAAM,gBAAgB,UAAmD;EACvE,MAAM,eAAe;EACrB,UAAU;CACZ;CAEA,MAAM,UAAU,WAAW,oBAAoB,SAAS;CAExD,OACE,oBAAC,mBAAmB,UAApB;EAA6B,OAAO;GAAE;GAAW;EAAQ;EACvD,UAAA,oBAAC,UAAD;GACE,KAAK;GACL,WAAW;GACX,mBAAiB;GACjB,UAAU;GACV,GAAI;GAEH;EACK,CAAA;CACmB,CAAA;AAEjC,CAAC;AAED,YAAY,cAAc;;;;;AAiB1B,MAAa,oBAAoB,MAAM,YAGpC,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAC5C,MAAM,EAAE,YAAY,MAAM,WAAW,kBAAkB;CAEvD,OACE,qBAAC,OAAD;EACO;EACL,WAAW,WAAW,4BAA4B,SAAS;EAC3D,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,kBAAkB,cAAc;;;;;AAuBhC,MAAa,qBAAqB,MAAM,YAGrC,EAAE,WAAW,QAAQ,GAAG,UAAU,GAAG,SAAS,QAAQ;CACvD,MAAM,EAAE,cAAc,MAAM,WAAW,kBAAkB;CACzD,MAAM,MAAM,IAAI;CAEhB,OACE,oBAAC,KAAD;EACO;EACL,IAAI;EACJ,WAAW,WAAW,4BAA4B,SAAS;EAC3D,GAAI;EAEH;CACE,CAAA;AAET,CAAC;AAED,mBAAmB,cAAc;;;;;AAiBjC,MAAa,kBAAkB,MAAM,YAGlC,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAC5C,OACE,oBAAC,OAAD;EACO;EACL,WAAW,WAAW,0BAA0B,SAAS;EACzD,GAAI;EAEH;CACE,CAAA;AAET,CAAC;AAED,gBAAgB,cAAc;;;;;AAiB9B,MAAa,oBAAoB,MAAM,YAGpC,EAAE,WAAW,UAAU,GAAG,SAAS,QAAQ;CAC5C,OACE,oBAAC,OAAD;EACO;EACL,WAAW,WAAW,4BAA4B,SAAS;EAC3D,GAAI;EAEH;CACE,CAAA;AAET,CAAC;AAED,kBAAkB,cAAc"}
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,5 +1,5 @@
1
1
  import React from "react";
2
- import { FormControlWidth } from "@dsn-starter-kit/core";
2
+ import { FormControlInlineSize } from "@dsn-starter-kit/core";
3
3
  //#region src/NumberInput/NumberInput.d.ts
4
4
  interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'type'> {
5
5
  /**
@@ -8,10 +8,10 @@ interface NumberInputProps extends Omit<React.InputHTMLAttributes<HTMLInputEleme
8
8
  */
9
9
  invalid?: boolean;
10
10
  /**
11
- * Width variant for the input
12
- * @default undefined (uses default max-width from form-control)
11
+ * Inline-size variant for the input
12
+ * @default undefined (uses default max-inline-size from form-control)
13
13
  */
14
- width?: FormControlWidth;
14
+ inlineSize?: FormControlInlineSize;
15
15
  /**
16
16
  * Allow decimal input (e.g. amounts). When false, uses GOV.UK pattern:
17
17
  * inputmode="numeric" + pattern="[0-9]*" for a numeric-only keyboard.
@@ -1 +1 @@
1
- {"version":3,"file":"NumberInput.d.mts","names":[],"sources":["../../src/NumberInput/NumberInput.tsx"],"mappings":";;;UAIiB,yBAAyB,KACxC,MAAM,oBAAoB;;;;;EAO1B;;;;;EAMA,QAAQ;;;;;;;EAQR;;;;EAKA;;;;;;;;;;;;;;;;;;;cAoBW,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"NumberInput.d.mts","names":[],"sources":["../../src/NumberInput/NumberInput.tsx"],"mappings":";;;UAIiB,yBAAyB,KACxC,MAAM,oBAAoB;;;;;EAO1B;;;;;EAMA,aAAa;;;;;;;EAQb;;;;EAKA;;;;;;;;;;;;;;;;;;;cAoBW,aAAW,MAAA,0BAAA,mBAAA,MAAA,cAAA"}
@@ -20,8 +20,8 @@ import { jsx } from "react/jsx-runtime";
20
20
  * <NumberInput invalid aria-invalid="true" aria-describedby="error" />
21
21
  * ```
22
22
  */
23
- const NumberInput = React.forwardRef(({ className, invalid, width, allowDecimals = false, ...props }, ref) => {
24
- const classes = classNames("dsn-text-input", width && `dsn-text-input--width-${width}`, className);
23
+ const NumberInput = React.forwardRef(({ className, invalid, inlineSize, allowDecimals = false, ...props }, ref) => {
24
+ const classes = classNames("dsn-text-input", inlineSize && `dsn-text-input--inline-size-${inlineSize}`, className);
25
25
  return /* @__PURE__ */ jsx("input", {
26
26
  ref,
27
27
  type: "text",
@@ -1 +1 @@
1
- {"version":3,"file":"NumberInput2.mjs","names":[],"sources":["../../src/NumberInput/NumberInput.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, FormControlWidth } from '@dsn-starter-kit/core';\nimport './NumberInput.css';\n\nexport interface NumberInputProps extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n 'type'\n> {\n /**\n * Whether the input is in an invalid state\n * @default false\n */\n invalid?: boolean;\n\n /**\n * Width variant for the input\n * @default undefined (uses default max-width from form-control)\n */\n width?: FormControlWidth;\n\n /**\n * Allow decimal input (e.g. amounts). When false, uses GOV.UK pattern:\n * inputmode=\"numeric\" + pattern=\"[0-9]*\" for a numeric-only keyboard.\n * When true, uses inputmode=\"decimal\" without pattern restriction.\n * @default false\n */\n allowDecimals?: boolean;\n\n /**\n * Additional CSS class names\n */\n className?: string;\n}\n\n/**\n * Number Input component\n * Uses type=\"text\" with inputmode=\"decimal\" for better mobile UX (no spinner buttons)\n *\n * @example\n * ```tsx\n * // Basic usage\n * <NumberInput placeholder=\"0,00\" />\n *\n * // With label\n * <FormFieldLabel htmlFor=\"amount\">Bedrag</FormFieldLabel>\n * <NumberInput id=\"amount\" />\n *\n * // Invalid state\n * <NumberInput invalid aria-invalid=\"true\" aria-describedby=\"error\" />\n * ```\n */\nexport const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(\n ({ className, invalid, width, allowDecimals = false, ...props }, ref) => {\n const classes = classNames(\n 'dsn-text-input',\n width && `dsn-text-input--width-${width}`,\n className\n );\n\n return (\n <input\n ref={ref}\n type=\"text\"\n inputMode={allowDecimals ? 'decimal' : 'numeric'}\n pattern={allowDecimals ? undefined : '[0-9]*'}\n className={classes}\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n {...props}\n />\n );\n }\n);\n\nNumberInput.displayName = 'NumberInput';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,cAAc,MAAM,YAC9B,EAAE,WAAW,SAAS,OAAO,gBAAgB,OAAO,GAAG,SAAS,QAAQ;CACvE,MAAM,UAAU,WACd,kBACA,SAAS,yBAAyB,SAClC,SACF;CAEA,OACE,oBAAC,SAAD;EACO;EACL,MAAK;EACL,WAAW,gBAAgB,YAAY;EACvC,SAAS,gBAAgB,KAAA,IAAY;EACrC,WAAW;EACX,gBAAc,WAAW,KAAA;EACzB,cAAa;EACb,GAAI;CACL,CAAA;AAEL,CACF;AAEA,YAAY,cAAc"}
1
+ {"version":3,"file":"NumberInput2.mjs","names":[],"sources":["../../src/NumberInput/NumberInput.tsx"],"sourcesContent":["import React from 'react';\nimport { classNames, FormControlInlineSize } from '@dsn-starter-kit/core';\nimport './NumberInput.css';\n\nexport interface NumberInputProps extends Omit<\n React.InputHTMLAttributes<HTMLInputElement>,\n 'type'\n> {\n /**\n * Whether the input is in an invalid state\n * @default false\n */\n invalid?: boolean;\n\n /**\n * Inline-size variant for the input\n * @default undefined (uses default max-inline-size from form-control)\n */\n inlineSize?: FormControlInlineSize;\n\n /**\n * Allow decimal input (e.g. amounts). When false, uses GOV.UK pattern:\n * inputmode=\"numeric\" + pattern=\"[0-9]*\" for a numeric-only keyboard.\n * When true, uses inputmode=\"decimal\" without pattern restriction.\n * @default false\n */\n allowDecimals?: boolean;\n\n /**\n * Additional CSS class names\n */\n className?: string;\n}\n\n/**\n * Number Input component\n * Uses type=\"text\" with inputmode=\"decimal\" for better mobile UX (no spinner buttons)\n *\n * @example\n * ```tsx\n * // Basic usage\n * <NumberInput placeholder=\"0,00\" />\n *\n * // With label\n * <FormFieldLabel htmlFor=\"amount\">Bedrag</FormFieldLabel>\n * <NumberInput id=\"amount\" />\n *\n * // Invalid state\n * <NumberInput invalid aria-invalid=\"true\" aria-describedby=\"error\" />\n * ```\n */\nexport const NumberInput = React.forwardRef<HTMLInputElement, NumberInputProps>(\n (\n { className, invalid, inlineSize, allowDecimals = false, ...props },\n ref\n ) => {\n const classes = classNames(\n 'dsn-text-input',\n inlineSize && `dsn-text-input--inline-size-${inlineSize}`,\n className\n );\n\n return (\n <input\n ref={ref}\n type=\"text\"\n inputMode={allowDecimals ? 'decimal' : 'numeric'}\n pattern={allowDecimals ? undefined : '[0-9]*'}\n className={classes}\n aria-invalid={invalid || undefined}\n autoComplete=\"off\"\n {...props}\n />\n );\n }\n);\n\nNumberInput.displayName = 'NumberInput';\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;AAmDA,MAAa,cAAc,MAAM,YAE7B,EAAE,WAAW,SAAS,YAAY,gBAAgB,OAAO,GAAG,SAC5D,QACG;CACH,MAAM,UAAU,WACd,kBACA,cAAc,+BAA+B,cAC7C,SACF;CAEA,OACE,oBAAC,SAAD;EACO;EACL,MAAK;EACL,WAAW,gBAAgB,YAAY;EACvC,SAAS,gBAAgB,KAAA,IAAY;EACrC,WAAW;EACX,gBAAc,WAAW,KAAA;EACzB,cAAa;EACb,GAAI;CACL,CAAA;AAEL,CACF;AAEA,YAAY,cAAc"}
@@ -1 +1 @@
1
- {"version":3,"file":"PageHeader.d.mts","names":[],"sources":["../../src/PageHeader/PageHeader.tsx"],"mappings":";;KASY;KACA;KACA;UAEK,wBAAwB,KACvC,MAAM,eAAe;;;;;EAOrB,UAAU,MAAM;;;;;;;;EAShB,SAAS;;;;;;;;;;;EAYT,SAAS;;;;;;;;;EAUT,cAAc;;;;;;;EAQd;;;;;EAMA,oBAAoB,MAAM;;;;;;EAO1B,yBAAyB,MAAM;;;;EAK/B,sBAAsB,MAAM;;;;;EAM5B,2BAA2B,MAAM;;;;;EAMjC,aAAa,MAAM;;;;;;EAOnB,kBAAkB,MAAM;;;;;;;EAQxB;;;;EAKA;;;;EAKA;;;;EAKA;;;;EAKA;;;;;EAQA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;EAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmCW,YAAU,MAAA,0BAAA,kBAAA,MAAA,cAAA"}
1
+ {"version":3,"file":"PageHeader.d.mts","names":[],"sources":["../../src/PageHeader/PageHeader.tsx"],"mappings":";;KASY;KAOA;KACA;UAEK,wBAAwB,KACvC,MAAM,eAAe;;;;;EAOrB,UAAU,MAAM;;;;;;;;EAShB,SAAS;;;;;;;;;;;EAYT,SAAS;;;;;;;;;EAUT,cAAc;;;;;;;EAQd;;;;;EAMA,oBAAoB,MAAM;;;;;;EAO1B,yBAAyB,MAAM;;;;EAK/B,sBAAsB,MAAM;;;;;EAM5B,2BAA2B,MAAM;;;;;EAMjC,aAAa,MAAM;;;;;;EAOnB,kBAAkB,MAAM;;;;;;;EAQxB;;;;EAKA;;;;EAKA;;;;EAKA;;;;EAKA;;;;;EAQA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;EAMA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;;;;;;EAOA;EAEA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;cAmCW,YAAU,MAAA,0BAAA,kBAAA,MAAA,cAAA"}
@@ -8,6 +8,10 @@ import React from "react";
8
8
  import { classNames } from "@dsn-starter-kit/core";
9
9
  import { Fragment, jsx, jsxs } from "react/jsx-runtime";
10
10
  //#region src/PageHeader/PageHeader.tsx
11
+ /** Auto-hide: boven deze scrollpositie (px) blijft de header altijd zichtbaar */
12
+ const AUTO_HIDE_OFFSET = 100;
13
+ /** Auto-hide: zoveel px omhoog scrollen voordat de header terugkomt */
14
+ const AUTO_HIDE_REVEAL_DISTANCE = 64;
11
15
  /**
12
16
  * PageHeader component
13
17
  * Primaire navigatieheader voor een pagina. Mobile-first implementatie.
@@ -55,11 +59,24 @@ const PageHeader = React.forwardRef(({ className, logoSlot, sticky = "none", lay
55
59
  React.useEffect(() => {
56
60
  if (sticky !== "auto-hide") return;
57
61
  let lastScrollY = window.scrollY;
62
+ let scrolledUp = 0;
58
63
  const handleScroll = () => {
59
64
  const currentScrollY = window.scrollY;
60
- const isScrollingDown = currentScrollY > lastScrollY && currentScrollY > 100;
61
- combinedRef.current?.setAttribute("data-hidden", String(isScrollingDown));
65
+ const delta = currentScrollY - lastScrollY;
62
66
  lastScrollY = currentScrollY;
67
+ let hidden;
68
+ if (currentScrollY <= AUTO_HIDE_OFFSET) {
69
+ hidden = false;
70
+ scrolledUp = 0;
71
+ } else if (delta > 0) {
72
+ hidden = true;
73
+ scrolledUp = 0;
74
+ } else if (delta < 0) {
75
+ scrolledUp -= delta;
76
+ if (scrolledUp < AUTO_HIDE_REVEAL_DISTANCE) return;
77
+ hidden = false;
78
+ } else return;
79
+ combinedRef.current?.setAttribute("data-hidden", String(hidden));
63
80
  };
64
81
  window.addEventListener("scroll", handleScroll, { passive: true });
65
82
  return () => window.removeEventListener("scroll", handleScroll);
@@ -256,6 +273,7 @@ const PageHeader = React.forwardRef(({ className, logoSlot, sticky = "none", lay
256
273
  isOpen: isMenuOpen,
257
274
  onClose: handleMenuClose,
258
275
  side: "left",
276
+ triggerRef: menuButtonRef,
259
277
  children: [/* @__PURE__ */ jsx(DrawerHeader, { children: /* @__PURE__ */ jsx(DrawerHeading, { children: menuButtonLabel }) }), /* @__PURE__ */ jsx(DrawerBody, { children: /* @__PURE__ */ jsxs(Stack, {
260
278
  space: "5xl",
261
279
  children: [primaryNavigation && /* @__PURE__ */ jsx("nav", {