@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.
- package/dist/components/Dialog/Dialog.d.ts +94 -0
- package/dist/components/Dialog/Dialog.js +109 -0
- package/dist/components/Dialog/index.d.ts +4 -0
- package/dist/components/icons/WarningCircleFilledIcon.d.ts +6 -0
- package/dist/components/icons/WarningCircleFilledIcon.js +20 -0
- package/dist/components/icons/WarningCircleIcon.d.ts +6 -0
- package/dist/components/icons/WarningCircleIcon.js +20 -0
- package/dist/components/icons/index.d.ts +2 -0
- package/dist/components/icons/index.js +2 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/package.json +6 -3
- package/src/components/Dialog/Dialog.tsx +256 -0
- package/src/components/Dialog/index.ts +5 -0
- package/src/components/icons/WarningCircleFilledIcon.tsx +32 -0
- package/src/components/icons/WarningCircleIcon.tsx +33 -0
- package/src/components/icons/index.ts +2 -0
- package/src/index.ts +1 -0
|
@@ -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,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,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.
|
|
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.
|
|
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": "
|
|
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,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"
|