@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.
- package/README.md +138 -0
- package/dist/components/display/Tag/Tag.d.ts +28 -0
- package/dist/components/display/Tag/Tag.d.ts.map +1 -0
- package/dist/components/display/Tag/Tag.js +46 -0
- package/dist/components/feedback/Alert/Alert.d.ts +17 -2
- package/dist/components/feedback/Alert/Alert.d.ts.map +1 -1
- package/dist/components/feedback/Alert/Alert.js +8 -35
- package/dist/components/feedback/Toast/Toast.css.d.ts +10 -1
- package/dist/components/feedback/Toast/Toast.css.d.ts.map +1 -1
- package/dist/components/feedback/Toast/Toast.css.js +37 -10
- package/dist/components/feedback/Toast/ToastContext.d.ts +6 -0
- package/dist/components/feedback/Toast/ToastContext.d.ts.map +1 -1
- package/dist/components/feedback/Toast/ToastProvider.d.ts +15 -1
- package/dist/components/feedback/Toast/ToastProvider.d.ts.map +1 -1
- package/dist/components/feedback/Toast/ToastProvider.js +128 -44
- package/dist/components/feedback/Toast/useCountdowns.d.ts +14 -0
- package/dist/components/feedback/Toast/useCountdowns.d.ts.map +1 -0
- package/dist/components/feedback/Toast/useCountdowns.js +74 -0
- package/dist/components/form/Button/Button.d.ts +11 -3
- package/dist/components/form/Button/Button.d.ts.map +1 -1
- package/dist/components/form/Checkbox/Checkbox.d.ts +0 -1
- package/dist/components/form/Checkbox/Checkbox.d.ts.map +1 -1
- package/dist/components/form/Checkbox/Checkbox.js +7 -27
- package/dist/components/form/Checkbox/checkbox.css.d.ts.map +1 -1
- package/dist/components/form/Checkbox/checkbox.css.js +1 -0
- package/dist/components/form/IconButton/IconButton.d.ts +29 -0
- package/dist/components/form/IconButton/IconButton.d.ts.map +1 -0
- package/dist/components/form/IconButton/IconButton.js +61 -0
- package/dist/components/form/NumericInput/NumericInput.d.ts +11 -1
- package/dist/components/form/NumericInput/NumericInput.d.ts.map +1 -1
- package/dist/components/form/NumericInput/NumericInput.js +52 -24
- package/dist/components/form/Radio/Radio.d.ts +0 -1
- package/dist/components/form/Radio/Radio.d.ts.map +1 -1
- package/dist/components/form/Radio/Radio.js +1 -11
- package/dist/components/form/Radio/radio.css.d.ts.map +1 -1
- package/dist/components/form/Radio/radio.css.js +1 -0
- package/dist/components/form/Select/Select.d.ts +2 -1
- package/dist/components/form/Select/Select.d.ts.map +1 -1
- package/dist/components/form/Select/Select.js +4 -24
- package/dist/components/form/Switch/Switch.d.ts +5 -1
- package/dist/components/form/Switch/Switch.d.ts.map +1 -1
- package/dist/components/form/Switch/Switch.js +19 -29
- package/dist/components/form/Switch/switch.css.d.ts.map +1 -1
- package/dist/components/form/Switch/switch.css.js +1 -0
- package/dist/components/form/TextInput/TextInput.d.ts +0 -1
- package/dist/components/form/TextInput/TextInput.d.ts.map +1 -1
- package/dist/components/form/TextInput/TextInput.js +0 -2
- package/dist/components/form/Textarea/Textarea.css.d.ts +2 -0
- package/dist/components/form/Textarea/Textarea.css.d.ts.map +1 -1
- package/dist/components/form/Textarea/Textarea.css.js +17 -1
- package/dist/components/form/Textarea/Textarea.d.ts +11 -0
- package/dist/components/form/Textarea/Textarea.d.ts.map +1 -1
- package/dist/components/form/Textarea/Textarea.js +8 -2
- package/dist/components/layout/Inline/Inline.d.ts +11 -3
- package/dist/components/layout/Inline/Inline.d.ts.map +1 -1
- package/dist/components/layout/Inline/Inline.js +5 -3
- package/dist/components/layout/Stack/Stack.d.ts +12 -8
- package/dist/components/layout/Stack/Stack.d.ts.map +1 -1
- package/dist/components/layout/Stack/Stack.js +8 -4
- package/dist/components/navigation/Link/Link.d.ts +1 -1
- package/dist/components/navigation/Link/Link.d.ts.map +1 -1
- package/dist/components/navigation/Link/Link.js +2 -2
- package/dist/components/overlay/Dialog/Dialog.d.ts +4 -1
- package/dist/components/overlay/Dialog/Dialog.d.ts.map +1 -1
- package/dist/components/overlay/Dialog/Dialog.js +19 -46
- package/dist/components/overlay/Drawer/Drawer.d.ts +9 -2
- package/dist/components/overlay/Drawer/Drawer.d.ts.map +1 -1
- package/dist/components/overlay/Drawer/Drawer.js +25 -44
- package/dist/components/overlay/Drawer/drawer.css.d.ts.map +1 -1
- package/dist/components/overlay/Drawer/drawer.css.js +4 -8
- package/dist/components/overlay/Popover/Popover.d.ts +15 -3
- package/dist/components/overlay/Popover/Popover.d.ts.map +1 -1
- package/dist/components/overlay/Popover/Popover.js +35 -11
- package/dist/components/overlay/Popover/popover-utils.d.ts +38 -1
- package/dist/components/overlay/Popover/popover-utils.d.ts.map +1 -1
- package/dist/components/overlay/Popover/popover-utils.js +92 -41
- package/dist/components/overlay/Tooltip/Tooltip.d.ts +8 -0
- package/dist/components/overlay/Tooltip/Tooltip.d.ts.map +1 -1
- package/dist/components/overlay/Tooltip/Tooltip.js +3 -2
- package/dist/components/overlay/requestCloseReason.d.ts +6 -0
- package/dist/components/overlay/requestCloseReason.d.ts.map +1 -0
- package/dist/components/overlay/useModalOverlay.d.ts +35 -0
- package/dist/components/overlay/useModalOverlay.d.ts.map +1 -0
- package/dist/components/overlay/useModalOverlay.js +85 -0
- package/dist/components/typography/Heading/Heading.d.ts +2 -1
- package/dist/components/typography/Heading/Heading.d.ts.map +1 -1
- package/dist/components/typography/Heading/Heading.js +9 -1
- package/dist/components/typography/Text/Text.d.ts +3 -2
- package/dist/components/typography/Text/Text.d.ts.map +1 -1
- package/dist/components/typography/Text/Text.js +9 -1
- package/dist/components/typography/textOverflow.css.d.ts +3 -0
- package/dist/components/typography/textOverflow.css.d.ts.map +1 -0
- package/dist/components/typography/textOverflow.css.js +32 -0
- package/dist/components/typography/textOverflow.d.ts +9 -0
- package/dist/components/typography/textOverflow.d.ts.map +1 -0
- package/dist/components/typography/textOverflow.js +10 -0
- package/dist/hooks/useClickOutside/useClickOutside.d.ts.map +1 -1
- package/dist/hooks/useClickOutside/useClickOutside.js +27 -7
- package/dist/hooks/useModalDialog/useModalDialog.d.ts +13 -0
- package/dist/hooks/useModalDialog/useModalDialog.d.ts.map +1 -0
- package/dist/hooks/useModalDialog/useModalDialog.js +39 -0
- package/dist/icons/icons.d.ts +8 -0
- package/dist/icons/icons.d.ts.map +1 -0
- package/dist/icons/icons.js +39 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/testUtils/browser/consumerStyles.css.d.ts +2 -0
- package/dist/testUtils/browser/consumerStyles.css.d.ts.map +1 -0
- package/dist/testUtils/browser/pointer.d.ts +18 -0
- package/dist/testUtils/browser/pointer.d.ts.map +1 -0
- package/dist/testUtils/testTheme.css.d.ts.map +1 -1
- package/package.json +2 -2
- package/dist/components/feedback/Alert/Alert.css.d.ts +0 -2
- package/dist/components/feedback/Alert/Alert.css.d.ts.map +0 -1
- package/dist/components/feedback/Alert/Alert.css.js +0 -15
- package/dist/components/overlay/Dialog/dialogHelper.d.ts +0 -7
- package/dist/components/overlay/Dialog/dialogHelper.d.ts.map +0 -1
- 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
|
-
|
|
7
|
-
|
|
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;
|
|
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
|
|
22
|
+
role,
|
|
23
23
|
className: classnames(containerClassName, className),
|
|
24
24
|
display: "flex",
|
|
25
25
|
alignItems: "center",
|
|
26
|
-
|
|
26
|
+
gap: 2,
|
|
27
27
|
...restProps,
|
|
28
28
|
children: [
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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":"
|
|
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
|
|
3
|
+
import { style } from "@vanilla-extract/css";
|
|
4
4
|
setFileScope("src/components/feedback/Toast/Toast.css.ts", "@blockle/blocks-react");
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
|
|
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
|
|
18
|
+
const toast = style({
|
|
10
19
|
"@layer": {
|
|
11
20
|
[layers.molecule]: {
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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":"
|
|
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 {
|
|
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 {
|
|
6
|
-
import {
|
|
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 {
|
|
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
|
|
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
|
|
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((
|
|
57
|
+
setToasts((prev) => prev.filter((toast2) => toast2.id !== id));
|
|
28
58
|
});
|
|
29
59
|
}, []);
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
122
|
+
"div",
|
|
67
123
|
{
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
134
|
+
className: classnames(toast, toastClassName),
|
|
78
135
|
style: {
|
|
79
136
|
viewTransitionName: `toast-${id}`
|
|
80
137
|
},
|
|
81
|
-
|
|
82
|
-
|
|
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
|
] });
|