@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.
Files changed (126) hide show
  1. package/libs/chunk-PWVRDQ3R.js +8 -0
  2. package/libs/chunk-PWVRDQ3R.js.map +1 -0
  3. package/libs/chunk-SVS4MX3U.cjs +31 -0
  4. package/libs/chunk-SVS4MX3U.cjs.map +1 -0
  5. package/libs/{icons-1f5afc0c.d.ts → icons-31ace3de.d.ts} +86 -49
  6. package/libs/icons.cjs +2 -2
  7. package/libs/icons.d.cts +1 -1
  8. package/libs/icons.d.ts +1 -1
  9. package/libs/icons.js +1 -1
  10. package/libs/index.cjs +34 -34
  11. package/libs/index.cjs.map +1 -1
  12. package/libs/index.d.cts +46 -15
  13. package/libs/index.d.ts +46 -15
  14. package/libs/index.js +7 -7
  15. package/libs/index.js.map +1 -1
  16. package/package.json +4 -3
  17. package/src/components/README.mdx +84 -0
  18. package/src/components/alert/README.mdx +86 -0
  19. package/src/components/alert/alert.mdx +74 -0
  20. package/src/components/alert/alert.scss +80 -0
  21. package/src/components/alert/alert.stories.tsx +132 -0
  22. package/src/components/alert/alert.tsx +154 -0
  23. package/src/components/alert/elements/README.mdx +77 -0
  24. package/src/components/alert/elements/dismiss-button.stories.tsx +31 -0
  25. package/src/components/alert/elements/dismiss-button.tsx +28 -0
  26. package/src/components/badge/badge.mdx +124 -0
  27. package/src/components/badge/badge.scss +4 -4
  28. package/src/components/buttons/button.scss +1 -0
  29. package/src/components/buttons/button.test.tsx +72 -72
  30. package/src/components/details/details.scss +20 -1
  31. package/src/components/dialog/README.mdx +187 -0
  32. package/src/components/dialog/dialog-modal.stories.tsx +113 -0
  33. package/src/components/dialog/dialog-modal.tsx +111 -0
  34. package/src/components/dialog/dialog.scss +28 -13
  35. package/src/components/dialog/dialog.stories.tsx +85 -30
  36. package/src/components/dialog/dialog.tsx +106 -61
  37. package/src/components/dialog/hooks/useClickOutside.ts +33 -0
  38. package/src/components/dialog/views/README.mdx +182 -0
  39. package/src/components/dialog/views/dialog-footer.tsx +45 -0
  40. package/src/components/dialog/{view → views}/dialog-header.stories.tsx +3 -4
  41. package/src/components/dialog/views/dialog-header.tsx +61 -0
  42. package/src/components/icons/components/add.tsx +14 -14
  43. package/src/components/icons/components/alert-solid.tsx +36 -0
  44. package/src/components/icons/components/alert-square-solid.tsx +36 -0
  45. package/src/components/icons/components/info-solid.tsx +40 -0
  46. package/src/components/icons/components/info.tsx +36 -0
  47. package/src/components/icons/components/question-solid.tsx +36 -0
  48. package/src/components/icons/components/success-solid.tsx +36 -0
  49. package/src/components/icons/components/warn-solid.tsx +36 -0
  50. package/src/components/icons/icon.stories.tsx +42 -0
  51. package/src/components/icons/icon.tsx +57 -41
  52. package/src/components/icons/index.ts +36 -29
  53. package/src/components/ui.tsx +28 -25
  54. package/src/decorators/instructions.tsx +44 -0
  55. package/src/hooks/useDialogClickHandler.ts +26 -0
  56. package/src/index.scss +23 -22
  57. package/src/sass/_globals.scss +7 -1
  58. package/src/styles/alert/alert.css +68 -0
  59. package/src/styles/alert/alert.css.map +1 -0
  60. package/src/styles/badge/badge.css +3 -3
  61. package/src/styles/details/details.css +14 -1
  62. package/src/styles/details/details.css.map +1 -1
  63. package/src/styles/dialog/dialog.css +28 -13
  64. package/src/styles/dialog/dialog.css.map +1 -1
  65. package/src/styles/index.css +121 -28
  66. package/src/styles/index.css.map +1 -1
  67. package/libs/chunk-QHIABQNQ.js +0 -8
  68. package/libs/chunk-QHIABQNQ.js.map +0 -1
  69. package/libs/chunk-ZOHIKF6I.cjs +0 -31
  70. package/libs/chunk-ZOHIKF6I.cjs.map +0 -1
  71. package/libs/components/badge/badge.css +0 -1
  72. package/libs/components/badge/badge.css.map +0 -1
  73. package/libs/components/badge/badge.min.css +0 -3
  74. package/libs/components/breadcrumbs/breadcrumb.css +0 -1
  75. package/libs/components/breadcrumbs/breadcrumb.css.map +0 -1
  76. package/libs/components/breadcrumbs/breadcrumb.min.css +0 -3
  77. package/libs/components/buttons/button.css +0 -1
  78. package/libs/components/buttons/button.css.map +0 -1
  79. package/libs/components/buttons/button.min.css +0 -3
  80. package/libs/components/cards/card-style.css +0 -1
  81. package/libs/components/cards/card-style.css.map +0 -1
  82. package/libs/components/cards/card-style.min.css +0 -3
  83. package/libs/components/cards/card.css +0 -1
  84. package/libs/components/cards/card.css.map +0 -1
  85. package/libs/components/cards/card.min.css +0 -3
  86. package/libs/components/details/details.css +0 -1
  87. package/libs/components/details/details.css.map +0 -1
  88. package/libs/components/details/details.min.css +0 -3
  89. package/libs/components/dialog/dialog.css +0 -1
  90. package/libs/components/dialog/dialog.css.map +0 -1
  91. package/libs/components/dialog/dialog.min.css +0 -3
  92. package/libs/components/form/form.css +0 -1
  93. package/libs/components/form/form.css.map +0 -1
  94. package/libs/components/form/form.min.css +0 -3
  95. package/libs/components/icons/icon.css +0 -1
  96. package/libs/components/icons/icon.css.map +0 -1
  97. package/libs/components/icons/icon.min.css +0 -3
  98. package/libs/components/images/img.css +0 -1
  99. package/libs/components/images/img.css.map +0 -1
  100. package/libs/components/images/img.min.css +0 -3
  101. package/libs/components/layout/landmarks.css +0 -1
  102. package/libs/components/layout/landmarks.css.map +0 -1
  103. package/libs/components/layout/landmarks.min.css +0 -3
  104. package/libs/components/link/link.css +0 -1
  105. package/libs/components/link/link.css.map +0 -1
  106. package/libs/components/link/link.min.css +0 -3
  107. package/libs/components/nav/nav.css +0 -1
  108. package/libs/components/nav/nav.css.map +0 -1
  109. package/libs/components/nav/nav.min.css +0 -3
  110. package/libs/components/progress/progress.css +0 -1
  111. package/libs/components/progress/progress.css.map +0 -1
  112. package/libs/components/progress/progress.min.css +0 -3
  113. package/libs/components/styles/index.css +0 -1
  114. package/libs/components/styles/index.css.map +0 -1
  115. package/libs/components/styles/index.min.css +0 -3
  116. package/libs/components/tag/tag.css +0 -1
  117. package/libs/components/tag/tag.css.map +0 -1
  118. package/libs/components/tag/tag.min.css +0 -3
  119. package/libs/components/text-to-speech/text-to-speech.css +0 -1
  120. package/libs/components/text-to-speech/text-to-speech.css.map +0 -1
  121. package/libs/components/text-to-speech/text-to-speech.min.css +0 -3
  122. package/libs/index.css +0 -1
  123. package/libs/index.css.map +0 -1
  124. package/src/components/alert-dialog/alert-dialog.stories.tsx +0 -35
  125. package/src/components/alert-dialog/alert-dialog.tsx +0 -76
  126. 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: 320px;
