@blockle/blocks-react 3.0.1 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (119) hide show
  1. package/README.md +138 -0
  2. package/dist/components/display/Tag/Tag.d.ts +28 -0
  3. package/dist/components/display/Tag/Tag.d.ts.map +1 -0
  4. package/dist/components/display/Tag/Tag.js +46 -0
  5. package/dist/components/feedback/Alert/Alert.d.ts +17 -2
  6. package/dist/components/feedback/Alert/Alert.d.ts.map +1 -1
  7. package/dist/components/feedback/Alert/Alert.js +8 -35
  8. package/dist/components/feedback/Toast/Toast.css.d.ts +10 -1
  9. package/dist/components/feedback/Toast/Toast.css.d.ts.map +1 -1
  10. package/dist/components/feedback/Toast/Toast.css.js +37 -10
  11. package/dist/components/feedback/Toast/ToastContext.d.ts +6 -0
  12. package/dist/components/feedback/Toast/ToastContext.d.ts.map +1 -1
  13. package/dist/components/feedback/Toast/ToastProvider.d.ts +15 -1
  14. package/dist/components/feedback/Toast/ToastProvider.d.ts.map +1 -1
  15. package/dist/components/feedback/Toast/ToastProvider.js +128 -44
  16. package/dist/components/feedback/Toast/useCountdowns.d.ts +14 -0
  17. package/dist/components/feedback/Toast/useCountdowns.d.ts.map +1 -0
  18. package/dist/components/feedback/Toast/useCountdowns.js +74 -0
  19. package/dist/components/form/Button/Button.d.ts +11 -3
  20. package/dist/components/form/Button/Button.d.ts.map +1 -1
  21. package/dist/components/form/Checkbox/Checkbox.d.ts +0 -1
  22. package/dist/components/form/Checkbox/Checkbox.d.ts.map +1 -1
  23. package/dist/components/form/Checkbox/Checkbox.js +7 -27
  24. package/dist/components/form/Checkbox/checkbox.css.d.ts.map +1 -1
  25. package/dist/components/form/Checkbox/checkbox.css.js +1 -0
  26. package/dist/components/form/IconButton/IconButton.d.ts +29 -0
  27. package/dist/components/form/IconButton/IconButton.d.ts.map +1 -0
  28. package/dist/components/form/IconButton/IconButton.js +61 -0
  29. package/dist/components/form/NumericInput/NumericInput.d.ts +11 -1
  30. package/dist/components/form/NumericInput/NumericInput.d.ts.map +1 -1
  31. package/dist/components/form/NumericInput/NumericInput.js +52 -24
  32. package/dist/components/form/Radio/Radio.d.ts +0 -1
  33. package/dist/components/form/Radio/Radio.d.ts.map +1 -1
  34. package/dist/components/form/Radio/Radio.js +1 -11
  35. package/dist/components/form/Radio/radio.css.d.ts.map +1 -1
  36. package/dist/components/form/Radio/radio.css.js +1 -0
  37. package/dist/components/form/Select/Select.d.ts +2 -1
  38. package/dist/components/form/Select/Select.d.ts.map +1 -1
  39. package/dist/components/form/Select/Select.js +4 -24
  40. package/dist/components/form/Switch/Switch.d.ts +5 -1
  41. package/dist/components/form/Switch/Switch.d.ts.map +1 -1
  42. package/dist/components/form/Switch/Switch.js +19 -29
  43. package/dist/components/form/Switch/switch.css.d.ts.map +1 -1
  44. package/dist/components/form/Switch/switch.css.js +1 -0
  45. package/dist/components/form/TextInput/TextInput.d.ts +0 -1
  46. package/dist/components/form/TextInput/TextInput.d.ts.map +1 -1
  47. package/dist/components/form/TextInput/TextInput.js +0 -2
  48. package/dist/components/form/Textarea/Textarea.css.d.ts +2 -0
  49. package/dist/components/form/Textarea/Textarea.css.d.ts.map +1 -1
  50. package/dist/components/form/Textarea/Textarea.css.js +17 -1
  51. package/dist/components/form/Textarea/Textarea.d.ts +11 -0
  52. package/dist/components/form/Textarea/Textarea.d.ts.map +1 -1
  53. package/dist/components/form/Textarea/Textarea.js +8 -2
  54. package/dist/components/layout/Inline/Inline.d.ts +11 -3
  55. package/dist/components/layout/Inline/Inline.d.ts.map +1 -1
  56. package/dist/components/layout/Inline/Inline.js +5 -3
  57. package/dist/components/layout/Stack/Stack.d.ts +12 -8
  58. package/dist/components/layout/Stack/Stack.d.ts.map +1 -1
  59. package/dist/components/layout/Stack/Stack.js +8 -4
  60. package/dist/components/navigation/Link/Link.d.ts +1 -1
  61. package/dist/components/navigation/Link/Link.d.ts.map +1 -1
  62. package/dist/components/navigation/Link/Link.js +2 -2
  63. package/dist/components/overlay/Dialog/Dialog.d.ts +4 -1
  64. package/dist/components/overlay/Dialog/Dialog.d.ts.map +1 -1
  65. package/dist/components/overlay/Dialog/Dialog.js +19 -46
  66. package/dist/components/overlay/Drawer/Drawer.d.ts +9 -2
  67. package/dist/components/overlay/Drawer/Drawer.d.ts.map +1 -1
  68. package/dist/components/overlay/Drawer/Drawer.js +25 -44
  69. package/dist/components/overlay/Drawer/drawer.css.d.ts.map +1 -1
  70. package/dist/components/overlay/Drawer/drawer.css.js +4 -8
  71. package/dist/components/overlay/Popover/Popover.d.ts +15 -3
  72. package/dist/components/overlay/Popover/Popover.d.ts.map +1 -1
  73. package/dist/components/overlay/Popover/Popover.js +35 -11
  74. package/dist/components/overlay/Popover/popover-utils.d.ts +38 -1
  75. package/dist/components/overlay/Popover/popover-utils.d.ts.map +1 -1
  76. package/dist/components/overlay/Popover/popover-utils.js +92 -41
  77. package/dist/components/overlay/Tooltip/Tooltip.d.ts +8 -0
  78. package/dist/components/overlay/Tooltip/Tooltip.d.ts.map +1 -1
  79. package/dist/components/overlay/Tooltip/Tooltip.js +3 -2
  80. package/dist/components/overlay/requestCloseReason.d.ts +6 -0
  81. package/dist/components/overlay/requestCloseReason.d.ts.map +1 -0
  82. package/dist/components/overlay/useModalOverlay.d.ts +35 -0
  83. package/dist/components/overlay/useModalOverlay.d.ts.map +1 -0
  84. package/dist/components/overlay/useModalOverlay.js +85 -0
  85. package/dist/components/typography/Heading/Heading.d.ts +2 -1
  86. package/dist/components/typography/Heading/Heading.d.ts.map +1 -1
  87. package/dist/components/typography/Heading/Heading.js +9 -1
  88. package/dist/components/typography/Text/Text.d.ts +3 -2
  89. package/dist/components/typography/Text/Text.d.ts.map +1 -1
  90. package/dist/components/typography/Text/Text.js +9 -1
  91. package/dist/components/typography/textOverflow.css.d.ts +3 -0
  92. package/dist/components/typography/textOverflow.css.d.ts.map +1 -0
  93. package/dist/components/typography/textOverflow.css.js +32 -0
  94. package/dist/components/typography/textOverflow.d.ts +9 -0
  95. package/dist/components/typography/textOverflow.d.ts.map +1 -0
  96. package/dist/components/typography/textOverflow.js +10 -0
  97. package/dist/hooks/useClickOutside/useClickOutside.d.ts.map +1 -1
  98. package/dist/hooks/useClickOutside/useClickOutside.js +27 -7
  99. package/dist/hooks/useModalDialog/useModalDialog.d.ts +13 -0
  100. package/dist/hooks/useModalDialog/useModalDialog.d.ts.map +1 -0
  101. package/dist/hooks/useModalDialog/useModalDialog.js +39 -0
  102. package/dist/icons/icons.d.ts +8 -0
  103. package/dist/icons/icons.d.ts.map +1 -0
  104. package/dist/icons/icons.js +39 -0
  105. package/dist/index.d.ts +5 -2
  106. package/dist/index.d.ts.map +1 -1
  107. package/dist/index.js +4 -0
  108. package/dist/testUtils/browser/consumerStyles.css.d.ts +2 -0
  109. package/dist/testUtils/browser/consumerStyles.css.d.ts.map +1 -0
  110. package/dist/testUtils/browser/pointer.d.ts +18 -0
  111. package/dist/testUtils/browser/pointer.d.ts.map +1 -0
  112. package/dist/testUtils/testTheme.css.d.ts.map +1 -1
  113. package/package.json +2 -2
  114. package/dist/components/feedback/Alert/Alert.css.d.ts +0 -2
  115. package/dist/components/feedback/Alert/Alert.css.d.ts.map +0 -1
  116. package/dist/components/feedback/Alert/Alert.css.js +0 -15
  117. package/dist/components/overlay/Dialog/dialogHelper.d.ts +0 -7
  118. package/dist/components/overlay/Dialog/dialogHelper.d.ts.map +0 -1
  119. package/dist/components/overlay/Dialog/dialogHelper.js +0 -22
