@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.
- package/dist/Card/Card.d.mts +86 -21
- package/dist/Card/Card.d.mts.map +1 -1
- package/dist/Card/Card2.mjs +100 -17
- package/dist/Card/Card2.mjs.map +1 -1
- package/dist/Card/index.d.mts +2 -2
- package/dist/DateInput/DateInput.d.mts +1 -1
- package/dist/DateInput/DateInput.d.mts.map +1 -1
- package/dist/DateInput/DateInput2.mjs +1 -2
- package/dist/DateInput/DateInput2.mjs.map +1 -1
- package/dist/DateInputGroup/DateInputGroup2.mjs +3 -3
- package/dist/DateInputGroup/DateInputGroup2.mjs.map +1 -1
- package/dist/Drawer/Drawer.d.mts +11 -3
- package/dist/Drawer/Drawer.d.mts.map +1 -1
- package/dist/Drawer/Drawer2.mjs +28 -4
- package/dist/Drawer/Drawer2.mjs.map +1 -1
- package/dist/EmailInput/EmailInput.d.mts +10 -5
- package/dist/EmailInput/EmailInput.d.mts.map +1 -1
- package/dist/EmailInput/EmailInput2.mjs +2 -2
- package/dist/EmailInput/EmailInput2.mjs.map +1 -1
- package/dist/ModalDialog/ModalDialog.d.mts +12 -2
- package/dist/ModalDialog/ModalDialog.d.mts.map +1 -1
- package/dist/ModalDialog/ModalDialog2.mjs +35 -8
- package/dist/ModalDialog/ModalDialog2.mjs.map +1 -1
- package/dist/NumberInput/NumberInput.d.mts +4 -4
- package/dist/NumberInput/NumberInput.d.mts.map +1 -1
- package/dist/NumberInput/NumberInput2.mjs +2 -2
- package/dist/NumberInput/NumberInput2.mjs.map +1 -1
- package/dist/PageHeader/PageHeader.d.mts.map +1 -1
- package/dist/PageHeader/PageHeader2.mjs +20 -2
- package/dist/PageHeader/PageHeader2.mjs.map +1 -1
- package/dist/PasswordInput/PasswordInput.d.mts +10 -5
- package/dist/PasswordInput/PasswordInput.d.mts.map +1 -1
- package/dist/PasswordInput/PasswordInput2.mjs +2 -2
- package/dist/PasswordInput/PasswordInput2.mjs.map +1 -1
- package/dist/SearchInput/SearchInput.d.mts +10 -5
- package/dist/SearchInput/SearchInput.d.mts.map +1 -1
- package/dist/SearchInput/SearchInput2.mjs +2 -3
- package/dist/SearchInput/SearchInput2.mjs.map +1 -1
- package/dist/Select/Select.d.mts +6 -6
- package/dist/Select/Select.d.mts.map +1 -1
- package/dist/Select/Select2.mjs +4 -5
- package/dist/Select/Select2.mjs.map +1 -1
- package/dist/TelephoneInput/TelephoneInput.d.mts +10 -5
- package/dist/TelephoneInput/TelephoneInput.d.mts.map +1 -1
- package/dist/TelephoneInput/TelephoneInput2.mjs +2 -2
- package/dist/TelephoneInput/TelephoneInput2.mjs.map +1 -1
- package/dist/TextArea/TextArea.d.mts +10 -5
- package/dist/TextArea/TextArea.d.mts.map +1 -1
- package/dist/TextArea/TextArea2.mjs +2 -2
- package/dist/TextArea/TextArea2.mjs.map +1 -1
- package/dist/TextInput/TextInput.d.mts +4 -4
- package/dist/TextInput/TextInput.d.mts.map +1 -1
- package/dist/TextInput/TextInput2.mjs +2 -2
- package/dist/TextInput/TextInput2.mjs.map +1 -1
- package/dist/TimeInput/TimeInput.d.mts +1 -1
- package/dist/TimeInput/TimeInput.d.mts.map +1 -1
- package/dist/TimeInput/TimeInput2.mjs +1 -2
- package/dist/TimeInput/TimeInput2.mjs.map +1 -1
- package/dist/index.cjs +303 -53
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +5439 -3838
- package/dist/index.d.cts +163 -55
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +2 -2
- package/dist/utils/focusTrap.mjs +99 -0
- package/dist/utils/focusTrap.mjs.map +1 -0
- package/package.json +3 -3
package/dist/Drawer/Drawer2.mjs
CHANGED
|
@@ -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({
|
|
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 {
|
|
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
|
-
*
|
|
12
|
-
* @default undefined (uses default max-
|
|
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
|
-
|
|
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":"
|
|
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,
|
|
24
|
-
const classes = classNames("dsn-text-input",
|
|
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,
|
|
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)
|
|
25
|
-
* Dit garandeert
|
|
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":";;
|
|
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({
|
|
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)
|
|
14
|
-
* Dit garandeert
|
|
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)
|
|
42
|
-
|
|
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 {
|
|
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
|
-
*
|
|
12
|
-
* @default undefined (uses default max-
|
|
11
|
+
* Inline-size variant for the input
|
|
12
|
+
* @default undefined (uses default max-inline-size from form-control)
|
|
13
13
|
*/
|
|
14
|
-
|
|
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,
|
|
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,
|
|
24
|
-
const classes = classNames("dsn-text-input",
|
|
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,
|
|
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;
|
|
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
|
|
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", {
|