@coopdigital/react 1.5.1 → 1.7.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.
@@ -0,0 +1,94 @@
1
+ import React, { type DialogHTMLAttributes, type HTMLAttributes, type JSX, type ReactEventHandler, type ReactNode, type Ref } from "react";
2
+ export interface DialogProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "closedby"> {
3
+ /** Content inside the Dialog.
4
+ *
5
+ * This should be a `<Dialog.Header>` and `<Dialog.Body>`, although arbitrary JSX children are also accepted. */
6
+ children: string | ReactNode;
7
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
8
+ className?: string;
9
+ /** **(Optional)** Specify how the dialog can be closed. Set this to `none` to disable escape key and click-outside behaviour.
10
+ *
11
+ * Default: `any` */
12
+ closedBy?: "any" | "none";
13
+ /** **(Optional)** Specify the Dialog id. Will be auto-generated if not set. */
14
+ id?: string;
15
+ /** Specify an accessible label for the dialog that will be read out by screen readers. */
16
+ label: string;
17
+ /** **(Optional)** Specify the Dialog mode.
18
+ *
19
+ * Default: `dialog`
20
+ */
21
+ mode?: "sheet" | "dialog";
22
+ /** **(Optional)** Specific a callback to run when the Dialog closes. */
23
+ onClose?: ReactEventHandler<HTMLDialogElement>;
24
+ /** **(Optional)** Specific a callback to run when the Dialog opens. */
25
+ onOpen?: ReactEventHandler<HTMLDialogElement>;
26
+ /** **(Optional)** Specific a callback to run when the Dialog opens or closes. */
27
+ onToggle?: ReactEventHandler<HTMLDialogElement>;
28
+ /** Specify the Dialog open state. This should reflect the value of your `useState`.
29
+ *
30
+ * Default: `false`
31
+ */
32
+ open: boolean;
33
+ /** **(Optional)** Specify the Dialog position. This only applies when the `mode` is set to `sheet`.
34
+ *
35
+ * Default: `bottom`
36
+ */
37
+ position?: "right" | "bottom" | "left";
38
+ /** **(Optional)** Specify a custom React ref for this component. */
39
+ ref?: Ref<HTMLDialogElement>;
40
+ /** **(Optional)** Specify an ARIA role for the dialog. Use `alertdialog` if the message includes urgent information that requires an explicit user action.
41
+ *
42
+ * Default: `dialog`
43
+ */
44
+ role?: "dialog" | "alertdialog";
45
+ }
46
+ interface DialogHeaderProps extends HTMLAttributes<HTMLDivElement> {
47
+ /** **(Optional)** Content inside the Dialog Header. */
48
+ children?: React.ReactNode;
49
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
50
+ className?: string;
51
+ /** **(Optional)** Specify whether to render the close button.
52
+ *
53
+ * Default: `true`
54
+ */
55
+ close?: boolean;
56
+ }
57
+ interface DialogBodyProps extends HTMLAttributes<HTMLDivElement> {
58
+ /** **(Optional)** Content inside the Dialog Body. */
59
+ children?: React.ReactNode;
60
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
61
+ className?: string;
62
+ }
63
+ declare const Dialog: {
64
+ ({ children, className, closedBy, label, mode, onClose, onOpen, onToggle, open, position, ref, role, ...props }: DialogProps): JSX.Element;
65
+ slot: {
66
+ name: string;
67
+ };
68
+ Header: {
69
+ ({ children, className, close, ...props }: DialogHeaderProps): JSX.Element;
70
+ slot: {
71
+ name: string;
72
+ };
73
+ };
74
+ Body: {
75
+ ({ children, className, ...props }: DialogBodyProps): JSX.Element;
76
+ slot: {
77
+ name: string;
78
+ };
79
+ };
80
+ };
81
+ declare const DialogHeader: {
82
+ ({ children, className, close, ...props }: DialogHeaderProps): JSX.Element;
83
+ slot: {
84
+ name: string;
85
+ };
86
+ };
87
+ declare const DialogBody: {
88
+ ({ children, className, ...props }: DialogBodyProps): JSX.Element;
89
+ slot: {
90
+ name: string;
91
+ };
92
+ };
93
+ export default Dialog;
94
+ export { Dialog, DialogBody, DialogHeader };
@@ -0,0 +1,109 @@
1
+ "use client";
2
+ import { jsx, jsxs } from 'react/jsx-runtime';
3
+ import clsx from 'clsx';
4
+ import mergeRefs from 'merge-refs';
5
+ import { createContext, useRef, useEffect, useContext } from 'react';
6
+ import { hasUserBg } from '../../utils/index.js';
7
+ import { CrossIcon } from '../icons/CrossIcon.js';
8
+
9
+ const DialogContext = createContext({ ref: null });
10
+ const useDialog = () => {
11
+ const ctx = useContext(DialogContext);
12
+ if (!ctx)
13
+ throw new Error("Dialog components must be used inside <Dialog>");
14
+ return ctx;
15
+ };
16
+ const Dialog = ({ children, className, closedBy = "any", label, mode = "dialog", onClose, onOpen, onToggle, open = false, position, ref, role, ...props }) => {
17
+ const dialogRef = useRef(null);
18
+ useEffect(() => {
19
+ const dialog = dialogRef.current;
20
+ if (!dialog)
21
+ return;
22
+ if (open) {
23
+ dialog.showModal();
24
+ }
25
+ else if (!open) {
26
+ dialog.close();
27
+ }
28
+ }, [open]);
29
+ useEffect(() => {
30
+ const dialog = dialogRef === null || dialogRef === void 0 ? void 0 : dialogRef.current;
31
+ if (!dialog)
32
+ return;
33
+ /**
34
+ * https://bugs.webkit.org/show_bug.cgi?id=190870
35
+ *
36
+ * There is a bug in Safari that means scrollable areas are not
37
+ * automatically focusable. To work around this, we would need to
38
+ * add tabindex=0 to the dialog body container, even if there isn't
39
+ * enough content to scroll, which would negatively impact
40
+ * accessibility for other browsers. We decided the least-worst
41
+ * option was to have the dialog itself scrollable as this works
42
+ * everywhere we tested it. If Safari ever fixes the bug, we'll
43
+ * update the dialog to scroll the body only.
44
+ */
45
+ const setScrolledState = () => {
46
+ const y = dialog.scrollTop;
47
+ dialog.setAttribute("data-scrolled", y && y > 0 ? "true" : "false");
48
+ };
49
+ dialog.addEventListener("scroll", setScrolledState, {
50
+ passive: true,
51
+ });
52
+ return () => {
53
+ dialog.removeEventListener("scroll", setScrolledState);
54
+ };
55
+ }, [dialogRef]);
56
+ const handleToggle = (e) => {
57
+ if (e.newState === "open") {
58
+ onOpen === null || onOpen === void 0 ? void 0 : onOpen(e);
59
+ }
60
+ };
61
+ const handleBackdropClick = (event) => {
62
+ const dialog = dialogRef === null || dialogRef === void 0 ? void 0 : dialogRef.current;
63
+ if (dialog && event.target === dialog && closedBy !== "none") {
64
+ dialog.close();
65
+ }
66
+ };
67
+ const handleCancel = (event) => {
68
+ if (closedBy === "none") {
69
+ event.preventDefault();
70
+ }
71
+ };
72
+ const componentProps = {
73
+ "aria-label": label,
74
+ "aria-modal": true,
75
+ className: clsx("coop-dialog", mode === "sheet" && "coop-sheet", !hasUserBg(className) && "bg-tint-grey", className),
76
+ closedby: closedBy,
77
+ "data-mode": mode,
78
+ "data-open": open,
79
+ "data-position": position,
80
+ role,
81
+ ...props,
82
+ };
83
+ return (jsx(DialogContext.Provider, { value: { ref: dialogRef }, children: jsx("dialog", { ...componentProps, onCancel: handleCancel, onClick: handleBackdropClick, onClose: onClose, onToggle: (e) => {
84
+ handleToggle(e);
85
+ onToggle === null || onToggle === void 0 ? void 0 : onToggle(e);
86
+ }, ref: mergeRefs(dialogRef, ref), children: children }) }));
87
+ };
88
+ const DialogHeader = ({ children, className, close = true, ...props }) => {
89
+ const componentProps = {
90
+ className: clsx("coop-dialog--header", !hasUserBg(className) && "bg-tint-grey", className),
91
+ ...props,
92
+ };
93
+ const dialog = useDialog();
94
+ return (jsxs("div", { ...componentProps, children: [children, close && (jsxs("button", { "aria-label": "Close", className: "coop-dialog--close", onClick: () => { var _a, _b; return (_b = (_a = dialog.ref) === null || _a === void 0 ? void 0 : _a.current) === null || _b === void 0 ? void 0 : _b.close(); }, children: ["Close ", jsx(CrossIcon, { role: "presentation" })] }))] }));
95
+ };
96
+ const DialogBody = ({ children, className, ...props }) => {
97
+ const componentProps = {
98
+ className: clsx("coop-dialog--body", className),
99
+ ...props,
100
+ };
101
+ return jsx("div", { ...componentProps, children: children });
102
+ };
103
+ Dialog.slot = { name: "Dialog" };
104
+ DialogHeader.slot = { name: "DialogHeader" };
105
+ DialogBody.slot = { name: "DialogBody" };
106
+ Dialog.Header = DialogHeader;
107
+ Dialog.Body = DialogBody;
108
+
109
+ export { Dialog, DialogBody, DialogHeader, Dialog as default };
@@ -0,0 +1,4 @@
1
+ import Dialog from "./Dialog";
2
+ export default Dialog;
3
+ export { Dialog };
4
+ export * from "./Dialog";
@@ -0,0 +1,6 @@
1
+ import { type SVGProps } from "react";
2
+ interface IconProps extends SVGProps<SVGSVGElement> {
3
+ alt?: string;
4
+ }
5
+ export declare const WarningCircleFilledIcon: ({ alt, className, fill, id, ref, ...props }: IconProps) => import("react").JSX.Element;
6
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import clsx from 'clsx';
3
+ import { useId } from '../../hooks/useId.js';
4
+
5
+ const WarningCircleFilledIcon = ({ alt, className, fill, id, ref, ...props }) => {
6
+ const uid = useId(id);
7
+ const componentProps = {
8
+ "aria-labelledby": alt ? uid + "__title" : undefined,
9
+ className: clsx("coop-icon", className),
10
+ "data-icon": "warning-circle-filled",
11
+ fill: fill !== null && fill !== void 0 ? fill : "currentColor",
12
+ id: uid,
13
+ role: alt ? "img" : undefined,
14
+ viewBox: "0 0 32 32",
15
+ ...props,
16
+ };
17
+ return (jsxs("svg", { ...componentProps, ref: ref, children: [alt ? jsx("title", { id: uid + "__title", children: alt }) : null, jsx("path", { clipRule: "evenodd", d: "M16 2c7.732 0 14 6.268 14 14s-6.268 14-14 14S2 23.732 2 16 8.268 2 16 2m0 18a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3m0-11.5a1.5 1.5 0 0 0-1.5 1.5v6a1.5 1.5 0 0 0 3 0v-6A1.5 1.5 0 0 0 16 8.5", fillRule: "evenodd" })] }));
18
+ };
19
+
20
+ export { WarningCircleFilledIcon };
@@ -0,0 +1,6 @@
1
+ import { type SVGProps } from "react";
2
+ interface IconProps extends SVGProps<SVGSVGElement> {
3
+ alt?: string;
4
+ }
5
+ export declare const WarningCircleIcon: ({ alt, className, fill, id, ref, ...props }: IconProps) => import("react").JSX.Element;
6
+ export {};
@@ -0,0 +1,20 @@
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import clsx from 'clsx';
3
+ import { useId } from '../../hooks/useId.js';
4
+
5
+ const WarningCircleIcon = ({ alt, className, fill, id, ref, ...props }) => {
6
+ const uid = useId(id);
7
+ const componentProps = {
8
+ "aria-labelledby": alt ? uid + "__title" : undefined,
9
+ className: clsx("coop-icon", className),
10
+ "data-icon": "warning-circle",
11
+ fill: fill !== null && fill !== void 0 ? fill : "currentColor",
12
+ id: uid,
13
+ role: alt ? "img" : undefined,
14
+ viewBox: "0 0 32 32",
15
+ ...props,
16
+ };
17
+ return (jsxs("svg", { ...componentProps, ref: ref, children: [alt ? jsx("title", { id: uid + "__title", children: alt }) : null, jsx("path", { d: "M16 17.5a1.5 1.5 0 0 1-1.5-1.5v-6a1.5 1.5 0 0 1 3 0v6a1.5 1.5 0 0 1-1.5 1.5M16 23a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3" }), jsx("path", { clipRule: "evenodd", d: "M16 30C8.268 30 2 23.732 2 16S8.268 2 16 2s14 6.268 14 14-6.268 14-14 14m0-2c6.627 0 12-5.373 12-12S22.627 4 16 4 4 9.373 4 16s5.373 12 12 12", fillRule: "evenodd" })] }));
18
+ };
19
+
20
+ export { WarningCircleIcon };
@@ -101,6 +101,8 @@ export { TruckIcon } from "./TruckIcon";
101
101
  export { UserWipeIcon } from "./UserWipeIcon";