package/README.md CHANGED
@@ -1 +1,139 @@
1
1
  # @blockle/blocks-react
2
+
3
+ ## Testing
4
+
5
+ `Dialog` and `Drawer` open with `HTMLDialogElement.showModal()` and close with `close()`. jsdom does not implement these, so in a jsdom test an open `Dialog` stays a closed `<dialog>`, and Testing Library cannot find it, or anything in it, by role.
6
+
7
+ Add this polyfill to your Vitest or Jest setup file. It only defines the methods jsdom is missing:
8
+
9
+ ```ts
10
+ const DialogPrototype = HTMLDialogElement.prototype;
11
+
12
+ if (!DialogPrototype.showModal) {
13
+ DialogPrototype.showModal = function showModal() {
14
+ this.setAttribute('open', '');
15
+ };
16
+ }
17
+
18
+ if (!DialogPrototype.show) {
19
+ DialogPrototype.show = function show() {
20
+ this.setAttribute('open', '');
21
+ };
22
+ }
23
+
24
+ if (!DialogPrototype.close) {
25
+ DialogPrototype.close = function close() {
26
+ if (!this.hasAttribute('open')) {
27
+ return;
28
+ }
29
+
30
+ this.removeAttribute('open');
31
+ this.dispatchEvent(new Event('close'));
32
+ };
33
+ }
34
+ ```
35
+
36
+ With it, an open dialog can be found with `getByRole('dialog')`:
37
+
38
+ ```tsx
39
+ render(
40
+ <Dialog open onRequestClose={() => {}} aria-label="Settings">
41
+ <button type="button">Save</button>
42
+ </Dialog>,
43
+ );
44
+
45
+ screen.getByRole('dialog', { name: 'Settings' });
46
+ screen.getByRole('button', { name: 'Save' });
47
+ ```
48
+
49
+ Under the polyfill the dialog is open but not modal: it is not in the top layer, and nothing outside it is made inert, so content behind it can still be queried and clicked. Test modal behaviour, focus trapping and clicks outside in a real browser, for example with Vitest Browser Mode or Playwright.
50
+
51
+ ### Popover
52
+
53
+ `Popover` opens with `showPopover()` and closes with `hidePopover()`. jsdom does not implement the Popover API, and its default stylesheet hides every `[popover]` element that is not open, so Testing Library treats an open popover's content as hidden and `getByRole` finds nothing in it.
54
+
55
+ Add this polyfill to your test setup. It only defines the methods jsdom is missing, and marks an open popover with a `data-popover-open` attribute that a test stylesheet shows:
56
+
57
+ ```ts
58
+ const PopoverPrototype = HTMLElement.prototype;
59
+
60
+ if (!PopoverPrototype.showPopover) {
61
+ PopoverPrototype.showPopover = function showPopover() {
62
+ this.setAttribute('data-popover-open', '');
63
+ };
64
+ }
65
+
66
+ if (!PopoverPrototype.hidePopover) {
67
+ PopoverPrototype.hidePopover = function hidePopover() {
68
+ this.removeAttribute('data-popover-open');
69
+ };
70
+ }
71
+
72
+ if (!PopoverPrototype.togglePopover) {
73
+ PopoverPrototype.togglePopover = function togglePopover(force?: boolean) {
74
+ return this.toggleAttribute('data-popover-open', force);
75
+ };
76
+ }
77
+
78
+ const popoverStyle = document.createElement('style');
79
+ popoverStyle.textContent = '[popover][data-popover-open] { display: block; }';
80
+ document.head.append(popoverStyle);
81
+ ```
82
+
83
+ With it, an open popover's content can be found by role:
84
+
85
+ ```tsx
86
+ render(
87
+ <Popover open anchorElement={anchorRef} onRequestClose={() => {}}>
88
+ <button type="button">Inside</button>
89
+ </Popover>,
90
+ );
91
+
92
+ screen.getByRole('button', { name: 'Inside' });
93
+ ```
94
+
95
+ Other libraries may check for `showPopover` and behave differently once it exists; dnd-kit, for example, drags differently. If a global polyfill breaks such tests, apply it only inside the `describe` that opens a popover, and remove what it added afterwards:
96
+
97
+ ```ts
98
+ import { afterAll, beforeAll, describe } from 'vitest';
99
+
100
+ describe('with a popover open', () => {
101
+ const polyfills: Record<string, (this: HTMLElement, force?: boolean) => unknown> = {
102
+ showPopover() {
103
+ this.setAttribute('data-popover-open', '');
104
+ },
105
+ hidePopover() {
106
+ this.removeAttribute('data-popover-open');
107
+ },
108
+ togglePopover(force?: boolean) {
109
+ return this.toggleAttribute('data-popover-open', force);
110
+ },
111
+ };
112
+ const added: string[] = [];
113
+ const popoverStyle = document.createElement('style');
114
+
115
+ beforeAll(() => {
116
+ for (const [method, polyfill] of Object.entries(polyfills)) {
117
+ if (!(method in HTMLElement.prototype)) {
118
+ Reflect.set(HTMLElement.prototype, method, polyfill);
119
+ added.push(method);
120
+ }
121
+ }
122
+
123
+ popoverStyle.textContent = '[popover][data-popover-open] { display: block; }';
124
+ document.head.append(popoverStyle);
125
+ });
126
+
127
+ afterAll(() => {
128
+ for (const method of added) {
129
+ Reflect.deleteProperty(HTMLElement.prototype, method);
130
+ }
131
+
132
+ popoverStyle.remove();
133
+ });
134
+
135
+ // Tests that open a popover
136
+ });
137
+ ```
138
+
139
+ Where the Popover API is missing and no polyfill is applied, `Popover` warns when it opens or closes instead of throwing. Its content then stays hidden under jsdom's default styles. Test positioning, the top layer and clicks outside in a real browser.
@@ -0,0 +1,28 @@
1
+ import { ComponentThemes, HTMLElementProps } from '@blockle/blocks-core';
2
+ import { default as React } from 'react';
3
+ type TagTheme = ComponentThemes['tag'];
4
+ export type TagProps = {
5
+ /**
6
+ * Render the single child element instead of a `span`, with the Tag's
7
+ * classes and attributes merged onto it
8
+ */
9
+ asChild?: boolean;
10
+ children: React.ReactNode;
11
+ /**
12
+ * Content placed after the text, such as an `IconButton` that removes the
13
+ * Tag. It is sized to the Tag's line height, so it doesn't make the Tag
14
+ * taller.
15
+ */
16
+ endSlot?: React.ReactNode;
17
+ intent?: TagTheme['variants']['intent'];
18
+ ref?: React.Ref<HTMLSpanElement>;
19
+ size?: TagTheme['variants']['size'];
20
+ /**
21
+ * Content placed before the text, such as an `Icon`, an avatar `img` or an
22
+ * emoji. It is sized to the Tag's line height.
23
+ */
24
+ startSlot?: React.ReactNode;
25
+ } & Omit<HTMLElementProps<HTMLSpanElement>, 'children' | 'ref' | 'size'>;
26
+ export declare const Tag: React.FC<TagProps>;
27
+ export {};
28
+ //# sourceMappingURL=Tag.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Tag.d.ts","sourceRoot":"","sources":["../../../../src/components/display/Tag/Tag.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AAI/B,KAAK,QAAQ,GAAG,eAAe,CAAC,KAAK,CAAC,CAAC;AAEvC,MAAM,MAAM,QAAQ,GAAG;IACrB;;;OAGG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B;;;;OAIG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC;IACxC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,QAAQ,CAAC,UAAU,CAAC,CAAC,MAAM,CAAC,CAAC;IACpC;;;OAGG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B,GAAG,IAAI,CAAC,gBAAgB,CAAC,eAAe,CAAC,EAAE,UAAU,GAAG,KAAK,GAAG,MAAM,CAAC,CAAC;AAQzE,eAAO,MAAM,GAAG,EAAE,KAAK,CAAC,EAAE,CAAC,QAAQ,CAoClC,CAAC"}
@@ -0,0 +1,46 @@
1
+ "use client";
2
+ import { jsxs, jsx } from "react/jsx-runtime";
3
+ import { classnames } from "@blockle/blocks-core";
4
+ import { createSlottable } from "@blockle/blocks-react-slot";
5
+ import { useComponentStyles } from "../../../hooks/useComponentStyles/useComponentStyles.js";
6
+ const [Template, Slot] = createSlottable("span");
7
+ function hasContent(node) {
8
+ return node !== void 0 && node !== null && node !== false && node !== "";
9
+ }
10
+ const Tag = ({
11
+ asChild,
12
+ children,
13
+ className,
14
+ endSlot,
15
+ intent,
16
+ size,
17
+ startSlot,
18
+ ...restProps
19
+ }) => {
20
+ const tagClassName = useComponentStyles("tag", {
21
+ root: true,
22
+ variants: { intent, size }
23
+ });
24
+ const startSlotClassName = useComponentStyles(
25
+ "tag",
26
+ { startSlot: true },
27
+ false
28
+ );
29
+ const endSlotClassName = useComponentStyles("tag", { endSlot: true }, false);
30
+ return /* @__PURE__ */ jsxs(
31
+ Template,
32
+ {
33
+ asChild,
34
+ className: classnames(tagClassName, className),
35
+ ...restProps,
36
+ children: [
37
+ hasContent(startSlot) && /* @__PURE__ */ jsx("span", { className: startSlotClassName, children: startSlot }),
38
+ /* @__PURE__ */ jsx(Slot, { children }),
39
+ hasContent(endSlot) && /* @__PURE__ */ jsx("span", { className: endSlotClassName, children: endSlot })
40
+ ]
41
+ }
42
+ );
43
+ };
44
+ export {
45
+ Tag
46
+ };
@@ -2,9 +2,24 @@ import { ComponentThemes, HTMLElementProps } from '@blockle/blocks-core';
2
2
  type AlertTheme = ComponentThemes['alert'];
