@becket-ui/react 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/LICENSE +21 -0
  2. package/dist/components/Badge.d.ts +22 -0
  3. package/dist/components/Badge.d.ts.map +1 -0
  4. package/dist/components/Badge.js +39 -0
  5. package/dist/components/Badge.js.map +1 -0
  6. package/dist/components/Button.d.ts +24 -0
  7. package/dist/components/Button.d.ts.map +1 -0
  8. package/dist/components/Button.js +46 -0
  9. package/dist/components/Button.js.map +1 -0
  10. package/dist/components/Button.test.js +45 -0
  11. package/dist/components/Button.test.js.map +1 -0
  12. package/dist/components/Card.d.ts +37 -0
  13. package/dist/components/Card.d.ts.map +1 -0
  14. package/dist/components/Card.js +75 -0
  15. package/dist/components/Card.js.map +1 -0
  16. package/dist/components/Checkbox.d.ts +14 -0
  17. package/dist/components/Checkbox.d.ts.map +1 -0
  18. package/dist/components/Checkbox.js +46 -0
  19. package/dist/components/Checkbox.js.map +1 -0
  20. package/dist/components/Dialog.d.ts +41 -0
  21. package/dist/components/Dialog.d.ts.map +1 -0
  22. package/dist/components/Dialog.js +173 -0
  23. package/dist/components/Dialog.js.map +1 -0
  24. package/dist/components/Dialog.test.js +89 -0
  25. package/dist/components/Dialog.test.js.map +1 -0
  26. package/dist/components/Drawer.d.ts +38 -0
  27. package/dist/components/Drawer.d.ts.map +1 -0
  28. package/dist/components/Drawer.js +174 -0
  29. package/dist/components/Drawer.js.map +1 -0
  30. package/dist/components/Drawer.test.js +50 -0
  31. package/dist/components/Drawer.test.js.map +1 -0
  32. package/dist/components/Field.d.ts +37 -0
  33. package/dist/components/Field.d.ts.map +1 -0
  34. package/dist/components/Field.js +186 -0
  35. package/dist/components/Field.js.map +1 -0
  36. package/dist/components/Field.test.js +33 -0
  37. package/dist/components/Field.test.js.map +1 -0
  38. package/dist/components/Flex.d.ts +38 -0
  39. package/dist/components/Flex.d.ts.map +1 -0
  40. package/dist/components/Flex.js +85 -0
  41. package/dist/components/Flex.js.map +1 -0
  42. package/dist/components/Heading.d.ts +14 -0
  43. package/dist/components/Heading.d.ts.map +1 -0
  44. package/dist/components/Heading.js +33 -0
  45. package/dist/components/Heading.js.map +1 -0
  46. package/dist/components/Hide.d.ts +39 -0
  47. package/dist/components/Hide.d.ts.map +1 -0
  48. package/dist/components/Hide.js +51 -0
  49. package/dist/components/Hide.js.map +1 -0
  50. package/dist/components/Link.d.ts +14 -0
  51. package/dist/components/Link.d.ts.map +1 -0
  52. package/dist/components/Link.js +23 -0
  53. package/dist/components/Link.js.map +1 -0
  54. package/dist/components/Menu.d.ts +11 -0
  55. package/dist/components/Menu.d.ts.map +1 -0
  56. package/dist/components/Menu.js +132 -0
  57. package/dist/components/Menu.js.map +1 -0
  58. package/dist/components/Menu.test.js +69 -0
  59. package/dist/components/Menu.test.js.map +1 -0
  60. package/dist/components/Radio.d.ts +38 -0
  61. package/dist/components/Radio.d.ts.map +1 -0
  62. package/dist/components/Radio.js +144 -0
  63. package/dist/components/Radio.js.map +1 -0
  64. package/dist/components/Select.d.ts +17 -0
  65. package/dist/components/Select.d.ts.map +1 -0
  66. package/dist/components/Select.js +33 -0
  67. package/dist/components/Select.js.map +1 -0
  68. package/dist/components/Separator.d.ts +14 -0
  69. package/dist/components/Separator.d.ts.map +1 -0
  70. package/dist/components/Separator.js +25 -0
  71. package/dist/components/Separator.js.map +1 -0
  72. package/dist/components/SimpleGrid.d.ts +26 -0
  73. package/dist/components/SimpleGrid.d.ts.map +1 -0
  74. package/dist/components/SimpleGrid.js +98 -0
  75. package/dist/components/SimpleGrid.js.map +1 -0
  76. package/dist/components/Spinner.d.ts +10 -0
  77. package/dist/components/Spinner.d.ts.map +1 -0
  78. package/dist/components/Spinner.js +25 -0
  79. package/dist/components/Spinner.js.map +1 -0
  80. package/dist/components/Stack.d.ts +32 -0
  81. package/dist/components/Stack.d.ts.map +1 -0
  82. package/dist/components/Stack.js +81 -0
  83. package/dist/components/Stack.js.map +1 -0
  84. package/dist/components/Switch.d.ts +20 -0
  85. package/dist/components/Switch.d.ts.map +1 -0
  86. package/dist/components/Switch.js +83 -0
  87. package/dist/components/Switch.js.map +1 -0
  88. package/dist/components/Switch.test.js +28 -0
  89. package/dist/components/Switch.test.js.map +1 -0
  90. package/dist/components/Table.d.ts +23 -0
  91. package/dist/components/Table.d.ts.map +1 -0
  92. package/dist/components/Table.js +90 -0
  93. package/dist/components/Table.js.map +1 -0
  94. package/dist/components/Tabs.d.ts +21 -0
  95. package/dist/components/Tabs.d.ts.map +1 -0
  96. package/dist/components/Tabs.js +140 -0
  97. package/dist/components/Tabs.js.map +1 -0
  98. package/dist/components/Tabs.test.js +41 -0
  99. package/dist/components/Tabs.test.js.map +1 -0
  100. package/dist/components/Tag.d.ts +22 -0
  101. package/dist/components/Tag.d.ts.map +1 -0
  102. package/dist/components/Tag.js +39 -0
  103. package/dist/components/Tag.js.map +1 -0
  104. package/dist/components/Text.d.ts +9 -0
  105. package/dist/components/Text.d.ts.map +1 -0
  106. package/dist/components/Text.js +18 -0
  107. package/dist/components/Text.js.map +1 -0
  108. package/dist/components/TextField.d.ts +19 -0
  109. package/dist/components/TextField.d.ts.map +1 -0
  110. package/dist/components/TextField.js +32 -0
  111. package/dist/components/TextField.js.map +1 -0
  112. package/dist/components/Textarea.d.ts +17 -0
  113. package/dist/components/Textarea.d.ts.map +1 -0
  114. package/dist/components/Textarea.js +34 -0
  115. package/dist/components/Textarea.js.map +1 -0
  116. package/dist/components/Tooltip.d.ts +17 -0
  117. package/dist/components/Tooltip.d.ts.map +1 -0
  118. package/dist/components/Tooltip.js +217 -0
  119. package/dist/components/Tooltip.js.map +1 -0
  120. package/dist/components/Tooltip.test.js +58 -0
  121. package/dist/components/Tooltip.test.js.map +1 -0
  122. package/dist/helpers/gradientBorder.d.ts +5 -0
  123. package/dist/helpers/gradientBorder.d.ts.map +1 -0
  124. package/dist/helpers/gradientBorder.js +25 -0
  125. package/dist/helpers/gradientBorder.js.map +1 -0
  126. package/dist/helpers/labelColors.d.ts +17 -0
  127. package/dist/helpers/labelColors.d.ts.map +1 -0
  128. package/dist/helpers/labelColors.js +54 -0
  129. package/dist/helpers/labelColors.js.map +1 -0
  130. package/dist/helpers/nativeDialog.d.ts +23 -0
  131. package/dist/helpers/nativeDialog.d.ts.map +1 -0
  132. package/dist/helpers/nativeDialog.js +85 -0
  133. package/dist/helpers/nativeDialog.js.map +1 -0
  134. package/dist/helpers/responsive.d.ts +63 -0
  135. package/dist/helpers/responsive.d.ts.map +1 -0
  136. package/dist/helpers/responsive.js +78 -0
  137. package/dist/helpers/responsive.js.map +1 -0
  138. package/dist/helpers/storybookControls.d.ts +16 -0
  139. package/dist/helpers/storybookControls.d.ts.map +1 -0
  140. package/dist/helpers/storybookControls.js +40 -0
  141. package/dist/helpers/storybookControls.js.map +1 -0
  142. package/dist/index.d.ts +35 -0
  143. package/dist/index.d.ts.map +1 -0
  144. package/dist/index.js +158 -0
  145. package/dist/index.js.map +1 -0
  146. package/package.json +79 -0