102
102
  export { VoucherFilledIcon } from "./VoucherFilledIcon";
103
103
  export { VoucherIcon } from "./VoucherIcon";
104
+ export { WarningCircleFilledIcon } from "./WarningCircleFilledIcon";
105
+ export { WarningCircleIcon } from "./WarningCircleIcon";
104
106
  export { WarningFilledIcon } from "./WarningFilledIcon";
105
107
  export { WarningIcon } from "./WarningIcon";
106
108
  export { WifiIcon } from "./WifiIcon";
@@ -101,6 +101,8 @@ export { TruckIcon } from './TruckIcon.js';
101
101
  export { UserWipeIcon } from './UserWipeIcon.js';
102
102
  export { VoucherFilledIcon } from './VoucherFilledIcon.js';
103
103
  export { VoucherIcon } from './VoucherIcon.js';
104
+ export { WarningCircleFilledIcon } from './WarningCircleFilledIcon.js';
105
+ export { WarningCircleIcon } from './WarningCircleIcon.js';
104
106
  export { WarningFilledIcon } from './WarningFilledIcon.js';
105
107
  export { WarningIcon } from './WarningIcon.js';
106
108
  export { WifiIcon } from './WifiIcon.js';
package/dist/index.d.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./components/Button";
4
4
  export * from "./components/Card";