3
3
  export type AlertProps = {
4
4
  children?: React.ReactNode;
5
+ /**
6
+ * Content placed after the message, such as a `Button`, or an `IconButton`
7
+ * to dismiss the Alert. The message takes the remaining width.
8
+ */
9
+ endSlot?: React.ReactNode;
5
10
  intent?: AlertTheme['variants']['intent'];
6
- onRequestClose?: () => void;
7
- } & HTMLElementProps<HTMLDivElement>;
11
+ /**
12
+ * `alert` announces the message as urgent, for a message shown in response
13
+ * to something that just happened. Use `status` for a lasting notice, such
14
+ * as a banner that stays while a record is archived.
15
+ * @default 'alert'
16
+ */
17
+ role?: 'alert' | 'status';
18
+ /**
19
+ * Content placed before the message, such as an icon
20
+ */
21
+ startSlot?: React.ReactNode;
22
+ } & Omit<HTMLElementProps<HTMLDivElement>, 'role'>;
8
23
  export declare const Alert: React.FC<AlertProps>;
9
24
  export {};
10
25
  //# sourceMappingURL=Alert.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Alert.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/Alert/Alert.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAO9B,KAAK,UAAU,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;AAE3C,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC1C,cAAc,CAAC,EAAE,MAAM,IAAI,CAAC;CAC7B,GAAG,gBAAgB,CAAC,cAAc,CAAC,CAAC;AAErC,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,UAAU,CAwDtC,CAAC"}