@@ -0,0 +1,173 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ createContext,
5
+ forwardRef,
6
+ useContext,
7
+ useLayoutEffect,
8
+ useRef
9
+ } from "react";
10
+ import { dialog as dialogRecipe } from "@becket-ui/tokens/recipes";
11
+ import { assignRef, useNativeDialog } from "../helpers/nativeDialog";
12
+ const DialogContext = createContext(null);
13
+ function useDialog() {
14
+ const ctx = useContext(DialogContext);
15
+ if (!ctx) {
16
+ throw new Error("Dialog parts must be used within Dialog");
17
+ }
18
+ return ctx;
19
+ }
20
+ function mergeClassName(...classes) {
21
+ return classes.filter(Boolean).join(" ");
22
+ }
23
+ const Dialog = forwardRef(
24
+ ({
25
+ open,
26
+ defaultOpen = false,
27
+ onOpenChange,
28
+ title,
29
+ footer,
30
+ role = "dialog",
31
+ className,
32
+ children,
33
+ onClick,
34
+ ...props
35
+ }, ref) => {
36
+ const innerRef = useRef(null);
37
+ const { setOpen, titleId, descriptionId, labelledBy, describedBy, registerTitle, registerDescription } = useNativeDialog(innerRef, {
38
+ open,
39
+ defaultOpen,
40
+ onOpenChange,
41
+ titlePresent: title != null,
42
+ descriptionPresent: children != null && children !== false
43
+ });
44
+ const styles = dialogRecipe();
45
+ const composeChrome = title != null || footer != null;
46
+ const lightDismiss = role !== "alertdialog";
47
+ return /* @__PURE__ */ jsx(
48
+ DialogContext.Provider,
49
+ {
50
+ value: { setOpen, titleId, descriptionId, registerTitle, registerDescription },
51
+ children: /* @__PURE__ */ jsx(
52
+ "dialog",
53
+ {
54
+ ref: (node) => {
55
+ innerRef.current = node;
56
+ assignRef(ref, node);
57
+ },
58
+ role,
59
+ tabIndex: -1,
60
+ className: mergeClassName(styles.root, className),
61
+ ...props,
62
+ "aria-modal": "true",
63
+ "aria-labelledby": labelledBy,
64
+ "aria-describedby": describedBy,
65
+ onClick: (event) => {
66
+ onClick?.(event);
67
+ if (event.defaultPrevented || !lightDismiss) return;
68
+ if (event.target === event.currentTarget) setOpen(false);
69
+ },
70
+ onClose: () => setOpen(false),
71
+ children: composeChrome ? /* @__PURE__ */ jsxs(Fragment, { children: [
72
+ title != null && /* @__PURE__ */ jsxs(DialogHeader, { children: [
73
+ /* @__PURE__ */ jsx(DialogTitle, { children: title }),
74
+ /* @__PURE__ */ jsx(DialogClose, {})
75
+ ] }),
76
+ /* @__PURE__ */ jsx(DialogBody, { children }),
77
+ footer != null && /* @__PURE__ */ jsx(DialogFooter, { children: footer })
78
+ ] }) : children
79
+ }
80
+ )
81
+ }
82
+ );
83
+ }
84
+ );
85
+ Dialog.displayName = "Dialog";
86
+ const DialogHeader = forwardRef(
87
+ ({ className, ...props }, ref) => {
88
+ const styles = dialogRecipe();
89
+ return /* @__PURE__ */ jsx("div", { ref, ...props, className: mergeClassName(styles.header, className) });
90
+ }
91
+ );
92
+ DialogHeader.displayName = "DialogHeader";
93
+ const DialogTitle = forwardRef(
94
+ ({ className, as: Comp = "h2", ...props }, ref) => {
95
+ const { titleId, registerTitle } = useDialog();
96
+ const styles = dialogRecipe();
97
+ useLayoutEffect(() => {
98
+ registerTitle(true);
99
+ return () => registerTitle(false);
100
+ }, [registerTitle]);
101
+ return /* @__PURE__ */ jsx(Comp, { ref, id: titleId, ...props, className: mergeClassName(styles.title, className) });
102
+ }
103
+ );
104
+ DialogTitle.displayName = "DialogTitle";
105
+ const DialogBody = forwardRef(
106
+ ({ className, children, ...props }, ref) => {
107
+ const { descriptionId, registerDescription } = useDialog();
108
+ const styles = dialogRecipe();
109
+ const hasContent = children != null && children !== false;
110
+ useLayoutEffect(() => {
111
+ registerDescription(hasContent);
112
+ return () => registerDescription(false);
113
+ }, [hasContent, registerDescription]);
114
+ return /* @__PURE__ */ jsx(
115
+ "div",
116
+ {
117
+ ref,
118
+ id: descriptionId,
119
+ ...props,
120
+ className: mergeClassName(styles.body, className),
121
+ children
122
+ }
123
+ );
124
+ }
125
+ );
126
+ DialogBody.displayName = "DialogBody";
127
+ const DialogFooter = forwardRef(
128
+ ({ className, ...props }, ref) => {
129
+ const styles = dialogRecipe();
130
+ return /* @__PURE__ */ jsx(
131
+ "div",
132
+ {
133
+ ref,
134
+ "data-becket-dialog-footer": "",
135
+ ...props,
136
+ className: mergeClassName(styles.footer, className)
137
+ }
138
+ );
139
+ }
140
+ );
141
+ DialogFooter.displayName = "DialogFooter";
142
+ const DialogClose = forwardRef(
143
+ ({ className, children = "\xD7", type = "button", onClick, ...props }, ref) => {
144
+ const { setOpen } = useDialog();
145
+ const styles = dialogRecipe();
146
+ return /* @__PURE__ */ jsx(
147
+ "button",
148
+ {
149
+ ref,
150
+ type,
151
+ "data-becket-dialog-close": "",
152
+ ...props,
153
+ "aria-label": props["aria-label"] ?? "Close",
154
+ className: mergeClassName(styles.close, className),
155
+ onClick: (event) => {
156
+ onClick?.(event);
157
+ if (!event.defaultPrevented) setOpen(false);
158
+ },
159
+ children
160
+ }
161
+ );
162
+ }
163
+ );
164
+ DialogClose.displayName = "DialogClose";
165
+ export {
166
+ Dialog,
167
+ DialogBody,
168
+ DialogClose,
169
+ DialogFooter,
170
+ DialogHeader,
171
+ DialogTitle
172
+ };
173
+ //# sourceMappingURL=Dialog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/Dialog.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n forwardRef,\n useContext,\n useLayoutEffect,\n useRef,\n type ComponentPropsWithoutRef,\n type DialogHTMLAttributes,\n type ElementType,\n type MouseEvent,\n type ReactNode,\n} from 'react';\nimport { dialog as dialogRecipe } from '@becket-ui/tokens/recipes';\nimport { assignRef, useNativeDialog } from '../helpers/nativeDialog';\n\ntype DialogRole = 'dialog' | 'alertdialog';\n\ntype DialogContextValue = {\n setOpen: (open: boolean) => void;\n titleId: string;\n descriptionId: string;\n registerTitle: (present: boolean) => void;\n registerDescription: (present: boolean) => void;\n};\n\nconst DialogContext = createContext<DialogContextValue | null>(null);\n\nfunction useDialog() {\n const ctx = useContext(DialogContext);\n if (!ctx) {\n throw new Error('Dialog parts must be used within Dialog');\n }\n return ctx;\n}\n\nfunction mergeClassName(...classes: (string | undefined)[]): string {\n return classes.filter(Boolean).join(' ');\n}\n\nexport type DialogProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'title'> & {\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n /** Composed heading. When set, Dialog renders header + close. */\n title?: ReactNode;\n /** Composed actions (pass `Button` nodes). When set, Dialog renders the footer. */\n footer?: ReactNode;\n /**\n * `alertdialog` is for confirms: no backdrop dismiss, Escape still closes,\n * initial focus is the first footer action (Cancel) when present.\n */\n role?: DialogRole;\n children?: ReactNode;\n};\n\nexport const Dialog = forwardRef<HTMLDialogElement, DialogProps>(\n (\n {\n open,\n defaultOpen = false,\n onOpenChange,\n title,\n footer,\n role = 'dialog',\n className,\n children,\n onClick,\n ...props\n },\n ref,\n ) => {\n const innerRef = useRef<HTMLDialogElement>(null);\n const { setOpen, titleId, descriptionId, labelledBy, describedBy, registerTitle, registerDescription } =\n useNativeDialog(innerRef, {\n open,\n defaultOpen,\n onOpenChange,\n titlePresent: title != null,\n descriptionPresent: children != null && children !== false,\n });\n const styles = dialogRecipe();\n const composeChrome = title != null || footer != null;\n const lightDismiss = role !== 'alertdialog';\n\n return (\n <DialogContext.Provider\n value={{ setOpen, titleId, descriptionId, registerTitle, registerDescription }}\n >\n <dialog\n ref={(node) => {\n innerRef.current = node;\n assignRef(ref, node);\n }}\n role={role}\n tabIndex={-1}\n className={mergeClassName(styles.root, className)}\n {...props}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n onClick={(event: MouseEvent<HTMLDialogElement>) => {\n onClick?.(event);\n if (event.defaultPrevented || !lightDismiss) return;\n if (event.target === event.currentTarget) setOpen(false);\n }}\n onClose={() => setOpen(false)}\n >\n {composeChrome ? (\n <>\n {title != null && (\n <DialogHeader>\n <DialogTitle>{title}</DialogTitle>\n <DialogClose />\n </DialogHeader>\n )}\n <DialogBody>{children}</DialogBody>\n {footer != null && <DialogFooter>{footer}</DialogFooter>}\n </>\n ) : (\n children\n )}\n </dialog>\n </DialogContext.Provider>\n );\n },\n);\n\nDialog.displayName = 'Dialog';\n\nexport const DialogHeader = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n ({ className, ...props }, ref) => {\n const styles = dialogRecipe();\n return <div ref={ref} {...props} className={mergeClassName(styles.header, className)} />;\n },\n);\nDialogHeader.displayName = 'DialogHeader';\n\ntype DialogTitleProps = ComponentPropsWithoutRef<'h2'> & { as?: ElementType };\n\nexport const DialogTitle = forwardRef<HTMLHeadingElement, DialogTitleProps>(\n ({ className, as: Comp = 'h2', ...props }, ref) => {\n const { titleId, registerTitle } = useDialog();\n const styles = dialogRecipe();\n useLayoutEffect(() => {\n registerTitle(true);\n return () => registerTitle(false);\n }, [registerTitle]);\n return (\n <Comp ref={ref} id={titleId} {...props} className={mergeClassName(styles.title, className)} />\n );\n },\n);\nDialogTitle.displayName = 'DialogTitle';\n\nexport const DialogBody = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n ({ className, children, ...props }, ref) => {\n const { descriptionId, registerDescription } = useDialog();\n const styles = dialogRecipe();\n const hasContent = children != null && children !== false;\n useLayoutEffect(() => {\n registerDescription(hasContent);\n return () => registerDescription(false);\n }, [hasContent, registerDescription]);\n return (\n <div\n ref={ref}\n id={descriptionId}\n {...props}\n className={mergeClassName(styles.body, className)}\n >\n {children}\n </div>\n );\n },\n);\nDialogBody.displayName = 'DialogBody';\n\nexport const DialogFooter = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n ({ className, ...props }, ref) => {\n const styles = dialogRecipe();\n return (\n <div\n ref={ref}\n data-becket-dialog-footer=\"\"\n {...props}\n className={mergeClassName(styles.footer, className)}\n />\n );\n },\n);\nDialogFooter.displayName = 'DialogFooter';\n\ntype DialogCloseProps = ComponentPropsWithoutRef<'button'>;\n\nexport const DialogClose = forwardRef<HTMLButtonElement, DialogCloseProps>(\n ({ className, children = '×', type = 'button', onClick, ...props }, ref) => {\n const { setOpen } = useDialog();\n const styles = dialogRecipe();\n return (\n <button\n ref={ref}\n type={type}\n data-becket-dialog-close=\"\"\n {...props}\n aria-label={props['aria-label'] ?? 'Close'}\n className={mergeClassName(styles.close, className)}\n onClick={(event) => {\n onClick?.(event);\n if (!event.defaultPrevented) setOpen(false);\n }}\n >\n {children}\n </button>\n );\n },\n);\nDialogClose.displayName = 'DialogClose';\n"],"mappings":"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","names":[]}
@@ -0,0 +1,89 @@
1
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
5
+ import { assertNoAxeViolations } from "@becket-ui/a11y";
6
+ import { Button } from "./Button";
7
+ import { Dialog } from "./Dialog";
8
+ function ConfirmDialog({
9
+ open = true,
10
+ role = "dialog",
11
+ onOpenChange = () => {
12
+ },
13
+ ariaLabel
14
+ }) {
15
+ return /* @__PURE__ */ jsx(
16
+ Dialog,
17
+ {
18
+ open,
19
+ onOpenChange,
20
+ role,
21
+ title: ariaLabel ? void 0 : "Confirm",
22
+ "aria-label": ariaLabel,
23
+ footer: /* @__PURE__ */ jsxs(Fragment, { children: [
24
+ /* @__PURE__ */ jsx(Button, { visual: "outline", onClick: () => onOpenChange(false), children: "Cancel" }),
25
+ /* @__PURE__ */ jsx(Button, { visual: "primary", onClick: () => onOpenChange(false), children: "Confirm" })
26
+ ] }),
27
+ children: "Native dialog body."
28
+ }
29
+ );
30
+ }
31
+ describe("Dialog", () => {
32
+ it("points labelledby and describedby at rendered title and body", () => {
33
+ render(/* @__PURE__ */ jsx(ConfirmDialog, {}));
34
+ const dialog = screen.getByRole("dialog", { name: "Confirm" });
35
+ const labelledBy = dialog.getAttribute("aria-labelledby");
36
+ const describedBy = dialog.getAttribute("aria-describedby");
37
+ expect(document.getElementById(labelledBy)?.textContent).toBe("Confirm");
38
+ expect(document.getElementById(describedBy)?.textContent).toContain("Native dialog body");
39
+ });
40
+ it("omits labelledby when there is no title node", () => {
41
+ render(/* @__PURE__ */ jsx(ConfirmDialog, { ariaLabel: "Unnamed confirm" }));
42
+ const dialog = screen.getByRole("dialog", { name: "Unnamed confirm" });
43
+ expect(dialog.getAttribute("aria-labelledby")).toBeNull();
44
+ });
45
+ it("moves initial focus to Cancel, not Close", async () => {
46
+ render(/* @__PURE__ */ jsx(ConfirmDialog, {}));
47
+ await waitFor(() => {
48
+ expect(document.activeElement).toBe(screen.getByRole("button", { name: "Cancel" }));
49
+ });
50
+ expect(document.activeElement).not.toBe(screen.getByRole("button", { name: "Close" }));
51
+ });
52
+ it("tabs from the first footer action to Confirm", async () => {
53
+ const user = userEvent.setup();
54
+ render(/* @__PURE__ */ jsx(ConfirmDialog, {}));
55
+ await waitFor(() => {
56
+ expect(document.activeElement).toBe(screen.getByRole("button", { name: "Cancel" }));
57
+ });
58
+ await user.tab();
59
+ expect(document.activeElement).toBe(screen.getByRole("button", { name: "Confirm" }));
60
+ });
61
+ it("closes a dialog on backdrop click but not an alertdialog", () => {
62
+ const onDialog = vi.fn();
63
+ const { rerender } = render(/* @__PURE__ */ jsx(ConfirmDialog, { onOpenChange: onDialog }));
64
+ fireEvent.click(screen.getByRole("dialog", { name: "Confirm" }));
65
+ expect(onDialog).toHaveBeenCalledWith(false);
66
+ const onAlert = vi.fn();
67
+ rerender(/* @__PURE__ */ jsx(ConfirmDialog, { role: "alertdialog", onOpenChange: onAlert }));
68
+ fireEvent.click(screen.getByRole("alertdialog", { name: "Confirm" }));
69
+ expect(onAlert).not.toHaveBeenCalled();
70
+ });
71
+ it("lets Close aria-label be overridden", () => {
72
+ render(
73
+ /* @__PURE__ */ jsx(Dialog, { open: true, title: "Confirm", children: "Body" })
74
+ );
75
+ expect(screen.getByRole("button", { name: "Close" })).toBeTruthy();
76
+ });
77
+ it("closes on Escape via the native dialog close event", () => {
78
+ const onOpenChange = vi.fn();
79
+ render(/* @__PURE__ */ jsx(ConfirmDialog, { onOpenChange }));
80
+ fireEvent.keyDown(screen.getByRole("dialog", { name: "Confirm" }), { key: "Escape" });
81
+ fireEvent(screen.getByRole("dialog", { name: "Confirm" }), new Event("close", { bubbles: true }));
82
+ expect(onOpenChange).toHaveBeenCalledWith(false);
83
+ });
84
+ it("has no axe violations when open", async () => {
85
+ const { container } = render(/* @__PURE__ */ jsx(ConfirmDialog, {}));
86
+ await assertNoAxeViolations(container);
87
+ });
88
+ });
89
+ //# sourceMappingURL=Dialog.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/Dialog.test.tsx"],"sourcesContent":["import { describe, expect, it, vi } from 'vitest';\nimport { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport userEvent from '@testing-library/user-event';\nimport { assertNoAxeViolations } from '@becket-ui/a11y';\n\nimport { Button } from './Button';\nimport { Dialog } from './Dialog';\n\nfunction ConfirmDialog({\n open = true,\n role = 'dialog' as 'dialog' | 'alertdialog',\n onOpenChange = () => {},\n ariaLabel,\n}: {\n open?: boolean;\n role?: 'dialog' | 'alertdialog';\n onOpenChange?: (open: boolean) => void;\n ariaLabel?: string;\n}) {\n return (\n <Dialog\n open={open}\n onOpenChange={onOpenChange}\n role={role}\n title={ariaLabel ? undefined : 'Confirm'}\n aria-label={ariaLabel}\n footer={\n <>\n <Button visual=\"outline\" onClick={() => onOpenChange(false)}>\n Cancel\n </Button>\n <Button visual=\"primary\" onClick={() => onOpenChange(false)}>\n Confirm\n </Button>\n </>\n }\n >\n Native dialog body.\n </Dialog>\n );\n}\n\ndescribe('Dialog', () => {\n it('points labelledby and describedby at rendered title and body', () => {\n render(<ConfirmDialog />);\n const dialog = screen.getByRole('dialog', { name: 'Confirm' });\n const labelledBy = dialog.getAttribute('aria-labelledby');\n const describedBy = dialog.getAttribute('aria-describedby');\n expect(document.getElementById(labelledBy as string)?.textContent).toBe('Confirm');\n expect(document.getElementById(describedBy as string)?.textContent).toContain('Native dialog body');\n });\n\n it('omits labelledby when there is no title node', () => {\n render(<ConfirmDialog ariaLabel=\"Unnamed confirm\" />);\n const dialog = screen.getByRole('dialog', { name: 'Unnamed confirm' });\n expect(dialog.getAttribute('aria-labelledby')).toBeNull();\n });\n\n it('moves initial focus to Cancel, not Close', async () => {\n render(<ConfirmDialog />);\n await waitFor(() => {\n expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Cancel' }));\n });\n expect(document.activeElement).not.toBe(screen.getByRole('button', { name: 'Close' }));\n });\n\n it('tabs from the first footer action to Confirm', async () => {\n const user = userEvent.setup();\n render(<ConfirmDialog />);\n await waitFor(() => {\n expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Cancel' }));\n });\n await user.tab();\n expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Confirm' }));\n });\n\n it('closes a dialog on backdrop click but not an alertdialog', () => {\n const onDialog = vi.fn();\n const { rerender } = render(<ConfirmDialog onOpenChange={onDialog} />);\n fireEvent.click(screen.getByRole('dialog', { name: 'Confirm' }));\n expect(onDialog).toHaveBeenCalledWith(false);\n\n const onAlert = vi.fn();\n rerender(<ConfirmDialog role=\"alertdialog\" onOpenChange={onAlert} />);\n fireEvent.click(screen.getByRole('alertdialog', { name: 'Confirm' }));\n expect(onAlert).not.toHaveBeenCalled();\n });\n\n it('lets Close aria-label be overridden', () => {\n render(\n <Dialog open title=\"Confirm\">\n Body\n </Dialog>,\n );\n expect(screen.getByRole('button', { name: 'Close' })).toBeTruthy();\n });\n\n it('closes on Escape via the native dialog close event', () => {\n const onOpenChange = vi.fn();\n render(<ConfirmDialog onOpenChange={onOpenChange} />);\n fireEvent.keyDown(screen.getByRole('dialog', { name: 'Confirm' }), { key: 'Escape' });\n fireEvent(screen.getByRole('dialog', { name: 'Confirm' }), new Event('close', { bubbles: true }));\n expect(onOpenChange).toHaveBeenCalledWith(false);\n });\n\n it('has no axe violations when open', async () => {\n const { container } = render(<ConfirmDialog />);\n await assertNoAxeViolations(container);\n });\n});\n"],"mappings":"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","names":[]}
@@ -0,0 +1,38 @@
1
+ import { type DialogHTMLAttributes, type ElementType, type ReactNode } from 'react';
2
+ type DrawerPlacement = 'start' | 'end';
3
+ type DrawerRole = 'dialog' | 'alertdialog';
4
+ export type DrawerProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'title'> & {
5
+ open?: boolean;
6
+ defaultOpen?: boolean;
7
+ onOpenChange?: (open: boolean) => void;
8
+ placement?: DrawerPlacement;
9
+ /** Composed heading. When set, Drawer renders header + close. */
10
+ title?: ReactNode;
11
+ /** Composed actions (pass `Button` nodes). When set, Drawer renders the footer. */
12
+ footer?: ReactNode;
13
+ /** Same as Dialog: `alertdialog` disables backdrop dismiss. */
14
+ role?: DrawerRole;
15
+ children?: ReactNode;
16
+ };
17
+ export declare const Drawer: import("react").ForwardRefExoticComponent<Omit<DialogHTMLAttributes<HTMLDialogElement>, "open" | "title"> & {
18
+ open?: boolean;
19
+ defaultOpen?: boolean;
20
+ onOpenChange?: (open: boolean) => void;
21
+ placement?: DrawerPlacement;
22
+ /** Composed heading. When set, Drawer renders header + close. */
23
+ title?: ReactNode;
24
+ /** Composed actions (pass `Button` nodes). When set, Drawer renders the footer. */
25
+ footer?: ReactNode;
26
+ /** Same as Dialog: `alertdialog` disables backdrop dismiss. */
27
+ role?: DrawerRole;
28
+ children?: ReactNode;
29
+ } & import("react").RefAttributes<HTMLDialogElement>>;
30
+ export declare const DrawerHeader: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
31
+ export declare const DrawerTitle: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLHeadingElement>, HTMLHeadingElement>, "ref"> & {
32
+ as?: ElementType;
33
+ } & import("react").RefAttributes<HTMLHeadingElement>>;
34
+ export declare const DrawerBody: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
35
+ export declare const DrawerFooter: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & import("react").RefAttributes<HTMLDivElement>>;
36
+ export declare const DrawerClose: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").ButtonHTMLAttributes<HTMLButtonElement>, HTMLButtonElement>, "ref"> & import("react").RefAttributes<HTMLButtonElement>>;
37
+ export {};
38
+ //# sourceMappingURL=Drawer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Drawer.d.ts","sourceRoot":"","sources":["../../src/components/Drawer.tsx"],"names":[],"mappings":"AAEA,OAAO,EAOL,KAAK,oBAAoB,EACzB,KAAK,WAAW,EAEhB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAIf,KAAK,eAAe,GAAG,OAAO,GAAG,KAAK,CAAC;AACvC,KAAK,UAAU,GAAG,QAAQ,GAAG,aAAa,CAAC;AAwB3C,MAAM,MAAM,WAAW,GAAG,IAAI,CAAC,oBAAoB,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG;IAC1F,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI,CAAC;IACvC,SAAS,CAAC,EAAE,eAAe,CAAC;IAC5B,iEAAiE;IACjE,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,mFAAmF;IACnF,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,+DAA+D;IAC/D,IAAI,CAAC,EAAE,UAAU,CAAC;IAClB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,MAAM;WAbV,OAAO;kBACA,OAAO;mBACN,CAAC,IAAI,EAAE,OAAO,KAAK,IAAI;gBAC1B,eAAe;IAC3B,iEAAiE;YACzD,SAAS;IACjB,mFAAmF;aAC1E,SAAS;IAClB,+DAA+D;WACxD,UAAU;eACN,SAAS;qDA0ErB,CAAC;AAIF,eAAO,MAAM,YAAY,2MAKxB,CAAC;AAKF,eAAO,MAAM,WAAW;SAFwC,WAAW;sDAc1E,CAAC;AAGF,eAAO,MAAM,UAAU,2MAoBtB,CAAC;AAGF,eAAO,MAAM,YAAY,2MAYxB,CAAC;AAGF,eAAO,MAAM,WAAW,0NAqBvB,CAAC"}
@@ -0,0 +1,174 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import {
4
+ createContext,
5
+ forwardRef,
6
+ useContext,
7
+ useLayoutEffect,
8
+ useRef
9
+ } from "react";
10
+ import { drawer as drawerRecipe } from "@becket-ui/tokens/recipes";
11
+ import { assignRef, useNativeDialog } from "../helpers/nativeDialog";
12
+ const DrawerContext = createContext(null);
13
+ function useDrawer() {
14
+ const ctx = useContext(DrawerContext);
15
+ if (!ctx) {
16
+ throw new Error("Drawer parts must be used within Drawer");
17
+ }
18
+ return ctx;
19
+ }
20
+ function mergeClassName(...classes) {
21
+ return classes.filter(Boolean).join(" ");
22
+ }
23
+ const Drawer = forwardRef(
24
+ ({
25
+ open,
26
+ defaultOpen = false,
27
+ onOpenChange,
28
+ placement = "end",
29
+ title,
30
+ footer,
31
+ role = "dialog",
32
+ className,
33
+ children,
34
+ onClick,
35
+ ...props
36
+ }, ref) => {
37
+ const innerRef = useRef(null);
38
+ const { setOpen, titleId, descriptionId, labelledBy, describedBy, registerTitle, registerDescription } = useNativeDialog(innerRef, {
39
+ open,
40
+ defaultOpen,
41
+ onOpenChange,
42
+ titlePresent: title != null,
43
+ descriptionPresent: children != null && children !== false
44
+ });
45
+ const styles = drawerRecipe({ placement });
46
+ const composeChrome = title != null || footer != null;
47
+ const lightDismiss = role !== "alertdialog";
48
+ return /* @__PURE__ */ jsx(
49
+ DrawerContext.Provider,
50
+ {
51
+ value: { setOpen, titleId, descriptionId, registerTitle, registerDescription },
52
+ children: /* @__PURE__ */ jsx(
53
+ "dialog",
54
+ {
55
+ ref: (node) => {
56
+ innerRef.current = node;
57
+ assignRef(ref, node);
58
+ },
59
+ role,
60
+ tabIndex: -1,
61
+ className: mergeClassName(styles.root, className),
62
+ ...props,
63
+ "aria-modal": "true",
64
+ "aria-labelledby": labelledBy,
65
+ "aria-describedby": describedBy,
66
+ onClick: (event) => {
67
+ onClick?.(event);
68
+ if (event.defaultPrevented || !lightDismiss) return;
69
+ if (event.target === event.currentTarget) setOpen(false);
70
+ },
71
+ onClose: () => setOpen(false),
72
+ children: composeChrome ? /* @__PURE__ */ jsxs(Fragment, { children: [
73
+ title != null && /* @__PURE__ */ jsxs(DrawerHeader, { children: [
74
+ /* @__PURE__ */ jsx(DrawerTitle, { children: title }),
75
+ /* @__PURE__ */ jsx(DrawerClose, {})
76
+ ] }),
77
+ /* @__PURE__ */ jsx(DrawerBody, { children }),
78
+ footer != null && /* @__PURE__ */ jsx(DrawerFooter, { children: footer })
79
+ ] }) : children
80
+ }
81
+ )
82
+ }
83
+ );
84
+ }
85
+ );
86
+ Drawer.displayName = "Drawer";
87
+ const DrawerHeader = forwardRef(
88
+ ({ className, ...props }, ref) => {
89
+ const styles = drawerRecipe();
90
+ return /* @__PURE__ */ jsx("div", { ref, ...props, className: mergeClassName(styles.header, className) });
91
+ }
92
+ );
93
+ DrawerHeader.displayName = "DrawerHeader";
94
+ const DrawerTitle = forwardRef(
95
+ ({ className, as: Comp = "h2", ...props }, ref) => {
96
+ const { titleId, registerTitle } = useDrawer();
97
+ const styles = drawerRecipe();
98
+ useLayoutEffect(() => {
99
+ registerTitle(true);
100
+ return () => registerTitle(false);
101
+ }, [registerTitle]);
102
+ return /* @__PURE__ */ jsx(Comp, { ref, id: titleId, ...props, className: mergeClassName(styles.title, className) });
103
+ }
104
+ );
105
+ DrawerTitle.displayName = "DrawerTitle";
106
+ const DrawerBody = forwardRef(
107
+ ({ className, children, ...props }, ref) => {
108
+ const { descriptionId, registerDescription } = useDrawer();
109
+ const styles = drawerRecipe();
110
+ const hasContent = children != null && children !== false;
111
+ useLayoutEffect(() => {
112
+ registerDescription(hasContent);
113
+ return () => registerDescription(false);
114
+ }, [hasContent, registerDescription]);
115
+ return /* @__PURE__ */ jsx(
116
+ "div",
117
+ {
118
+ ref,
119
+ id: descriptionId,
120
+ ...props,
121
+ className: mergeClassName(styles.body, className),
122
+ children
123
+ }
124
+ );
125
+ }
126
+ );
127
+ DrawerBody.displayName = "DrawerBody";
128
+ const DrawerFooter = forwardRef(
129
+ ({ className, ...props }, ref) => {
130
+ const styles = drawerRecipe();
131
+ return /* @__PURE__ */ jsx(
132
+ "div",
133
+ {
134
+ ref,
135
+ "data-becket-dialog-footer": "",
136
+ ...props,
137
+ className: mergeClassName(styles.footer, className)
138
+ }
139
+ );
140
+ }
141
+ );
142
+ DrawerFooter.displayName = "DrawerFooter";
143
+ const DrawerClose = forwardRef(
144
+ ({ className, children = "\xD7", type = "button", onClick, ...props }, ref) => {
145
+ const { setOpen } = useDrawer();
146
+ const styles = drawerRecipe();
147
+ return /* @__PURE__ */ jsx(
148
+ "button",
149
+ {
150
+ ref,
151
+ type,
152
+ "data-becket-dialog-close": "",
153
+ ...props,
154
+ "aria-label": props["aria-label"] ?? "Close",
155
+ className: mergeClassName(styles.close, className),
156
+ onClick: (event) => {
157
+ onClick?.(event);
158
+ if (!event.defaultPrevented) setOpen(false);
159
+ },
160
+ children
161
+ }
162
+ );
163
+ }
164
+ );
165
+ DrawerClose.displayName = "DrawerClose";
166
+ export {
167
+ Drawer,
168
+ DrawerBody,
169
+ DrawerClose,
170
+ DrawerFooter,
171
+ DrawerHeader,
172
+ DrawerTitle
173
+ };
174
+ //# sourceMappingURL=Drawer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/Drawer.tsx"],"sourcesContent":["'use client';\n\nimport {\n createContext,\n forwardRef,\n useContext,\n useLayoutEffect,\n useRef,\n type ComponentPropsWithoutRef,\n type DialogHTMLAttributes,\n type ElementType,\n type MouseEvent,\n type ReactNode,\n} from 'react';\nimport { drawer as drawerRecipe } from '@becket-ui/tokens/recipes';\nimport { assignRef, useNativeDialog } from '../helpers/nativeDialog';\n\ntype DrawerPlacement = 'start' | 'end';\ntype DrawerRole = 'dialog' | 'alertdialog';\n\ntype DrawerContextValue = {\n setOpen: (open: boolean) => void;\n titleId: string;\n descriptionId: string;\n registerTitle: (present: boolean) => void;\n registerDescription: (present: boolean) => void;\n};\n\nconst DrawerContext = createContext<DrawerContextValue | null>(null);\n\nfunction useDrawer() {\n const ctx = useContext(DrawerContext);\n if (!ctx) {\n throw new Error('Drawer parts must be used within Drawer');\n }\n return ctx;\n}\n\nfunction mergeClassName(...classes: (string | undefined)[]): string {\n return classes.filter(Boolean).join(' ');\n}\n\nexport type DrawerProps = Omit<DialogHTMLAttributes<HTMLDialogElement>, 'open' | 'title'> & {\n open?: boolean;\n defaultOpen?: boolean;\n onOpenChange?: (open: boolean) => void;\n placement?: DrawerPlacement;\n /** Composed heading. When set, Drawer renders header + close. */\n title?: ReactNode;\n /** Composed actions (pass `Button` nodes). When set, Drawer renders the footer. */\n footer?: ReactNode;\n /** Same as Dialog: `alertdialog` disables backdrop dismiss. */\n role?: DrawerRole;\n children?: ReactNode;\n};\n\nexport const Drawer = forwardRef<HTMLDialogElement, DrawerProps>(\n (\n {\n open,\n defaultOpen = false,\n onOpenChange,\n placement = 'end',\n title,\n footer,\n role = 'dialog',\n className,\n children,\n onClick,\n ...props\n },\n ref,\n ) => {\n const innerRef = useRef<HTMLDialogElement>(null);\n const { setOpen, titleId, descriptionId, labelledBy, describedBy, registerTitle, registerDescription } =\n useNativeDialog(innerRef, {\n open,\n defaultOpen,\n onOpenChange,\n titlePresent: title != null,\n descriptionPresent: children != null && children !== false,\n });\n const styles = drawerRecipe({ placement });\n const composeChrome = title != null || footer != null;\n const lightDismiss = role !== 'alertdialog';\n\n return (\n <DrawerContext.Provider\n value={{ setOpen, titleId, descriptionId, registerTitle, registerDescription }}\n >\n <dialog\n ref={(node) => {\n innerRef.current = node;\n assignRef(ref, node);\n }}\n role={role}\n tabIndex={-1}\n className={mergeClassName(styles.root, className)}\n {...props}\n aria-modal=\"true\"\n aria-labelledby={labelledBy}\n aria-describedby={describedBy}\n onClick={(event: MouseEvent<HTMLDialogElement>) => {\n onClick?.(event);\n if (event.defaultPrevented || !lightDismiss) return;\n if (event.target === event.currentTarget) setOpen(false);\n }}\n onClose={() => setOpen(false)}\n >\n {composeChrome ? (\n <>\n {title != null && (\n <DrawerHeader>\n <DrawerTitle>{title}</DrawerTitle>\n <DrawerClose />\n </DrawerHeader>\n )}\n <DrawerBody>{children}</DrawerBody>\n {footer != null && <DrawerFooter>{footer}</DrawerFooter>}\n </>\n ) : (\n children\n )}\n </dialog>\n </DrawerContext.Provider>\n );\n },\n);\n\nDrawer.displayName = 'Drawer';\n\nexport const DrawerHeader = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n ({ className, ...props }, ref) => {\n const styles = drawerRecipe();\n return <div ref={ref} {...props} className={mergeClassName(styles.header, className)} />;\n },\n);\nDrawerHeader.displayName = 'DrawerHeader';\n\ntype DrawerTitleProps = ComponentPropsWithoutRef<'h2'> & { as?: ElementType };\n\nexport const DrawerTitle = forwardRef<HTMLHeadingElement, DrawerTitleProps>(\n ({ className, as: Comp = 'h2', ...props }, ref) => {\n const { titleId, registerTitle } = useDrawer();\n const styles = drawerRecipe();\n useLayoutEffect(() => {\n registerTitle(true);\n return () => registerTitle(false);\n }, [registerTitle]);\n return (\n <Comp ref={ref} id={titleId} {...props} className={mergeClassName(styles.title, className)} />\n );\n },\n);\nDrawerTitle.displayName = 'DrawerTitle';\n\nexport const DrawerBody = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n ({ className, children, ...props }, ref) => {\n const { descriptionId, registerDescription } = useDrawer();\n const styles = drawerRecipe();\n const hasContent = children != null && children !== false;\n useLayoutEffect(() => {\n registerDescription(hasContent);\n return () => registerDescription(false);\n }, [hasContent, registerDescription]);\n return (\n <div\n ref={ref}\n id={descriptionId}\n {...props}\n className={mergeClassName(styles.body, className)}\n >\n {children}\n </div>\n );\n },\n);\nDrawerBody.displayName = 'DrawerBody';\n\nexport const DrawerFooter = forwardRef<HTMLDivElement, ComponentPropsWithoutRef<'div'>>(\n ({ className, ...props }, ref) => {\n const styles = drawerRecipe();\n return (\n <div\n ref={ref}\n data-becket-dialog-footer=\"\"\n {...props}\n className={mergeClassName(styles.footer, className)}\n />\n );\n },\n);\nDrawerFooter.displayName = 'DrawerFooter';\n\nexport const DrawerClose = forwardRef<HTMLButtonElement, ComponentPropsWithoutRef<'button'>>(\n ({ className, children = '×', type = 'button', onClick, ...props }, ref) => {\n const { setOpen } = useDrawer();\n const styles = drawerRecipe();\n return (\n <button\n ref={ref}\n type={type}\n data-becket-dialog-close=\"\"\n {...props}\n aria-label={props['aria-label'] ?? 'Close'}\n className={mergeClassName(styles.close, className)}\n onClick={(event) => {\n onClick?.(event);\n if (!event.defaultPrevented) setOpen(false);\n }}\n >\n {children}\n </button>\n );\n },\n);\nDrawerClose.displayName = 'DrawerClose';\n"],"mappings":"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","names":[]}
@@ -0,0 +1,50 @@
1
+ import { jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
4
+ import { assertNoAxeViolations } from "@becket-ui/a11y";
5
+ import { Button } from "./Button";
6
+ import { Drawer } from "./Drawer";
7
+ function FiltersDrawer({
8
+ onOpenChange = () => {
9
+ },
10
+ role = "dialog"
11
+ }) {
12
+ return /* @__PURE__ */ jsx(
13
+ Drawer,
14
+ {
15
+ open: true,
16
+ role,
17
+ onOpenChange,
18
+ title: "Filters",
19
+ footer: /* @__PURE__ */ jsx(Button, { visual: "primary", onClick: () => onOpenChange(false), children: "Done" }),
20
+ children: "Same a11y as Dialog."
21
+ }
22
+ );
23
+ }
24
+ describe("Drawer", () => {
25
+ it("names the dialog from the title and describes it from the body", () => {
26
+ render(/* @__PURE__ */ jsx(FiltersDrawer, {}));
27
+ const dialog = screen.getByRole("dialog", { name: "Filters" });
28
+ const labelledBy = dialog.getAttribute("aria-labelledby");
29
+ const describedBy = dialog.getAttribute("aria-describedby");
30
+ expect(document.getElementById(labelledBy)?.textContent).toBe("Filters");
31
+ expect(document.getElementById(describedBy)?.textContent).toContain("Same a11y as Dialog");
32
+ });
33
+ it("moves initial focus to the footer action, not Close", async () => {
34
+ render(/* @__PURE__ */ jsx(FiltersDrawer, {}));
35
+ await waitFor(() => {
36
+ expect(document.activeElement).toBe(screen.getByRole("button", { name: "Done" }));
37
+ });
38
+ });
39
+ it("does not light-dismiss an alertdialog", () => {
40
+ const onOpenChange = vi.fn();
41
+ render(/* @__PURE__ */ jsx(FiltersDrawer, { role: "alertdialog", onOpenChange }));
42
+ fireEvent.click(screen.getByRole("alertdialog", { name: "Filters" }));
43
+ expect(onOpenChange).not.toHaveBeenCalled();
44
+ });
45
+ it("has no axe violations when open", async () => {
46
+ const { container } = render(/* @__PURE__ */ jsx(FiltersDrawer, {}));
47
+ await assertNoAxeViolations(container);
48
+ });
49
+ });
50
+ //# sourceMappingURL=Drawer.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/Drawer.test.tsx"],"sourcesContent":["import { describe, expect, it, vi } from 'vitest';\nimport { fireEvent, render, screen, waitFor } from '@testing-library/react';\nimport { assertNoAxeViolations } from '@becket-ui/a11y';\n\nimport { Button } from './Button';\nimport { Drawer } from './Drawer';\n\nfunction FiltersDrawer({\n onOpenChange = () => {},\n role = 'dialog' as 'dialog' | 'alertdialog',\n}: {\n onOpenChange?: (open: boolean) => void;\n role?: 'dialog' | 'alertdialog';\n}) {\n return (\n <Drawer\n open\n role={role}\n onOpenChange={onOpenChange}\n title=\"Filters\"\n footer={\n <Button visual=\"primary\" onClick={() => onOpenChange(false)}>\n Done\n </Button>\n }\n >\n Same a11y as Dialog.\n </Drawer>\n );\n}\n\ndescribe('Drawer', () => {\n it('names the dialog from the title and describes it from the body', () => {\n render(<FiltersDrawer />);\n const dialog = screen.getByRole('dialog', { name: 'Filters' });\n const labelledBy = dialog.getAttribute('aria-labelledby');\n const describedBy = dialog.getAttribute('aria-describedby');\n expect(document.getElementById(labelledBy as string)?.textContent).toBe('Filters');\n expect(document.getElementById(describedBy as string)?.textContent).toContain('Same a11y as Dialog');\n });\n\n it('moves initial focus to the footer action, not Close', async () => {\n render(<FiltersDrawer />);\n await waitFor(() => {\n expect(document.activeElement).toBe(screen.getByRole('button', { name: 'Done' }));\n });\n });\n\n it('does not light-dismiss an alertdialog', () => {\n const onOpenChange = vi.fn();\n render(<FiltersDrawer role=\"alertdialog\" onOpenChange={onOpenChange} />);\n fireEvent.click(screen.getByRole('alertdialog', { name: 'Filters' }));\n expect(onOpenChange).not.toHaveBeenCalled();\n });\n\n it('has no axe violations when open', async () => {\n const { container } = render(<FiltersDrawer />);\n await assertNoAxeViolations(container);\n });\n});\n"],"mappings":"AAqBQ;AArBR,SAAS,UAAU,QAAQ,IAAI,UAAU;AACzC,SAAS,WAAW,QAAQ,QAAQ,eAAe;AACnD,SAAS,6BAA6B;AAEtC,SAAS,cAAc;AACvB,SAAS,cAAc;AAEvB,SAAS,cAAc;AAAA,EACrB,eAAe,MAAM;AAAA,EAAC;AAAA,EACtB,OAAO;AACT,GAGG;AACD,SACE;AAAA,IAAC;AAAA;AAAA,MACC,MAAI;AAAA,MACJ;AAAA,MACA;AAAA,MACA,OAAM;AAAA,MACN,QACE,oBAAC,UAAO,QAAO,WAAU,SAAS,MAAM,aAAa,KAAK,GAAG,kBAE7D;AAAA,MAEH;AAAA;AAAA,EAED;AAEJ;AAEA,SAAS,UAAU,MAAM;AACvB,KAAG,kEAAkE,MAAM;AACzE,WAAO,oBAAC,iBAAc,CAAE;AACxB,UAAM,SAAS,OAAO,UAAU,UAAU,EAAE,MAAM,UAAU,CAAC;AAC7D,UAAM,aAAa,OAAO,aAAa,iBAAiB;AACxD,UAAM,cAAc,OAAO,aAAa,kBAAkB;AAC1D,WAAO,SAAS,eAAe,UAAoB,GAAG,WAAW,EAAE,KAAK,SAAS;AACjF,WAAO,SAAS,eAAe,WAAqB,GAAG,WAAW,EAAE,UAAU,qBAAqB;AAAA,EACrG,CAAC;AAED,KAAG,uDAAuD,YAAY;AACpE,WAAO,oBAAC,iBAAc,CAAE;AACxB,UAAM,QAAQ,MAAM;AAClB,aAAO,SAAS,aAAa,EAAE,KAAK,OAAO,UAAU,UAAU,EAAE,MAAM,OAAO,CAAC,CAAC;AAAA,IAClF,CAAC;AAAA,EACH,CAAC;AAED,KAAG,yCAAyC,MAAM;AAChD,UAAM,eAAe,GAAG,GAAG;AAC3B,WAAO,oBAAC,iBAAc,MAAK,eAAc,cAA4B,CAAE;AACvE,cAAU,MAAM,OAAO,UAAU,eAAe,EAAE,MAAM,UAAU,CAAC,CAAC;AACpE,WAAO,YAAY,EAAE,IAAI,iBAAiB;AAAA,EAC5C,CAAC;AAED,KAAG,mCAAmC,YAAY;AAChD,UAAM,EAAE,UAAU,IAAI,OAAO,oBAAC,iBAAc,CAAE;AAC9C,UAAM,sBAAsB,SAAS;AAAA,EACvC,CAAC;AACH,CAAC;","names":[]}
@@ -0,0 +1,37 @@
1
+ import { type ComponentPropsWithoutRef, type InputHTMLAttributes, type LabelHTMLAttributes, type ReactNode } from 'react';
2
+ export type FieldSize = 'sm' | 'md' | 'lg';
3
+ export type FieldContextValue = {
4
+ id: string;
5
+ invalid: boolean;
6
+ size: FieldSize;
7
+ helperId: string;
8
+ errorId: string;
9
+ describedBy?: string;
10
+ };
11
+ export declare function useField(): FieldContextValue;
12
+ export declare function useOptionalField(): FieldContextValue | null;
13
+ export type FieldProps = ComponentPropsWithoutRef<'div'> & {
14
+ size?: FieldSize;
15
+ invalid?: boolean;
16
+ fullWidth?: boolean;
17
+ children?: ReactNode;
18
+ };
19
+ export declare const Field: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLDivElement>, HTMLDivElement>, "ref"> & {
20
+ size?: FieldSize;
21
+ invalid?: boolean;
22
+ fullWidth?: boolean;
23
+ children?: ReactNode;
24
+ } & import("react").RefAttributes<HTMLDivElement>>;
25
+ type FieldLabelProps = LabelHTMLAttributes<HTMLLabelElement>;
26
+ export declare const FieldLabel: import("react").ForwardRefExoticComponent<FieldLabelProps & import("react").RefAttributes<HTMLLabelElement>>;
27
+ export declare const FieldHelper: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & import("react").RefAttributes<HTMLSpanElement>>;
28
+ export declare const FieldError: import("react").ForwardRefExoticComponent<Omit<import("react").DetailedHTMLProps<import("react").HTMLAttributes<HTMLSpanElement>, HTMLSpanElement>, "ref"> & import("react").RefAttributes<HTMLSpanElement>>;
29
+ type FieldControlProps = Omit<InputHTMLAttributes<HTMLInputElement>, 'size'>;
30
+ /** Native text input wired to the parent Field (id, aria, size, invalid). */
31
+ export declare const FieldControl: import("react").ForwardRefExoticComponent<FieldControlProps & import("react").RefAttributes<HTMLInputElement>>;
32
+ type FieldSelectProps = Omit<ComponentPropsWithoutRef<'select'>, 'size'>;
33
+ export declare const FieldSelect: import("react").ForwardRefExoticComponent<FieldSelectProps & import("react").RefAttributes<HTMLSelectElement>>;
34
+ type FieldTextareaProps = Omit<ComponentPropsWithoutRef<'textarea'>, 'size'>;
35
+ export declare const FieldTextarea: import("react").ForwardRefExoticComponent<FieldTextareaProps & import("react").RefAttributes<HTMLTextAreaElement>>;
36
+ export {};
37
+ //# sourceMappingURL=Field.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Field.d.ts","sourceRoot":"","sources":["../../src/components/Field.tsx"],"names":[],"mappings":"AAEA,OAAO,EAQL,KAAK,wBAAwB,EAC7B,KAAK,mBAAmB,EACxB,KAAK,mBAAmB,EACxB,KAAK,SAAS,EACf,MAAM,OAAO,CAAC;AAGf,MAAM,MAAM,SAAS,GAAG,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;AAE3C,MAAM,MAAM,iBAAiB,GAAG;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,OAAO,EAAE,OAAO,CAAC;IACjB,IAAI,EAAE,SAAS,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAeF,wBAAgB,QAAQ,IAAI,iBAAiB,CAM5C;AAMD,wBAAgB,gBAAgB,IAAI,iBAAiB,GAAG,IAAI,CAG3D;AAcD,MAAM,MAAM,UAAU,GAAG,wBAAwB,CAAC,KAAK,CAAC,GAAG;IACzD,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,QAAQ,CAAC,EAAE,SAAS,CAAC;CACtB,CAAC;AAEF,eAAO,MAAM,KAAK;WANT,SAAS;cACN,OAAO;gBACL,OAAO;eACR,SAAS;kDAsCrB,CAAC;AAIF,KAAK,eAAe,GAAG,mBAAmB,CAAC,gBAAgB,CAAC,CAAC;AAE7D,eAAO,MAAM,UAAU,8GAQtB,CAAC;AAMF,eAAO,MAAM,WAAW,8MAiBvB,CAAC;AAIF,eAAO,MAAM,UAAU,8MAkBtB,CAAC;AAIF,KAAK,iBAAiB,GAAG,IAAI,CAAC,mBAAmB,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC,CAAC;AAE7E,6EAA6E;AAC7E,eAAO,MAAM,YAAY,gHAexB,CAAC;AAIF,KAAK,gBAAgB,GAAG,IAAI,CAAC,wBAAwB,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC,CAAC;AAEzE,eAAO,MAAM,WAAW,gHAevB,CAAC;AAIF,KAAK,kBAAkB,GAAG,IAAI,CAAC,wBAAwB,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC,CAAC;AAE7E,eAAO,MAAM,aAAa,oHAezB,CAAC"}