5
5
  export * from "./components/Checkbox";
6
6
  export * from "./components/DatePicker";
7
+ export * from "./components/Dialog";
7
8
  export * from "./components/Expandable";
8
9
  export * from "./components/Field";
9
10
  export * from "./components/Fieldset";
package/dist/index.js CHANGED
@@ -4,6 +4,7 @@ export { Button } from './components/Button/Button.js';
4
4
  export { Card, CardBadge, CardBody, CardHeading, CardImage, CardLabel } from './components/Card/Card.js';
5
5
  export { Checkbox } from './components/Checkbox/Checkbox.js';
6
6
  export { DatePicker } from './components/DatePicker/DatePicker.js';
7
+ export { Dialog, DialogBody, DialogHeader } from './components/Dialog/Dialog.js';
7
8
  export { Expandable, ExpandableContent, ExpandableSummary } from './components/Expandable/Expandable.js';
8
9
  export { Field, FieldError, FieldHint, FieldLabel } from './components/Field/Field.js';
9
10
  export { Fieldset, FieldsetError, FieldsetFields, FieldsetHint, FieldsetLegend } from './components/Fieldset/Fieldset.js';
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@coopdigital/react",
3
3
  "type": "module",
4
- "version": "1.5.1",
4
+ "version": "1.7.0",
5
5
  "private": false,