1
+ {"version":3,"file":"Alert.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/Alert/Alert.tsx"],"names":[],"mappings":"AAEA,OAAO,EACL,KAAK,eAAe,EAEpB,KAAK,gBAAgB,EACtB,MAAM,sBAAsB,CAAC;AAK9B,KAAK,UAAU,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;AAE3C,MAAM,MAAM,UAAU,GAAG;IACvB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,MAAM,CAAC,EAAE,UAAU,CAAC,UAAU,CAAC,CAAC,QAAQ,CAAC,CAAC;IAC1C;;;;;OAKG;IACH,IAAI,CAAC,EAAE,OAAO,GAAG,QAAQ,CAAC;IAC1B;;OAEG;IACH,SAAS,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;CAC7B,GAAG,IAAI,CAAC,gBAAgB,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC,CAAC;AAEnD,eAAO,MAAM,KAAK,EAAE,KAAK,CAAC,EAAE,CAAC,UAAU,CA8BtC,CAAC"}
@@ -2,14 +2,14 @@
2
2
  import { jsxs, jsx } from "react/jsx-runtime";
3
3
  import { classnames } from "@blockle/blocks-core";
4
4
  import { useComponentStyles } from "../../../hooks/useComponentStyles/useComponentStyles.js";
5
- import { Button } from "../../form/Button/Button.js";
6
5
  import { Box } from "../../layout/Box/Box.js";