3
- --dialog-gap: 0.75rem;
2
+ --dialog-min-w: max(20rem, 80%);
3
+ --dialog-gap: 0.625rem;
4
4
  --dialog-border-color: lightgray;
5
- --dialog-border-radius: 0.5rem;
6
- --dialog-padding: 0.5rem;
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 1px solid;
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
- min-width: var(--dialog-min-w);
19
+ width: var(--dialog-min-w);
20
+ min-width: var(--min-w);
18
21
  gap: var(--dialog-gap);
19
- border-color: var(--dialog-border-color);
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: 320px;
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: currentColor;
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
- justify-content: flex-start;
60
- gap: 0.5rem;
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: "Dialog Content",
42
+ children: content,
20
43
  },
21
44
  decorators: [
22
- (Story) => (
23
- <div
24
- style={{
25
- display: "flex",
26
- justifyContent: "center",
27
- alignItems: "center",
28
- width: "500px",
29
- marginInline: "20px",
30
- marginBlockStart: "5rem",
31
- }}
32
- >
33
- <Story />
34
- </div>
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 DialogComponent: Story = {
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.getByText(/dialog content/i)).toBeInTheDocument();
47
- expect(canvas.getByRole("dialog")).toBeInTheDocument();
87
+ await expect(canvas.getByRole("alertdialog")).toBeInTheDocument();
48
88
  },
49
- };
89
+ } as Story;
50
90
 