6
6
  "publishConfig": {
7
7
  "access": "public"
@@ -38,6 +38,8 @@
38
38
  "test": "vitest run",
39
39
  "test:watch": "vitest",
40
40
  "test:coverage": "vitest run --coverage",
41
+ "test:behaviour": "./test-utils/run-playwright-tests.sh -g behaviour",
42
+ "test:behaviour:ui": "npm run test:behaviour -- --ui",
41
43
  "test:avt": "./test-utils/run-playwright-tests.sh -g avt",
42
44
  "test:avt:ui": "npm run test:avt -- --ui",
43
45
  "test:vrt": "ENVIRONMENT=docker ./test-utils/run-playwright-tests.sh -g vrt --workers 4 --timeout 5000 --fully-parallel",
@@ -85,11 +87,12 @@
85
87
  "storybook": "$storybook"
86
88
  },
87
89
  "dependencies": {
88
- "@coopdigital/styles": "^1.3.3",
90
+ "@coopdigital/styles": "^1.4.0",
89
91
  "@radix-ui/react-popover": "^1.1.15",
92
+ "@radix-ui/react-slot": "^1.3.0",
90
93
  "clsx": "^2.1.1",
91
94
  "merge-refs": "^2.0.0",
92
95
  "react-day-picker": "^10.0.0"
93
96
  },