7
- import { closeButton } from "./Alert.css.js";
8
6
  const Alert = ({
9
7
  intent = "info",
10
8
  children,
11
- onRequestClose,
12
9
  className,
10
+ endSlot,
11
+ role = "alert",
12
+ startSlot,
13
13
  ...restProps
14
14
  }) => {
15
15
  const containerClassName = useComponentStyles("alert", {
@@ -19,43 +19,16 @@ const Alert = ({
19
19
  return /* @__PURE__ */ jsxs(
20
20
  Box,
21
21
  {
22
- role: "alert",
22
+ role,
23
23
  className: classnames(containerClassName, className),
24
24
  display: "flex",
25
25
  alignItems: "center",
26
- justifyContent: "space-between",
26
+ gap: 2,
27
27
  ...restProps,
28
28
  children: [
29
- /* @__PURE__ */ jsx("div", { children }),
30
- onRequestClose ? /* @__PURE__ */ jsx(
31
- Button,
32
- {
33
- variant: "ghost",
34
- size: "small",
35
- className: closeButton,
36
- onClick: () => onRequestClose?.(),
37
- children: /* @__PURE__ */ jsxs(
38
- "svg",
39
- {
40
- role: "presentation",
41
- xmlns: "http://www.w3.org/2000/svg",
42
- width: "24",
43
- height: "24",
44
- viewBox: "0 0 24 24",
45
- fill: "none",
46
- stroke: "currentColor",
47
- strokeWidth: "2",
48
- strokeLinecap: "round",
49
- strokeLinejoin: "round",
50
- children: [
51
- /* @__PURE__ */ jsx("path", { stroke: "none", d: "M0 0h24v24H0z", fill: "none" }),
52
- /* @__PURE__ */ jsx("path", { d: "M18 6l-12 12" }),
53
- /* @__PURE__ */ jsx("path", { d: "M6 6l12 12" })
54
- ]
55
- }
56
- )
57
- }
58
- ) : null
29
+ startSlot,
30
+ /* @__PURE__ */ jsx(Box, { flexGrow: 1, children }),
31
+ endSlot
59
32
  ]
60
33
  }
61
34
  );
@@ -1,2 +1,11 @@
1
- export declare const container: string;
1
+ export declare const stack: string;
2
+ export declare const toast: string;
3
+ export declare const placement: {
4
+ top: string;
5
+ 'top-start': string;
6
+ 'top-end': string;
7
+ bottom: string;
8
+ 'bottom-start': string;
9
+ 'bottom-end': string;
10
+ };
2
11
  //# sourceMappingURL=Toast.css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Toast.css.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/Toast/Toast.css.ts"],"names":[],"mappings":"AAQA,eAAO,MAAM,SAAS,QASpB,CAAC"}
1
+ {"version":3,"file":"Toast.css.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/Toast/Toast.css.ts"],"names":[],"mappings":"AAGA,eAAO,MAAM,KAAK,QAYhB,CAAC;AAEH,eAAO,MAAM,KAAK,QAOhB,CAAC;AAmBH,eAAO,MAAM,SAAS;;;;;;;CAOrB,CAAC"}
@@ -1,22 +1,49 @@
1
1
  import { setFileScope, endFileScope } from "@vanilla-extract/css/fileScope";
2
2
  import { layers } from "@blockle/blocks-core";
3
- import { style, keyframes } from "@vanilla-extract/css";
3
+ import { style } from "@vanilla-extract/css";
4
4
  setFileScope("src/components/feedback/Toast/Toast.css.ts", "@blockle/blocks-react");
5
- const toastEnter = keyframes({
6
- from: { opacity: 0, scale: 0.9, translate: "0 20%" },
7
- to: { opacity: 1, scale: 1, translate: "0 0" }
5
+ const stack = style({
6
+ "@layer": {
7
+ [layers.molecule]: {
8
+ position: "fixed",
9
+ display: "flex",
10
+ flexDirection: "column",
11
+ width: "100%",
12
+ boxSizing: "border-box",
13
+ // Let clicks through the stack's empty space, but not through a toast
14
+ pointerEvents: "none"
15
+ }
16
+ }
8
17
  });
9
- const container = style({
18
+ const toast = style({
10
19
  "@layer": {
11
20
  [layers.molecule]: {
12
- animationName: toastEnter,
13
- animationDuration: "240ms",
14
- animationTimingFunction: "ease-out",
15
- height: "fit-content"
21
+ height: "fit-content",
22
+ pointerEvents: "auto"
16
23
  }
17
24
  }
18
25
  });
26
+ const top = {
27
+ insetBlockStart: 0,
28
+ // Newest toast nearest the top edge
29
+ flexDirection: "column-reverse"
30
+ };
31
+ const bottom = { insetBlockEnd: 0 };
32
+ const center = { insetInline: 0, marginInline: "auto" };
33
+ const start = { insetInlineStart: 0 };
34
+ const end = { insetInlineEnd: 0 };
35
+ const placementStyle = (...rules) => style({ "@layer": { [layers.molecule]: Object.assign({}, ...rules) } });
36
+ const placement = {
37
+ top: placementStyle(top, center),
38
+ "top-start": placementStyle(top, start),
39
+ "top-end": placementStyle(top, end),
40
+ bottom: placementStyle(bottom, center),
41
+ "bottom-start": placementStyle(bottom, start),
42
+ "bottom-end": placementStyle(bottom, end)
43
+ };
19
44
  endFileScope();
20
45
  export {
21
- container
46
+ placement,
47
+ stack,
48
+ toast
22
49
  };
@@ -1,6 +1,12 @@
1
1
  import { AlertProps } from '../Alert/Alert.js';
2
2
  export type Toast = {
3
3
  id: string;
4
+ /**
5
+ * Milliseconds before the toast removes itself. The countdown pauses while
6
+ * any toast is hovered or focused, or the page is hidden. Leave it out to
7
+ * keep the toast until it's closed, and always leave it out when the toast
8
+ * contains an action, such as an "Undo" button.
9
+ */
4
10
  duration?: number;
5
11
  children?: React.ReactNode;
6
12
  intent?: AlertProps['intent'];
@@ -1 +1 @@
1
- {"version":3,"file":"ToastContext.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/Toast/ToastContext.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,MAAM,KAAK,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B,CAAC;AAEF,eAAO,MAAM,YAAY,kDAA+C,CAAC"}
1
+ {"version":3,"file":"ToastContext.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/Toast/ToastContext.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAEpD,MAAM,MAAM,KAAK,GAAG;IAClB,EAAE,EAAE,MAAM,CAAC;IACX;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,MAAM,CAAC,EAAE,UAAU,CAAC,QAAQ,CAAC,CAAC;CAC/B,CAAC;AAEF,MAAM,MAAM,gBAAgB,GAAG;IAC7B,GAAG,EAAE,CAAC,KAAK,EAAE,KAAK,KAAK,IAAI,CAAC;IAC5B,MAAM,EAAE,CAAC,EAAE,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B,CAAC;AAEF,eAAO,MAAM,YAAY,kDAA+C,CAAC"}
@@ -1,6 +1,20 @@
1
+ import { ToastPlacement } from '@blockle/blocks-core';
2
+ export type { ToastPlacement };
3
+ type ToastPosition = 'bottom-left' | 'bottom-center' | 'bottom-right' | 'top-left' | 'top-center' | 'top-right';
1
4
  export type ToastProviderProps = {
2
5
  children?: React.ReactNode;
3
- position?: 'bottom-left' | 'bottom-center' | 'bottom-right' | 'top-left' | 'top-center' | 'top-right';
6
+ /**
7
+ * Accessible name of each Toast's close button. Pass your own wording to
8
+ * translate it.
9
+ * @default 'Close'
10
+ */
11
+ closeLabel?: string;
12
+ /** Where the toasts sit on the screen. Defaults to `bottom` */
13
+ placement?: ToastPlacement;
14
+ /**
15
+ * @deprecated Use `placement`, which wins when both are passed
16
+ */
17
+ position?: ToastPosition;
4
18
  };
5
19
  export declare const ToastProvider: React.FC<ToastProviderProps>;
6
20
  //# sourceMappingURL=ToastProvider.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ToastProvider.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/Toast/ToastProvider.tsx"],"names":[],"mappings":"AA2BA,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B,QAAQ,CAAC,EACL,aAAa,GACb,eAAe,GACf,cAAc,GACd,UAAU,GACV,YAAY,GACZ,WAAW,CAAC;CACjB,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAwFtD,CAAC"}
1
+ {"version":3,"file":"ToastProvider.d.ts","sourceRoot":"","sources":["../../../../src/components/feedback/Toast/ToastProvider.tsx"],"names":[],"mappings":"AAEA,OAAO,EAAc,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AA+BvE,YAAY,EAAE,cAAc,EAAE,CAAC;AAE/B,KAAK,aAAa,GACd,aAAa,GACb,eAAe,GACf,cAAc,GACd,UAAU,GACV,YAAY,GACZ,WAAW,CAAC;AAWhB,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IAC3B;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,SAAS,CAAC,EAAE,cAAc,CAAC;IAC3B;;OAEG;IACH,QAAQ,CAAC,EAAE,aAAa,CAAC;CAC1B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,KAAK,CAAC,EAAE,CAAC,kBAAkB,CAqLtD,CAAC"}
@@ -1,58 +1,114 @@
1
1
  "use client";
2
2
  import { jsxs, Fragment, jsx } from "react/jsx-runtime";
3
- import { useState, useRef, useCallback, useMemo } from "react";
3
+ import { classnames } from "@blockle/blocks-core";
4
+ import { useState, useRef, useCallback, useEffect, useMemo } from "react";
4
5
  import { flushSync } from "react-dom";
5
- import { Box } from "../../layout/Box/Box.js";
6
- import { Stack } from "../../layout/Stack/Stack.js";
6
+ import { useComponentStyles } from "../../../hooks/useComponentStyles/useComponentStyles.js";
7
+ import { CloseIcon } from "../../../icons/icons.js";
8
+ import { IconButton } from "../../form/IconButton/IconButton.js";
7
9
  import { Alert } from "../Alert/Alert.js";
8
- import { container } from "./Toast.css.js";
10
+ import { toast, stack, placement } from "./Toast.css.js";
9
11
  import { ToastContext } from "./ToastContext.js";
12
+ import { useCountdowns } from "./useCountdowns.js";
10
13
  function makeTransition(transition) {
11
14
  if (document.startViewTransition) {
12
- document.startViewTransition(() => {
15
+ const viewTransition = document.startViewTransition(() => {
13
16
  flushSync(() => {
14
17
  transition();
15
18
  });
16
19
  });
20
+ viewTransition.ready.catch(() => {
21
+ });
17
22
  } else {
18
23
  transition();
19
24
  }
20
25
  }
21
- const ToastProvider = ({ children }) => {
26
+ const placementByPosition = {
27
+ "bottom-left": "bottom-start",
28
+ "bottom-center": "bottom",
29
+ "bottom-right": "bottom-end",
30
+ "top-left": "top-start",
31
+ "top-center": "top",
32
+ "top-right": "top-end"
33
+ };
34
+ const ToastProvider = ({
35
+ children,
36
+ closeLabel = "Close",
37
+ placement: placementProp,
38
+ position
39
+ }) => {
40
+ const placement$1 = placementProp ?? (position ? placementByPosition[position] : "bottom");
41
+ const rootClassName = useComponentStyles("toast", {
42
+ root: true,
43
+ variants: { placement: placement$1 }
44
+ });
45
+ const toastClassName = useComponentStyles("toast", {
46
+ toast: true,
47
+ variants: { placement: placement$1 }
48
+ });
22
49
  const [toasts, setToasts] = useState([]);
23
50
  const toastsRef = useRef(toasts);
24
51
  toastsRef.current = toasts;
25
- const removeToast = useCallback((id) => {
52
+ const stackRef = useRef(null);
53
+ const hoveredIdsRef = useRef(/* @__PURE__ */ new Set());
54
+ const focusWithinRef = useRef(false);
55
+ const commitRemoval = useCallback((id) => {
26
56
  makeTransition(() => {
27
- setToasts((prev) => prev.filter((toast) => toast.id !== id));
57
+ setToasts((prev) => prev.filter((toast2) => toast2.id !== id));
28
58
  });
29
59
  }, []);
30
- const addToast = useCallback((toast) => {
31
- const toasts2 = toastsRef.current;
32
- const match = toasts2.findIndex(({ id }) => id === toast.id) !== -1;
33
- function updateState(prev) {
34
- const match2 = prev.findIndex(({ id }) => id === toast.id) !== -1;
35
- if (match2) {
36
- return prev.map((item) => item.id === toast.id ? toast : item);
60
+ const countdowns = useCountdowns(commitRemoval);
61
+ const syncPaused = useCallback(() => {
62
+ countdowns.setPaused(
63
+ hoveredIdsRef.current.size > 0 || focusWithinRef.current || document.visibilityState === "hidden"
64
+ );
65
+ }, [countdowns]);
66
+ const removeToast = useCallback(
67
+ (id) => {
68
+ countdowns.clear(id);
69
+ hoveredIdsRef.current.delete(id);
70
+ syncPaused();
71
+ commitRemoval(id);
72
+ },
73
+ [countdowns, syncPaused, commitRemoval]
74
+ );
75
+ const addToast = useCallback(
76
+ (toast2) => {
77
+ const toasts2 = toastsRef.current;
78
+ const match = toasts2.findIndex(({ id }) => id === toast2.id) !== -1;
79
+ function updateState(prev) {
80
+ const match2 = prev.findIndex(({ id }) => id === toast2.id) !== -1;
81
+ if (match2) {
82
+ return prev.map((item) => item.id === toast2.id ? toast2 : item);
83
+ }
84
+ return [...prev, toast2];
37
85
  }
38
- return [...prev, toast];
39
- }
40
- if (match) {
41
- setToasts(updateState);
42
- return;
43
- }
44
- makeTransition(() => {
45
- setToasts(updateState);
46
- });
47
- if (toast.duration) {
48
- const toastId = toast.id;
49
- setTimeout(() => {
50
- makeTransition(() => {
51
- setToasts((prev) => prev.filter((toast2) => toast2.id !== toastId));
52
- });
53
- }, toast.duration);
54
- }
55
- }, []);
86
+ syncPaused();
87
+ if (toast2.duration) {
88
+ countdowns.set(toast2.id, toast2.duration);
89
+ } else {
90
+ countdowns.clear(toast2.id);
91
+ }
92
+ if (match) {
93
+ setToasts(updateState);
94
+ return;
95
+ }
96
+ makeTransition(() => {
97
+ setToasts(updateState);
98
+ });
99
+ },
100
+ [countdowns, syncPaused]
101
+ );
102
+ useEffect(() => {
103
+ document.addEventListener("visibilitychange", syncPaused);
104
+ return () => {
105
+ document.removeEventListener("visibilitychange", syncPaused);
106
+ };
107
+ }, [syncPaused]);
108
+ useEffect(() => {
109
+ focusWithinRef.current = stackRef.current?.contains(document.activeElement) ?? false;
110
+ syncPaused();
111
+ }, [toasts, syncPaused]);
56
112
  const contextValue = useMemo(
57
113
  () => ({
58
114
  add: addToast,
@@ -63,28 +119,56 @@ const ToastProvider = ({ children }) => {
63
119
  return /* @__PURE__ */ jsxs(Fragment, { children: [
64
120
  /* @__PURE__ */ jsx(ToastContext.Provider, { value: contextValue, children }),
65
121
  /* @__PURE__ */ jsx(
66
- Box,
122
+ "div",
67
123
  {
68
- asChild: true,
69
- position: "fixed",
70
- insetBlockEnd: 0,
71
- insetInline: 0,
72
- padding: 2,
73
- children: /* @__PURE__ */ jsx(Stack, { gap: 2, children: toasts.map(({ id, children: children2, intent }) => /* @__PURE__ */ jsx(
124
+ className: classnames(
125
+ stack,
126
+ placement[placement$1],
127
+ rootClassName
128
+ ),
129
+ ref: stackRef,
130
+ children: toasts.map(({ id, children: children2, intent }) => /* @__PURE__ */ jsx(
74
131
  Alert,
75
132
  {
76
133
  intent,
77
- className: container,
134
+ className: classnames(toast, toastClassName),
78
135
  style: {
79
136
  viewTransitionName: `toast-${id}`
80
137
  },
81
- onRequestClose: () => {
82
- removeToast(id);
138
+ onPointerEnter: () => {
139
+ hoveredIdsRef.current.add(id);
140
+ syncPaused();
141
+ },
142
+ onPointerLeave: () => {
143
+ hoveredIdsRef.current.delete(id);
144
+ syncPaused();
145
+ },
146
+ onFocus: () => {
147
+ focusWithinRef.current = true;
148
+ syncPaused();
149
+ },
150
+ onBlur: (event) => {
151
+ const stack2 = stackRef.current;
152
+ focusWithinRef.current = !!stack2 && (stack2.contains(event.relatedTarget) || stack2.contains(document.activeElement));
153
+ syncPaused();
83
154
  },
155
+ endSlot: /* @__PURE__ */ jsx(
156
+ IconButton,
157
+ {
158
+ label: closeLabel,
159
+ variant: "ghost",
160
+ size: "small",
161
+ intent: "neutral",
162
+ onClick: () => {
163
+ removeToast(id);
164
+ },
165
+ children: /* @__PURE__ */ jsx(CloseIcon, {})
166
+ }
167
+ ),
84
168
  children: children2
85
169
  },
86
170
  id
87
- )) })
171
+ ))
88
172
  }
89
173
  )
90
174
  ] });