51
- export const NoDialogTitle: Story = {
91
+ export const DialogWithButton: Story = {
92
+ decorators: buttonDecorator,
93
+ } as Story;
94
+
95
+ export const DialogInteractions: Story = {
52
96
  args: {
53
- dialogTitle: "",
54
- children:
55
- "Lorem ipsum dolor sit amet consectetur adipisicing elit. Fuga quod tenetur, alias vitae incidunt porro rem laboriosam deserunt, fugit eligendi eum eos ducimus inventore suscipit, quasi dignissimos dicta. Deleniti, error",
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
- expect(canvas.queryByRole("heading")).not.toBeInTheDocument();
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 "./view/dialog-header";
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
- export type DialogProps = {
6
- isOpen?: boolean;
7
- onOpen?: () => void;
8
- onClose?: () => void;
9
- onCancel?: () => void;
10
- dialogTitle?: string;
11
- showDialogHeader?: boolean;
12
- isAlertDialog?: boolean;
13
- children: React.ReactNode;
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
- export const Dialog = ({
17
- isOpen = true,
18
- dialogTitle = "Dialog",
19
- onOpen,
20
- onClose,
21
- onCancel,
22
- showDialogHeader = true,
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
- ...props
26
- }: DialogProps): JSX.Element => {
27
- const dialogRef = React.useRef<HTMLDialogElement>(null);
28
- const [dialogOpen, setDialogOpen] = React.useState(isOpen);
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
- React.useEffect(() => {
31
- if (isOpen && onOpen) {
32
- dialogRef.current?.showModal();
33
- onOpen();
34
- }
35
- setDialogOpen(isOpen);
36
- }, [isOpen]);
70
+ useEffect(() => {
71
+ setIsOpen(showDialog);
72
+ }, [showDialog]);
37
73
 
38
- const handleCancelEvent = (e: React.SyntheticEvent<HTMLDialogElement>) => {
39
- if (e.currentTarget === e.target) {
40
- if (onCancel) {
41
- onCancel();
42
- }
43
- }
44
- };
74
+ useEffect(() => {
75
+ const dialog = dialogRef.current;
76
+ if (!dialog) return;
45
77
 
46
- const handleCloseEvent = (e: React.SyntheticEvent<HTMLDialogElement>) => {
47
- if (e.currentTarget === e.target) {
48
- if (onClose) {
49
- onClose();
78
+ if (isOpen) {
79
+ if (isAlertDialog) {
80
+ dialog.show();
81
+ } else {
82
+ dialog.showModal();
50
83
  }
51
- isOpen = false;
52
- dialogRef.current?.close();
84
+ } else {
85
+ dialog.close();
53
86
  }
54
- };
87
+ }, [isOpen, isAlertDialog]);
55
88
 
56
- const closeDialog = () => {
57
- if (onClose) {
58
- onClose();
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
- open={dialogOpen}
99
+ role={isAlertDialog ? "alertdialog" : "dialog"}
68
100
  ref={dialogRef}
69
- role={isAlertDialog ? "alertdialog" : undefined}
70
- onCancel={handleCancelEvent}
71
- onClose={handleCloseEvent}
72
- {...props}
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
- {showDialogHeader && (
75
- <DialogHeader dialogTitle={dialogTitle} onClose={closeDialog} />
76
- )}
77
- {children}
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;