94
- "gitHead": "002d135011d4864e286bc1f8e7f0e23a394e5e33"
97
+ "gitHead": "9c6acadccf25d1fe890f403e198a3bce9f3661e7"
95
98
  }
@@ -0,0 +1,256 @@
1
+ "use client"
2
+ import clsx from "clsx"
3
+ import mergeRefs from "merge-refs"
4
+ import React, {
5
+ createContext,
6
+ type DialogHTMLAttributes,
7
+ type HTMLAttributes,
8
+ type JSX,
9
+ type MouseEvent,
10
+ type ReactEventHandler,
11
+ type ReactNode,
12
+ type Ref,
13
+ type RefObject,
14
+ type ToggleEvent,
15
+ useContext,
16
+ useEffect,
17
+ useRef,
18
+ } from "react"
19
+
20
+ import { hasUserBg } from "../../utils"
21
+ import { CrossIcon } from "../icons"
22
+
23
+ const DialogContext = createContext<{
24
+ ref: RefObject<HTMLDialogElement | null> | null
25
+ }>({ ref: null })
26
+
27
+ const useDialog = () => {
28
+ const ctx = useContext(DialogContext)
29
+ if (!ctx) throw new Error("Dialog components must be used inside <Dialog>")
30
+ return ctx
31
+ }
32
+
33
+ export interface DialogProps extends Omit<DialogHTMLAttributes<HTMLDialogElement>, "closedby"> {
34
+ /** Content inside the Dialog.
35
+ *
36
+ * This should be a `<Dialog.Header>` and `<Dialog.Body>`, although arbitrary JSX children are also accepted. */
37
+ children: string | ReactNode
38
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
39
+ className?: string
40
+ /** **(Optional)** Specify how the dialog can be closed. Set this to `none` to disable escape key and click-outside behaviour.
41
+ *
42
+ * Default: `any` */
43
+ closedBy?: "any" | "none"
44
+ /** **(Optional)** Specify the Dialog id. Will be auto-generated if not set. */
45
+ id?: string
46
+ /** Specify an accessible label for the dialog that will be read out by screen readers. */
47
+ label: string
48
+ /** **(Optional)** Specify the Dialog mode.
49
+ *
50
+ * Default: `dialog`
51
+ */
52
+ mode?: "sheet" | "dialog"
53
+ /** **(Optional)** Specific a callback to run when the Dialog closes. */
54
+ onClose?: ReactEventHandler<HTMLDialogElement>
55
+ /** **(Optional)** Specific a callback to run when the Dialog opens. */
56
+ onOpen?: ReactEventHandler<HTMLDialogElement>
57
+ /** **(Optional)** Specific a callback to run when the Dialog opens or closes. */
58
+ onToggle?: ReactEventHandler<HTMLDialogElement>
59
+ /** Specify the Dialog open state. This should reflect the value of your `useState`.
60
+ *
61
+ * Default: `false`
62
+ */
63
+ open: boolean
64
+ /** **(Optional)** Specify the Dialog position. This only applies when the `mode` is set to `sheet`.
65
+ *
66
+ * Default: `bottom`
67
+ */
68
+ position?: "right" | "bottom" | "left"
69
+ /** **(Optional)** Specify a custom React ref for this component. */
70
+ ref?: Ref<HTMLDialogElement>
71
+ /** **(Optional)** Specify an ARIA role for the dialog. Use `alertdialog` if the message includes urgent information that requires an explicit user action.
72
+ *
73
+ * Default: `dialog`
74
+ */
75
+ role?: "dialog" | "alertdialog"
76
+ }
77
+
78
+ interface DialogHeaderProps extends HTMLAttributes<HTMLDivElement> {
79
+ /** **(Optional)** Content inside the Dialog Header. */
80
+ children?: React.ReactNode
81
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
82
+ className?: string
83
+ /** **(Optional)** Specify whether to render the close button.
84
+ *
85
+ * Default: `true`
86
+ */
87
+ close?: boolean
88
+ }
89
+ interface DialogBodyProps extends HTMLAttributes<HTMLDivElement> {
90
+ /** **(Optional)** Content inside the Dialog Body. */
91
+ children?: React.ReactNode
92
+ /** **(Optional)** Specify additional CSS classes to be applied to the component. */
93
+ className?: string
94
+ }
95
+
96
+ const Dialog = ({
97
+ children,
98
+ className,
99
+ closedBy = "any",
100
+ label,
101
+ mode = "dialog",
102
+ onClose,
103
+ onOpen,
104
+ onToggle,
105
+ open = false,
106
+ position,
107
+ ref,
108
+ role,
109
+ ...props
110
+ }: DialogProps): JSX.Element => {
111
+ const dialogRef = useRef<HTMLDialogElement>(null)
112
+
113
+ useEffect(() => {
114
+ const dialog = dialogRef.current
115
+ if (!dialog) return
116
+
117
+ if (open) {
118
+ dialog.showModal()
119
+ } else if (!open) {
120
+ dialog.close()
121
+ }
122
+ }, [open])
123
+
124
+ useEffect(() => {
125
+ const dialog = dialogRef?.current
126
+ if (!dialog) return
127
+
128
+ /**
129
+ * https://bugs.webkit.org/show_bug.cgi?id=190870
130
+ *
131
+ * There is a bug in Safari that means scrollable areas are not
132
+ * automatically focusable. To work around this, we would need to
133
+ * add tabindex=0 to the dialog body container, even if there isn't
134
+ * enough content to scroll, which would negatively impact
135
+ * accessibility for other browsers. We decided the least-worst
136
+ * option was to have the dialog itself scrollable as this works
137
+ * everywhere we tested it. If Safari ever fixes the bug, we'll
138
+ * update the dialog to scroll the body only.
139
+ */
140
+
141
+ const setScrolledState = () => {
142
+ const y = dialog.scrollTop
143
+ dialog.setAttribute("data-scrolled", y && y > 0 ? "true" : "false")
144
+ }
145
+
146
+ dialog.addEventListener("scroll", setScrolledState, {
147
+ passive: true,
148
+ })
149
+
150
+ return () => {
151
+ dialog.removeEventListener("scroll", setScrolledState)
152
+ }
153
+ }, [dialogRef])
154
+
155
+ const handleToggle = (e: ToggleEvent<HTMLDialogElement>) => {
156
+ if (e.newState === "open") {
157
+ onOpen?.(e)
158
+ }
159
+ }
160
+
161
+ const handleBackdropClick = (event: MouseEvent<HTMLDialogElement>) => {
162
+ const dialog = dialogRef?.current
163
+ if (dialog && event.target === dialog && closedBy !== "none") {
164
+ dialog.close()
165
+ }
166
+ }
167
+
168
+ const handleCancel = (event: MouseEvent<HTMLDialogElement>) => {
169
+ if (closedBy === "none") {
170
+ event.preventDefault()
171
+ }
172
+ }
173
+
174
+ const componentProps = {
175
+ "aria-label": label,
176
+ "aria-modal": true,
177
+ className: clsx(
178
+ "coop-dialog",
179
+ mode === "sheet" && "coop-sheet",
180
+ !hasUserBg(className) && "bg-tint-grey",
181
+ className
182
+ ),
183
+ closedby: closedBy,
184
+ "data-mode": mode,
185
+ "data-open": open,
186
+ "data-position": position,
187
+ role,
188
+ ...props,
189
+ }
190
+
191
+ return (
192
+ <DialogContext.Provider value={{ ref: dialogRef }}>
193
+ {/*eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions */}
194
+ <dialog
195
+ {...componentProps}
196
+ onCancel={handleCancel}
197
+ onClick={handleBackdropClick}
198
+ onClose={onClose}
199
+ onToggle={(e) => {
200
+ handleToggle(e)
201
+ onToggle?.(e)
202
+ }}
203
+ ref={mergeRefs(dialogRef, ref)}
204
+ >
205
+ {children}
206
+ </dialog>
207
+ </DialogContext.Provider>
208
+ )
209
+ }
210
+
211
+ const DialogHeader = ({
212
+ children,
213
+ className,
214
+ close = true,
215
+ ...props
216
+ }: DialogHeaderProps): JSX.Element => {
217
+ const componentProps = {
218
+ className: clsx("coop-dialog--header", !hasUserBg(className) && "bg-tint-grey", className),
219
+ ...props,
220
+ }
221
+
222
+ const dialog = useDialog()
223
+
224
+ return (
225
+ <div {...componentProps}>
226
+ {children}
227
+ {close && (
228
+ <button
229
+ aria-label="Close"
230
+ className="coop-dialog--close"
231
+ onClick={() => dialog.ref?.current?.close()}
232
+ >
233
+ Close <CrossIcon role="presentation" />
234
+ </button>
235
+ )}
236
+ </div>
237
+ )
238
+ }
239
+
240
+ const DialogBody = ({ children, className, ...props }: DialogBodyProps): JSX.Element => {
241
+ const componentProps = {
242
+ className: clsx("coop-dialog--body", className),
243
+ ...props,
244
+ }
245
+ return <div {...componentProps}>{children}</div>
246
+ }
247
+
248
+ Dialog.slot = { name: "Dialog" }
249
+ DialogHeader.slot = { name: "DialogHeader" }
250
+ DialogBody.slot = { name: "DialogBody" }
251
+
252
+ Dialog.Header = DialogHeader
253
+ Dialog.Body = DialogBody
254
+
255
+ export default Dialog
256
+ export { Dialog, DialogBody, DialogHeader }
@@ -0,0 +1,5 @@
1
+ import Dialog from "./Dialog"
2
+
3
+ export default Dialog
4
+ export { Dialog }
5
+ export * from "./Dialog"
@@ -0,0 +1,32 @@
1
+ import clsx from "clsx"
2
+ import { type SVGProps } from "react"
3
+
4
+ import { useId } from "../../hooks/useId"
5
+
6
+ interface IconProps extends SVGProps<SVGSVGElement> {
7
+ alt?: string
8
+ }
9
+
10
+ export const WarningCircleFilledIcon = ({ alt, className, fill, id, ref, ...props }: IconProps) => {
11
+ const uid = useId(id)
12
+ const componentProps = {
13
+ "aria-labelledby": alt ? uid + "__title" : undefined,
14
+ className: clsx("coop-icon", className),
15
+ "data-icon": "warning-circle-filled",
16
+ fill: fill ?? "currentColor",
17
+ id: uid,
18
+ role: alt ? "img" : undefined,
19
+ viewBox: "0 0 32 32",
20
+ ...props,
21
+ }
22
+ return (
23
+ <svg {...componentProps} ref={ref}>
24
+ {alt ? <title id={uid + "__title"}>{alt}</title> : null}
25
+ <path
26
+ clipRule="evenodd"
27
+ d="M16 2c7.732 0 14 6.268 14 14s-6.268 14-14 14S2 23.732 2 16 8.268 2 16 2m0 18a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3m0-11.5a1.5 1.5 0 0 0-1.5 1.5v6a1.5 1.5 0 0 0 3 0v-6A1.5 1.5 0 0 0 16 8.5"
28
+ fillRule="evenodd"
29
+ />
30
+ </svg>
31
+ )
32
+ }
@@ -0,0 +1,33 @@
1
+ import clsx from "clsx"
2
+ import { type SVGProps } from "react"
3
+
4
+ import { useId } from "../../hooks/useId"
5
+
6
+ interface IconProps extends SVGProps<SVGSVGElement> {
7
+ alt?: string
8
+ }
9
+
10
+ export const WarningCircleIcon = ({ alt, className, fill, id, ref, ...props }: IconProps) => {
11
+ const uid = useId(id)
12
+ const componentProps = {
13
+ "aria-labelledby": alt ? uid + "__title" : undefined,
14
+ className: clsx("coop-icon", className),
15
+ "data-icon": "warning-circle",
16
+ fill: fill ?? "currentColor",
17
+ id: uid,
18
+ role: alt ? "img" : undefined,
19
+ viewBox: "0 0 32 32",
20
+ ...props,
21
+ }
22
+ return (
23
+ <svg {...componentProps} ref={ref}>
24
+ {alt ? <title id={uid + "__title"}>{alt}</title> : null}
25
+ <path d="M16 17.5a1.5 1.5 0 0 1-1.5-1.5v-6a1.5 1.5 0 0 1 3 0v6a1.5 1.5 0 0 1-1.5 1.5M16 23a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3" />
26
+ <path
27
+ clipRule="evenodd"
28
+ d="M16 30C8.268 30 2 23.732 2 16S8.268 2 16 2s14 6.268 14 14-6.268 14-14 14m0-2c6.627 0 12-5.373 12-12S22.627 4 16 4 4 9.373 4 16s5.373 12 12 12"
29
+ fillRule="evenodd"
30
+ />
31
+ </svg>
32
+ )
33
+ }
@@ -101,6 +101,8 @@ export { TruckIcon } from "./TruckIcon"
101
101
  export { UserWipeIcon } from "./UserWipeIcon"
102
102
  export { VoucherFilledIcon } from "./VoucherFilledIcon"
103
103
  export { VoucherIcon } from "./VoucherIcon"
104
+ export { WarningCircleFilledIcon } from "./WarningCircleFilledIcon"
105
+ export { WarningCircleIcon } from "./WarningCircleIcon"
104
106
  export { WarningFilledIcon } from "./WarningFilledIcon"
105
107
  export { WarningIcon } from "./WarningIcon"
106
108
  export { WifiIcon } from "./WifiIcon"
package/src/index.ts CHANGED
@@ -4,6 +4,7 @@ export * from "./components/Button"
4
4
  export * from "./components/Card"
5
5
  export * from "./components/Checkbox"
6
6
  export * from "./components/DatePicker"
7
+ export * from "./components/Dialog"
7
8
  export * from "./components/Expandable"
8
9
  export * from "./components/Field"
9
10
  export * from "./components/Fieldset"