@fpkit/acss 0.5.5 → 0.5.6
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/libs/chunk-PWVRDQ3R.js +8 -0
- package/libs/chunk-PWVRDQ3R.js.map +1 -0
- package/libs/chunk-SVS4MX3U.cjs +31 -0
- package/libs/chunk-SVS4MX3U.cjs.map +1 -0
- package/libs/{icons-1f5afc0c.d.ts → icons-31ace3de.d.ts} +86 -49
- package/libs/icons.cjs +2 -2
- package/libs/icons.d.cts +1 -1
- package/libs/icons.d.ts +1 -1
- package/libs/icons.js +1 -1
- package/libs/index.cjs +34 -34
- package/libs/index.cjs.map +1 -1
- package/libs/index.d.cts +46 -15
- package/libs/index.d.ts +46 -15
- package/libs/index.js +7 -7
- package/libs/index.js.map +1 -1
- package/package.json +4 -3
- package/src/components/README.mdx +84 -0
- package/src/components/alert/README.mdx +86 -0
- package/src/components/alert/alert.mdx +74 -0
- package/src/components/alert/alert.scss +80 -0
- package/src/components/alert/alert.stories.tsx +132 -0
- package/src/components/alert/alert.tsx +154 -0
- package/src/components/alert/elements/README.mdx +77 -0
- package/src/components/alert/elements/dismiss-button.stories.tsx +31 -0
- package/src/components/alert/elements/dismiss-button.tsx +28 -0
- package/src/components/badge/badge.mdx +124 -0
- package/src/components/badge/badge.scss +4 -4
- package/src/components/buttons/button.scss +1 -0
- package/src/components/buttons/button.test.tsx +72 -72
- package/src/components/details/details.scss +20 -1
- package/src/components/dialog/README.mdx +187 -0
- package/src/components/dialog/dialog-modal.stories.tsx +113 -0
- package/src/components/dialog/dialog-modal.tsx +111 -0
- package/src/components/dialog/dialog.scss +28 -13
- package/src/components/dialog/dialog.stories.tsx +85 -30
- package/src/components/dialog/dialog.tsx +106 -61
- package/src/components/dialog/hooks/useClickOutside.ts +33 -0
- package/src/components/dialog/views/README.mdx +182 -0
- package/src/components/dialog/views/dialog-footer.tsx +45 -0
- package/src/components/dialog/{view → views}/dialog-header.stories.tsx +3 -4
- package/src/components/dialog/views/dialog-header.tsx +61 -0
- package/src/components/icons/components/add.tsx +14 -14
- package/src/components/icons/components/alert-solid.tsx +36 -0
- package/src/components/icons/components/alert-square-solid.tsx +36 -0
- package/src/components/icons/components/info-solid.tsx +40 -0
- package/src/components/icons/components/info.tsx +36 -0
- package/src/components/icons/components/question-solid.tsx +36 -0
- package/src/components/icons/components/success-solid.tsx +36 -0
- package/src/components/icons/components/warn-solid.tsx +36 -0
- package/src/components/icons/icon.stories.tsx +42 -0
- package/src/components/icons/icon.tsx +57 -41
- package/src/components/icons/index.ts +36 -29
- package/src/components/ui.tsx +28 -25
- package/src/decorators/instructions.tsx +44 -0
- package/src/hooks/useDialogClickHandler.ts +26 -0
- package/src/index.scss +23 -22
- package/src/sass/_globals.scss +7 -1
- package/src/styles/alert/alert.css +68 -0
- package/src/styles/alert/alert.css.map +1 -0
- package/src/styles/badge/badge.css +3 -3
- package/src/styles/details/details.css +14 -1
- package/src/styles/details/details.css.map +1 -1
- package/src/styles/dialog/dialog.css +28 -13
- package/src/styles/dialog/dialog.css.map +1 -1
- package/src/styles/index.css +121 -28
- package/src/styles/index.css.map +1 -1
- package/libs/chunk-QHIABQNQ.js +0 -8
- package/libs/chunk-QHIABQNQ.js.map +0 -1
- package/libs/chunk-ZOHIKF6I.cjs +0 -31
- package/libs/chunk-ZOHIKF6I.cjs.map +0 -1
- package/libs/components/badge/badge.css +0 -1
- package/libs/components/badge/badge.css.map +0 -1
- package/libs/components/badge/badge.min.css +0 -3
- package/libs/components/breadcrumbs/breadcrumb.css +0 -1
- package/libs/components/breadcrumbs/breadcrumb.css.map +0 -1
- package/libs/components/breadcrumbs/breadcrumb.min.css +0 -3
- package/libs/components/buttons/button.css +0 -1
- package/libs/components/buttons/button.css.map +0 -1
- package/libs/components/buttons/button.min.css +0 -3
- package/libs/components/cards/card-style.css +0 -1
- package/libs/components/cards/card-style.css.map +0 -1
- package/libs/components/cards/card-style.min.css +0 -3
- package/libs/components/cards/card.css +0 -1
- package/libs/components/cards/card.css.map +0 -1
- package/libs/components/cards/card.min.css +0 -3
- package/libs/components/details/details.css +0 -1
- package/libs/components/details/details.css.map +0 -1
- package/libs/components/details/details.min.css +0 -3
- package/libs/components/dialog/dialog.css +0 -1
- package/libs/components/dialog/dialog.css.map +0 -1
- package/libs/components/dialog/dialog.min.css +0 -3
- package/libs/components/form/form.css +0 -1
- package/libs/components/form/form.css.map +0 -1
- package/libs/components/form/form.min.css +0 -3
- package/libs/components/icons/icon.css +0 -1
- package/libs/components/icons/icon.css.map +0 -1
- package/libs/components/icons/icon.min.css +0 -3
- package/libs/components/images/img.css +0 -1
- package/libs/components/images/img.css.map +0 -1
- package/libs/components/images/img.min.css +0 -3
- package/libs/components/layout/landmarks.css +0 -1
- package/libs/components/layout/landmarks.css.map +0 -1
- package/libs/components/layout/landmarks.min.css +0 -3
- package/libs/components/link/link.css +0 -1
- package/libs/components/link/link.css.map +0 -1
- package/libs/components/link/link.min.css +0 -3
- package/libs/components/nav/nav.css +0 -1
- package/libs/components/nav/nav.css.map +0 -1
- package/libs/components/nav/nav.min.css +0 -3
- package/libs/components/progress/progress.css +0 -1
- package/libs/components/progress/progress.css.map +0 -1
- package/libs/components/progress/progress.min.css +0 -3
- package/libs/components/styles/index.css +0 -1
- package/libs/components/styles/index.css.map +0 -1
- package/libs/components/styles/index.min.css +0 -3
- package/libs/components/tag/tag.css +0 -1
- package/libs/components/tag/tag.css.map +0 -1
- package/libs/components/tag/tag.min.css +0 -3
- package/libs/components/text-to-speech/text-to-speech.css +0 -1
- package/libs/components/text-to-speech/text-to-speech.css.map +0 -1
- package/libs/components/text-to-speech/text-to-speech.min.css +0 -3
- package/libs/index.css +0 -1
- package/libs/index.css.map +0 -1
- package/src/components/alert-dialog/alert-dialog.stories.tsx +0 -35
- package/src/components/alert-dialog/alert-dialog.tsx +0 -76
- package/src/components/dialog/view/dialog-header.tsx +0 -32
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { StoryObj, Meta } from "@storybook/react";
|
|
2
|
+
import { within, expect, userEvent, waitFor } from "@storybook/test";
|
|
3
|
+
|
|
4
|
+
import DialogModal from "./dialog-modal";
|
|
5
|
+
import WithInstructions from "#/decorators/instructions";
|
|
6
|
+
const meta: Meta<typeof DialogModal> = {
|
|
7
|
+
title: "FP.REACT Components/Dialog/DialogModal",
|
|
8
|
+
component: DialogModal,
|
|
9
|
+
tags: ["autodocs", "experimental"],
|
|
10
|
+
parameters: {
|
|
11
|
+
actions: { argTypesRegex: "^on.*" },
|
|
12
|
+
docs: {
|
|
13
|
+
description: {
|
|
14
|
+
component:
|
|
15
|
+
"DialogModal is a modal dialog component that provides an accessible overlay for displaying content.",
|
|
16
|
+
},
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
args: {
|
|
20
|
+
children: "Dialog Content",
|
|
21
|
+
title: "Dialog Title",
|
|
22
|
+
isOpen: false,
|
|
23
|
+
onClose: () => {},
|
|
24
|
+
},
|
|
25
|
+
} as Meta;
|
|
26
|
+
|
|
27
|
+
export default meta;
|
|
28
|
+
type Story = StoryObj<typeof DialogModal>;
|
|
29
|
+
|
|
30
|
+
export const Default: Story = {
|
|
31
|
+
args: {
|
|
32
|
+
children:
|
|
33
|
+
"DialogModal is a modal dialog component that provides an accessible overlay for displaying content.",
|
|
34
|
+
},
|
|
35
|
+
decorators: [WithInstructions()],
|
|
36
|
+
play: async ({ canvasElement }) => {
|
|
37
|
+
const canvas = within(canvasElement);
|
|
38
|
+
expect(canvas.getByRole("dialog")).toBeInTheDocument();
|
|
39
|
+
},
|
|
40
|
+
} as Story;
|
|
41
|
+
|
|
42
|
+
const instructions = (
|
|
43
|
+
<div>
|
|
44
|
+
<p>
|
|
45
|
+
In this example, the dialog is opened and closed using the Storybook
|
|
46
|
+
interactions.{" "}
|
|
47
|
+
</p>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
export const ModalInteractions: Story = {
|
|
52
|
+
args: {
|
|
53
|
+
children: "This dialog can be opened and closed using the button",
|
|
54
|
+
dialogTitle: "Interactive Dialog",
|
|
55
|
+
btnLabel: "Open Dialog",
|
|
56
|
+
},
|
|
57
|
+
decorators: [WithInstructions(instructions)],
|
|
58
|
+
play: async ({ canvasElement, step }) => {
|
|
59
|
+
const canvas = within(canvasElement);
|
|
60
|
+
|
|
61
|
+
// Find and click the open button
|
|
62
|
+
const openButton = canvas.getByRole("button", { name: /open dialog/i });
|
|
63
|
+
|
|
64
|
+
await step("Open Dialog", async () => {
|
|
65
|
+
await step("Open Dialog", async () => {
|
|
66
|
+
await userEvent.click(openButton, { delay: 1500 }); // Verify dialog is open
|
|
67
|
+
const dialog = canvas.getByRole("dialog");
|
|
68
|
+
expect(dialog).toBeVisible();
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await step("Close Dialog", async () => {
|
|
73
|
+
const dialog = canvas.getByRole("dialog");
|
|
74
|
+
|
|
75
|
+
// Find and click close button
|
|
76
|
+
const closeButton = canvas.getByRole("button", {
|
|
77
|
+
name: /close dialog/i,
|
|
78
|
+
});
|
|
79
|
+
expect(closeButton).toHaveFocus();
|
|
80
|
+
await userEvent.click(closeButton, { delay: 1000 });
|
|
81
|
+
// Verify dialog is closed
|
|
82
|
+
expect(dialog).not.toBeVisible();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
await step("Dialog focus order, close with cancel button", async () => {
|
|
86
|
+
await userEvent.click(openButton, { delay: 1000 });
|
|
87
|
+
const dialog = canvas.getByRole("dialog");
|
|
88
|
+
expect(dialog).toBeVisible();
|
|
89
|
+
expect(
|
|
90
|
+
canvas.getByRole("button", { name: /close dialog/i })
|
|
91
|
+
).toHaveFocus();
|
|
92
|
+
const cancelButton = canvas.getByRole("button", { name: /cancel/i });
|
|
93
|
+
await userEvent.tab();
|
|
94
|
+
expect(cancelButton).toHaveFocus();
|
|
95
|
+
await userEvent.keyboard(" ", { delay: 1000 });
|
|
96
|
+
expect(dialog).not.toBeVisible();
|
|
97
|
+
expect(openButton).toHaveFocus();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await step("Close Dialog with Escape Key", async () => {
|
|
101
|
+
expect(openButton).toHaveFocus();
|
|
102
|
+
await userEvent.click(openButton, { delay: 1000 });
|
|
103
|
+
await userEvent.tab();
|
|
104
|
+
expect(openButton).not.toHaveFocus();
|
|
105
|
+
|
|
106
|
+
const dialog = canvas.getByRole("dialog");
|
|
107
|
+
await userEvent.keyboard(" "); // Close the dialog with the keyboard
|
|
108
|
+
await waitFor(() => {
|
|
109
|
+
expect(dialog).not.toBeVisible();
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
},
|
|
113
|
+
} as Story;
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Dialog.tsx
|
|
2
|
+
import React from "react";
|
|
3
|
+
import Dialog from "./dialog";
|
|
4
|
+
import Button from "#components/buttons/button.jsx";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Additional props for the DialogModal component, extending the props of the Dialog component.
|
|
8
|
+
*
|
|
9
|
+
* @property {string} [className] - Optional className for the dialog content wrapper.
|
|
10
|
+
* @property {string} [btnLabel] - Label for the button that opens the dialog.
|
|
11
|
+
* @property {() => void} [btnOnClick] - Callback function to be called when the button is clicked.
|
|
12
|
+
* @property {"sm" | "md" | "lg"} [btnSize] - Size of the button that opens the dialog.
|
|
13
|
+
*/
|
|
14
|
+
interface DialogModalProps extends React.ComponentProps<typeof Dialog> {
|
|
15
|
+
/** Optional className for the dialog content wrapper */
|
|
16
|
+
className?: string;
|
|
17
|
+
btnLabel?: string;
|
|
18
|
+
btnOnClick?: () => void;
|
|
19
|
+
btnSize?: "sm" | "md" | "lg";
|
|
20
|
+
btnProps?: React.ComponentProps<typeof Button>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A modal dialog component that provides an accessible overlay with an optional trigger button.
|
|
25
|
+
* Extends the base Dialog component with additional button control functionality.
|
|
26
|
+
*
|
|
27
|
+
* @component
|
|
28
|
+
* @param {Object} props - Component props
|
|
29
|
+
* @param {boolean} [props.isAlertDialog] - Whether this is an alert dialog requiring user action
|
|
30
|
+
* @param {() => void} [props.onClose] - Callback when dialog is closed
|
|
31
|
+
* @param {string} [props.dialogTitle] - Title displayed in dialog header
|
|
32
|
+
* @param {string} [props.dialogLabel] - Accessible label for the dialog
|
|
33
|
+
* @param {string} [props.btnLabel="Open Dialog"] - Text label for the trigger button
|
|
34
|
+
* @param {"sm" | "md" | "lg"} [props.btnSize="md"] - Size of the trigger button
|
|
35
|
+
* @param {() => void} [props.btnOnClick] - Callback when trigger button is clicked
|
|
36
|
+
* @param {React.ReactNode} props.children - Content to display inside the dialog
|
|
37
|
+
* @param {() => void} [props.onConfirm] - Callback when confirm button is clicked (for alert dialogs)
|
|
38
|
+
* @param {string} [props.confirmLabel="Confirm"] - Text for the confirm button
|
|
39
|
+
* @param {string} [props.cancelLabel="Cancel"] - Text for the cancel button
|
|
40
|
+
* @param {string} [props.className] - Additional CSS class for the dialog wrapper
|
|
41
|
+
* @param {React.ComponentProps<typeof Button>} [props.btnProps] - Additional props for the trigger button
|
|
42
|
+
* @returns {JSX.Element} A dialog component with a trigger button
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* ```jsx
|
|
46
|
+
* <DialogModal
|
|
47
|
+
* dialogTitle="Confirm Action"
|
|
48
|
+
* btnLabel="Open Modal"
|
|
49
|
+
* btnSize="lg"
|
|
50
|
+
* onClose={() => console.log('Dialog closed')}
|
|
51
|
+
* >
|
|
52
|
+
* <p>Dialog content goes here</p>
|
|
53
|
+
* </DialogModal>
|
|
54
|
+
* ```
|
|
55
|
+
*/
|
|
56
|
+
|
|
57
|
+
export const DialogModal: React.FC<DialogModalProps> = ({
|
|
58
|
+
isAlertDialog,
|
|
59
|
+
onClose,
|
|
60
|
+
dialogTitle,
|
|
61
|
+
dialogLabel,
|
|
62
|
+
btnLabel = "Open Dialog",
|
|
63
|
+
btnSize = "sm",
|
|
64
|
+
btnOnClick,
|
|
65
|
+
children,
|
|
66
|
+
onConfirm,
|
|
67
|
+
confirmLabel = "Confirm",
|
|
68
|
+
cancelLabel = "Cancel",
|
|
69
|
+
className,
|
|
70
|
+
btnProps,
|
|
71
|
+
}) => {
|
|
72
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
73
|
+
|
|
74
|
+
const handleClose = () => {
|
|
75
|
+
setIsOpen(false);
|
|
76
|
+
onClose?.();
|
|
77
|
+
};
|
|
78
|
+
const handleButtonClick = () => {
|
|
79
|
+
setIsOpen(true);
|
|
80
|
+
btnOnClick?.();
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const dialogBtnProps = {
|
|
84
|
+
type: "button" as "button" | "submit" | "reset",
|
|
85
|
+
onClick: handleButtonClick,
|
|
86
|
+
"data-btn": btnSize,
|
|
87
|
+
...btnProps,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
return (
|
|
91
|
+
<>
|
|
92
|
+
<Button {...dialogBtnProps}>{btnLabel}</Button>
|
|
93
|
+
<Dialog
|
|
94
|
+
showDialog={isOpen}
|
|
95
|
+
dialogTitle={dialogTitle}
|
|
96
|
+
onClose={handleClose}
|
|
97
|
+
title={dialogTitle}
|
|
98
|
+
dialogLabel={dialogLabel}
|
|
99
|
+
className={className}
|
|
100
|
+
isAlertDialog={isAlertDialog}
|
|
101
|
+
onConfirm={onConfirm}
|
|
102
|
+
confirmLabel={confirmLabel}
|
|
103
|
+
cancelLabel={cancelLabel}
|
|
104
|
+
>
|
|
105
|
+
{children}
|
|
106
|
+
</Dialog>
|
|
107
|
+
</>
|
|
108
|
+
);
|
|
109
|
+
};
|
|
110
|
+
export default DialogModal;
|
|
111
|
+
DialogModal.displayName = "Dialog Modal";
|
|
@@ -1,25 +1,27 @@
|
|
|
1
1
|
:root {
|
|
2
|
-
--dialog-min-w:
|
|
3
|
-
--dialog-gap: 0.
|
|
2
|
+
--dialog-min-w: max(20rem, 80%);
|
|
3
|
+
--dialog-gap: 0.625rem;
|
|
4
4
|
--dialog-border-color: lightgray;
|
|
5
|
-
--dialog-border-
|
|
6
|
-
--dialog-
|
|
5
|
+
--dialog-border-width: thin;
|
|
6
|
+
--dialog-border-style: solid;
|
|
7
|
+
--dialog-border-radius: var(--border-radius);
|
|
8
|
+
--dialog-padding: 1.5rem;
|
|
7
9
|
--dialog-padding-inline: 1rem;
|
|
8
10
|
--dialog-close-color: gray;
|
|
9
11
|
--dialog-button-bg: transparent;
|
|
10
|
-
--dialog-button-border: transparent
|
|
12
|
+
--dialog-button-border: transparent thin solid;
|
|
11
13
|
--dialog-button-hover-bg: whitesmoke;
|
|
12
14
|
--dialog-display: flex;
|
|
13
15
|
--dialog-flex-direction: column;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
dialog {
|
|
17
|
-
|
|
19
|
+
width: var(--dialog-min-w);
|
|
20
|
+
min-width: var(--min-w);
|
|
18
21
|
gap: var(--dialog-gap);
|
|
19
|
-
border
|
|
22
|
+
border: var(--dialog-border-color) var(--dialog-border-width) solid;
|
|
20
23
|
border-radius: var(--dialog-border-radius);
|
|
21
24
|
padding: var(--dialog-padding);
|
|
22
|
-
padding-inline: var(--dialog-padding-inline);
|
|
23
25
|
padding-block-start: calc(var(--dialog-padding) - 0rem);
|
|
24
26
|
|
|
25
27
|
&[open] {
|
|
@@ -27,6 +29,15 @@ dialog {
|
|
|
27
29
|
flex-direction: var(--dialog-flex-direction);
|
|
28
30
|
gap: var(--dialog-gap);
|
|
29
31
|
}
|
|
32
|
+
section {
|
|
33
|
+
width: 100%;
|
|
34
|
+
display: flex;
|
|
35
|
+
justify-content: start;
|
|
36
|
+
gap: var(--dialog-gap);
|
|
37
|
+
flex-direction: var(--dialog-flex-direction);
|
|
38
|
+
margin-block-start: 0;
|
|
39
|
+
--sect-y: 0;
|
|
40
|
+
}
|
|
30
41
|
}
|
|
31
42
|
|
|
32
43
|
.dialog-header {
|
|
@@ -34,7 +45,7 @@ dialog {
|
|
|
34
45
|
justify-content: space-between;
|
|
35
46
|
align-items: center;
|
|
36
47
|
width: 100%;
|
|
37
|
-
min-width:
|
|
48
|
+
min-width: 100%;
|
|
38
49
|
h3 {
|
|
39
50
|
margin-block-start: 0;
|
|
40
51
|
margin-block-end: 0;
|
|
@@ -48,14 +59,18 @@ dialog {
|
|
|
48
59
|
cursor: pointer;
|
|
49
60
|
&:hover,
|
|
50
61
|
&:focus {
|
|
51
|
-
border-color:
|
|
62
|
+
border-color: var(--dialog-close-color);
|
|
52
63
|
background-color: var(--dialog-button-hover-bg);
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
}
|
|
56
67
|
|
|
57
|
-
.alert-dialog-actions
|
|
68
|
+
.alert-dialog-actions,
|
|
69
|
+
.dialog-footer {
|
|
58
70
|
display: flex;
|
|
59
|
-
|
|
60
|
-
|
|
71
|
+
flex-direction: row;
|
|
72
|
+
flex-wrap: wrap;
|
|
73
|
+
justify-content: var(--dialog-footer-justify, flex-end);
|
|
74
|
+
gap: var(--dialog-gap);
|
|
75
|
+
width: 100%;
|
|
61
76
|
}
|
|
@@ -1,14 +1,37 @@
|
|
|
1
|
-
import { StoryObj, Meta } from "@storybook/react";
|
|
2
|
-
import { within, expect } from "@storybook/test";
|
|
1
|
+
import { StoryObj, Meta, StoryFn } from "@storybook/react";
|
|
2
|
+
import { within, expect, userEvent } from "@storybook/test";
|
|
3
3
|
|
|
4
4
|
import Dialog from "./dialog";
|
|
5
|
+
import React from "react";
|
|
6
|
+
|
|
7
|
+
const content =
|
|
8
|
+
"This is a dialog component used to display modal dialogs. It can be used to show important information or prompt the user for input.";
|
|
9
|
+
|
|
10
|
+
const buttonDecorator = [
|
|
11
|
+
(Story: StoryFn) => {
|
|
12
|
+
const [isOpen, setIsOpen] = React.useState(false);
|
|
13
|
+
return (
|
|
14
|
+
<div>
|
|
15
|
+
<button onClick={() => setIsOpen(true)}>Open Dialog</button>
|
|
16
|
+
<Story
|
|
17
|
+
args={{
|
|
18
|
+
showDialog: isOpen,
|
|
19
|
+
onClose: () => setIsOpen(false),
|
|
20
|
+
dialogTitle: "Dialog Button",
|
|
21
|
+
children: content,
|
|
22
|
+
}}
|
|
23
|
+
/>
|
|
24
|
+
<section>{content}</section>
|
|
25
|
+
</div>
|
|
26
|
+
);
|
|
27
|
+
},
|
|
28
|
+
];
|
|
5
29
|
|
|
6
30
|
const meta: Meta<typeof Dialog> = {
|
|
7
|
-
title: "FP.REACT Components/Dialog",
|
|
31
|
+
title: "FP.REACT Components/Dialog/Dialogs",
|
|
8
32
|
component: Dialog,
|
|
9
33
|
tags: ["alpha"],
|
|
10
34
|
parameters: {
|
|
11
|
-
// actions: { argTypesRegex: "^on.*" },
|
|
12
35
|
docs: {
|
|
13
36
|
description: {
|
|
14
37
|
component: "Dialog component for displaying modal dialogs.",
|
|
@@ -16,46 +39,78 @@ const meta: Meta<typeof Dialog> = {
|
|
|
16
39
|
},
|
|
17
40
|
},
|
|
18
41
|
args: {
|
|
19
|
-
children:
|
|
42
|
+
children: content,
|
|
20
43
|
},
|
|
21
44
|
decorators: [
|
|
22
|
-
(Story) =>
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
45
|
+
(Story) => {
|
|
46
|
+
return (
|
|
47
|
+
<div
|
|
48
|
+
style={{
|
|
49
|
+
display: "flex",
|
|
50
|
+
justifyContent: "center",
|
|
51
|
+
alignItems: "center",
|
|
52
|
+
width: "500px",
|
|
53
|
+
marginInline: "20px",
|
|
54
|
+
marginBlockStart: "5rem",
|
|
55
|
+
}}
|
|
56
|
+
>
|
|
57
|
+
<Story />
|
|
58
|
+
</div>
|
|
59
|
+
);
|
|
60
|
+
},
|
|
36
61
|
],
|
|
37
62
|
} as Story;
|
|
38
63
|
|
|
39
64
|
export default meta;
|
|
40
65
|
type Story = StoryObj<typeof Dialog>;
|
|
41
66
|
|
|
42
|
-
export const
|
|
43
|
-
args: {
|
|
67
|
+
export const BasicDialog: Story = {
|
|
68
|
+
args: {
|
|
69
|
+
isAlertDialog: false,
|
|
70
|
+
showDialog: true,
|
|
71
|
+
dialogTitle: "Basic Dialog",
|
|
72
|
+
},
|
|
73
|
+
} as Story;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Show the dialog by default
|
|
77
|
+
* set the showDialog prop to true
|
|
78
|
+
*/
|
|
79
|
+
export const NonModalDialog: Story = {
|
|
80
|
+
args: {
|
|
81
|
+
showDialog: true,
|
|
82
|
+
isAlertDialog: true,
|
|
83
|
+
dialogTitle: "Non Modal Dialog",
|
|
84
|
+
},
|
|
44
85
|
play: async ({ canvasElement }) => {
|
|
45
86
|
const canvas = within(canvasElement);
|
|
46
|
-
expect(canvas.
|
|
47
|
-
expect(canvas.getByRole("dialog")).toBeInTheDocument();
|
|
87
|
+
await expect(canvas.getByRole("alertdialog")).toBeInTheDocument();
|
|
48
88
|
},
|
|
49
|
-
};
|
|
89
|
+
} as Story;
|
|
50
90
|
|
|
51
|
-
export const
|
|
91
|
+
export const DialogWithButton: Story = {
|
|
92
|
+
decorators: buttonDecorator,
|
|
93
|
+
} as Story;
|
|
94
|
+
|
|
95
|
+
export const DialogInteractions: Story = {
|
|
52
96
|
args: {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
97
|
+
isAlertDialog: false,
|
|
98
|
+
showDialog: true,
|
|
99
|
+
dialogTitle: "Dialog Interactions",
|
|
56
100
|
},
|
|
57
|
-
play: async ({ canvasElement }) => {
|
|
101
|
+
play: async ({ canvasElement, step }) => {
|
|
58
102
|
const canvas = within(canvasElement);
|
|
59
|
-
|
|
103
|
+
const dialog = canvas.getByRole("dialog");
|
|
104
|
+
const closeButton = canvas.getByRole("button", { name: /close dialog/i });
|
|
105
|
+
|
|
106
|
+
await step("Modal is rendered", async () => {
|
|
107
|
+
await expect(dialog).toBeInTheDocument();
|
|
108
|
+
await expect(closeButton).toBeInTheDocument();
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
await step("Close modal", async () => {
|
|
112
|
+
await userEvent.click(closeButton, { delay: 1000 });
|
|
113
|
+
// await expect(dialog).not.toBeInTheDocument();
|
|
114
|
+
});
|
|
60
115
|
},
|
|
61
116
|
} as Story;
|
|
@@ -1,83 +1,128 @@
|
|
|
1
|
-
import React from "react";
|
|
1
|
+
import React, { useRef, useEffect, CSSProperties } from "react";
|
|
2
2
|
import UI from "#components/ui";
|
|
3
|
-
import DialogHeader from "
|
|
3
|
+
import DialogHeader from "#components/dialog/views/dialog-header";
|
|
4
|
+
import DialogFooter from "#components/dialog/views/dialog-footer";
|
|
5
|
+
import { useDialogClickHandler } from "#hooks/useDialogClickHandler.js";
|
|
4
6
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
}
|
|
7
|
+
/**
|
|
8
|
+
* Defines the props for the Dialog component.
|
|
9
|
+
*
|
|
10
|
+
* @property {boolean} [showDialog] - Determines whether the dialog should be shown.
|
|
11
|
+
* @property {boolean} [isAlertDialog] - Determines whether the dialog should be displayed as an alert dialog.
|
|
12
|
+
* @property {() => void} [onClose] - A callback function to be called when the dialog is closed.
|
|
13
|
+
* @property {string} dialogTitle - The title of the dialog.
|
|
14
|
+
* @property {string} [dialogLabel] - An optional label for the dialog.
|
|
15
|
+
* @property {React.ReactNode} children - The content to be displayed inside the dialog.
|
|
16
|
+
* @property {() => void | Promise<void>} [onConfirm] - A callback function to be called when the user confirms the dialog.
|
|
17
|
+
* @property {string} [confirmLabel] - The label for the confirm button.
|
|
18
|
+
* @property {string} [cancelLabel] - The label for the cancel button.
|
|
19
|
+
* @property {string} [className] - An optional CSS class name to be applied to the dialog.
|
|
20
|
+
* @property {CSSProperties} [styles] - Optional inline styles to be applied to the dialog.
|
|
21
|
+
*/
|
|
22
|
+
type DialogModalProps = React.ComponentProps<typeof UI> &
|
|
23
|
+
React.ComponentProps<"dialog"> & {
|
|
24
|
+
dialogTitle: string;
|
|
25
|
+
dialogLabel?: string;
|
|
26
|
+
children: React.ReactNode;
|
|
27
|
+
showDialog?: boolean;
|
|
28
|
+
isAlertDialog?: boolean;
|
|
29
|
+
onClose?: () => void;
|
|
30
|
+
onConfirm?: () => void | Promise<void>;
|
|
31
|
+
confirmLabel?: string;
|
|
32
|
+
cancelLabel?: string;
|
|
33
|
+
className?: string;
|
|
34
|
+
hideFooter?: boolean;
|
|
35
|
+
styles?: CSSProperties;
|
|
36
|
+
};
|
|
15
37
|
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
/**
|
|
39
|
+
* Renders a dialog modal component with customizable content and behavior.
|
|
40
|
+
*
|
|
41
|
+
* @param showDialog - Determines whether the dialog should be shown.
|
|
42
|
+
* @param isAlertDialog - Determines whether the dialog should be displayed as an alert dialog.
|
|
43
|
+
* @param onClose - A callback function to be called when the dialog is closed.
|
|
44
|
+
* @param dialogTitle - The title of the dialog.
|
|
45
|
+
* @param dialogLabel - An optional label for the dialog.
|
|
46
|
+
* @param children - The content to be displayed inside the dialog.
|
|
47
|
+
* @param onConfirm - A callback function to be called when the user confirms the dialog.
|
|
48
|
+
* @param confirmLabel - The label for the confirm button.
|
|
49
|
+
* @param cancelLabel - The label for the cancel button.
|
|
50
|
+
* @param className - An optional CSS class name to be applied to the dialog.
|
|
51
|
+
* @param styles - Optional inline styles to be applied to the dialog.
|
|
52
|
+
*/
|
|
53
|
+
export const Dialog: React.FC<DialogModalProps> = ({
|
|
54
|
+
showDialog,
|
|
23
55
|
isAlertDialog,
|
|
56
|
+
onClose,
|
|
57
|
+
dialogTitle,
|
|
58
|
+
dialogLabel,
|
|
24
59
|
children,
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
60
|
+
onConfirm,
|
|
61
|
+
confirmLabel = "Confirm",
|
|
62
|
+
cancelLabel = "Cancel",
|
|
63
|
+
className = "",
|
|
64
|
+
hideFooter,
|
|
65
|
+
styles,
|
|
66
|
+
}) => {
|
|
67
|
+
const dialogRef = useRef<HTMLDialogElement>(null);
|
|
68
|
+
const [isOpen, setIsOpen] = React.useState(showDialog);
|
|
29
69
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
onOpen();
|
|
34
|
-
}
|
|
35
|
-
setDialogOpen(isOpen);
|
|
36
|
-
}, [isOpen]);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
setIsOpen(showDialog);
|
|
72
|
+
}, [showDialog]);
|
|
37
73
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
onCancel();
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
};
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const dialog = dialogRef.current;
|
|
76
|
+
if (!dialog) return;
|
|
45
77
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
78
|
+
if (isOpen) {
|
|
79
|
+
if (isAlertDialog) {
|
|
80
|
+
dialog.show();
|
|
81
|
+
} else {
|
|
82
|
+
dialog.showModal();
|
|
50
83
|
}
|
|
51
|
-
|
|
52
|
-
|
|
84
|
+
} else {
|
|
85
|
+
dialog.close();
|
|
53
86
|
}
|
|
54
|
-
};
|
|
87
|
+
}, [isOpen, isAlertDialog]);
|
|
55
88
|
|
|
56
|
-
const
|
|
57
|
-
if (onClose)
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
isOpen = false;
|
|
61
|
-
dialogRef.current?.close();
|
|
89
|
+
const handleClose = () => {
|
|
90
|
+
if (onClose) onClose();
|
|
91
|
+
setIsOpen(false);
|
|
62
92
|
};
|
|
63
93
|
|
|
94
|
+
const handleClickOutside = useDialogClickHandler(dialogRef, handleClose);
|
|
95
|
+
|
|
64
96
|
return (
|
|
65
97
|
<UI
|
|
66
98
|
as="dialog"
|
|
67
|
-
|
|
99
|
+
role={isAlertDialog ? "alertdialog" : "dialog"}
|
|
68
100
|
ref={dialogRef}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
{
|
|
101
|
+
onClose={handleClose}
|
|
102
|
+
onClick={handleClickOutside}
|
|
103
|
+
aria-modal={isOpen ? "true" : undefined}
|
|
104
|
+
className={`${"dialog-modal"} ${className}`}
|
|
105
|
+
aria-label={dialogLabel}
|
|
106
|
+
style={styles}
|
|
73
107
|
>
|
|
74
|
-
{
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
108
|
+
<DialogHeader dialogTitle={dialogTitle} onClick={handleClose} />
|
|
109
|
+
|
|
110
|
+
<UI
|
|
111
|
+
as="section"
|
|
112
|
+
className={`dialog-content ${className}`}
|
|
113
|
+
onClick={(e: React.MouseEvent) => e.stopPropagation()}
|
|
114
|
+
>
|
|
115
|
+
{children}
|
|
116
|
+
{!hideFooter && (
|
|
117
|
+
<DialogFooter
|
|
118
|
+
onClose={handleClose}
|
|
119
|
+
onConfirm={onConfirm}
|
|
120
|
+
confirmLabel={confirmLabel}
|
|
121
|
+
cancelLabel={cancelLabel}
|
|
122
|
+
/>
|
|
123
|
+
)}
|
|
124
|
+
</UI>
|
|
78
125
|
</UI>
|
|
79
126
|
);
|
|
80
127
|
};
|
|
81
|
-
|
|
82
128
|
export default React.memo(Dialog);
|
|
83
|
-
Dialog.displayName = "Dialog";
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// hooks/useClickOutside.ts
|
|
2
|
+
import { RefObject } from "react";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hook that handles click outside detection for any HTML element
|
|
6
|
+
* @param ref Reference to the element to detect clicks outside of
|
|
7
|
+
* @param handler Function to call when a click outside is detected
|
|
8
|
+
* @returns Click event handler function
|
|
9
|
+
*/
|
|
10
|
+
const useClickOutside = (
|
|
11
|
+
ref: RefObject<HTMLElement>,
|
|
12
|
+
handler: () => void
|
|
13
|
+
): ((e: React.MouseEvent<HTMLElement>) => void) => {
|
|
14
|
+
return (e: React.MouseEvent<HTMLElement>) => {
|
|
15
|
+
const dimensions = ref.current?.getBoundingClientRect();
|
|
16
|
+
|
|
17
|
+
if (!dimensions) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const isClickOutside =
|
|
22
|
+
e.clientY < dimensions.top ||
|
|
23
|
+
e.clientY > dimensions.bottom ||
|
|
24
|
+
e.clientX < dimensions.left ||
|
|
25
|
+
e.clientX > dimensions.right;
|
|
26
|
+
|
|
27
|
+
if (isClickOutside) {
|
|
28
|
+
handler();
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export default useClickOutside;
|