@fibery/ui-kit 1.18.0 → 1.19.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/package.json +8 -7
- package/src/back-button.tsx +1 -1
- package/src/button.tsx +1 -1
- package/src/design-system.ts +65 -17
- package/src/emoji-picker/emoji-picker-content-with-color.tsx +3 -3
- package/src/emoji-picker/primitives/category.tsx +1 -1
- package/src/emoji-picker/primitives/search.tsx +1 -1
- package/src/emoji-picker/primitives/skin-tone.tsx +1 -1
- package/src/error-alert.tsx +1 -1
- package/src/icons/Icon.tsx +2 -2
- package/src/icons/ast/BellRinging.ts +1 -1
- package/src/icons/ast/Clock.ts +8 -0
- package/src/icons/ast/ClockAlarm.ts +8 -0
- package/src/icons/ast/Copy.ts +1 -1
- package/src/icons/ast/ExtensionComments.ts +1 -1
- package/src/icons/ast/Monitor.ts +8 -0
- package/src/icons/ast/RicheditorCommentCreate.ts +1 -1
- package/src/icons/ast/Success.ts +8 -0
- package/src/icons/ast/WarningTriangle.ts +8 -0
- package/src/icons/ast/index.tsx +5 -0
- package/src/icons/react/Clock.tsx +12 -0
- package/src/icons/react/ClockAlarm.tsx +12 -0
- package/src/icons/react/Monitor.tsx +12 -0
- package/src/icons/react/Success.tsx +12 -0
- package/src/icons/react/WarningTriangle.tsx +12 -0
- package/src/icons/react/index.tsx +5 -0
- package/src/{Select → select}/custom-select-partials/clear-indicator.tsx +1 -1
- package/src/select/custom-select-partials/option.tsx +65 -0
- package/src/{Select → select}/index.tsx +39 -4
- package/src/{Select → select}/select-in-popover.tsx +4 -4
- package/src/{Select → select}/styles.ts +3 -5
- package/src/toast/primitives.tsx +26 -21
- package/src/toast/toast-action.tsx +20 -0
- package/src/toast/toast-queue.tsx +121 -0
- package/src/toast/toast.tsx +114 -0
- package/src/toast/toaster.tsx +72 -0
- package/src/Select/custom-select-partials/option.tsx +0 -92
- package/src/toast/index.tsx +0 -47
- /package/src/{Button → button}/actions-button-compact.tsx +0 -0
- /package/src/{Button → button}/actions-button.tsx +0 -0
- /package/src/{Button → button}/add-button.tsx +0 -0
- /package/src/{Button → button}/back-button.tsx +0 -0
- /package/src/{Button → button}/button-base.tsx +0 -0
- /package/src/{Button → button}/button-group.tsx +0 -0
- /package/src/{Button → button}/button.tsx +0 -0
- /package/src/{Button → button}/icon-button.tsx +0 -0
- /package/src/{Select → select}/custom-select-partials/drop-down-indicator.tsx +0 -0
- /package/src/{Select → select}/custom-select-partials/group-heading.tsx +0 -0
- /package/src/{Select → select}/custom-select-partials/menu.tsx +0 -0
- /package/src/{Select → select}/custom-select-partials/no-option-message.tsx +0 -0
- /package/src/{Select → select}/select-control-settings-context.tsx +0 -0
- /package/src/{Select → select}/select-loader.tsx +0 -0
|
@@ -22,12 +22,13 @@ import BaseCreatableSelect, {CreatableProps} from "react-select/creatable";
|
|
|
22
22
|
import WindowedSelect, {components, WindowedMenuList} from "react-windowed-select";
|
|
23
23
|
import {componentsStyles, singleLineComponentsStyle, virtualizedStyles, zIndexStyles} from "./styles";
|
|
24
24
|
import {useSelectControlSettings} from "./select-control-settings-context";
|
|
25
|
-
import {Option} from "./custom-select-partials/option";
|
|
25
|
+
import {Option, OptionSlow} from "./custom-select-partials/option";
|
|
26
26
|
import {Menu} from "./custom-select-partials/menu";
|
|
27
27
|
import {GroupHeading} from "./custom-select-partials/group-heading";
|
|
28
28
|
import {DropdownIndicator} from "./custom-select-partials/drop-down-indicator";
|
|
29
29
|
import {NoOptionsMessage} from "./custom-select-partials/no-option-message";
|
|
30
30
|
import {ClearIndicator} from "./custom-select-partials/clear-indicator";
|
|
31
|
+
import {isObject} from "lodash";
|
|
31
32
|
|
|
32
33
|
type GroupBase<T> = ReactSelectGroupBase<T> & {separator?: boolean};
|
|
33
34
|
|
|
@@ -69,7 +70,6 @@ export type SelectProps<
|
|
|
69
70
|
isCollectionMode?: IsMulti;
|
|
70
71
|
disabled?: boolean;
|
|
71
72
|
};
|
|
72
|
-
|
|
73
73
|
export function SingleRowSelect<
|
|
74
74
|
Option = unknown,
|
|
75
75
|
IsMulti extends boolean = boolean,
|
|
@@ -86,6 +86,23 @@ export function SingleRowSelect<
|
|
|
86
86
|
);
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
+
const FAST_OPTION_USAGE_THRESHOLD = 200;
|
|
90
|
+
function isGroup<Option, Group extends GroupBase<Option>>(optionOrGroup: Option | Group): optionOrGroup is Group {
|
|
91
|
+
return isObject(optionOrGroup) && "options" in optionOrGroup;
|
|
92
|
+
}
|
|
93
|
+
function countOptions<Option, Group extends GroupBase<Option>>(categorizedOptions?: OptionsOrGroups<Option, Group>) {
|
|
94
|
+
if (!categorizedOptions) {
|
|
95
|
+
return 0;
|
|
96
|
+
}
|
|
97
|
+
return categorizedOptions.reduce<number>((optionsAccumulator, categorizedOption) => {
|
|
98
|
+
if (isGroup<Option, Group>(categorizedOption)) {
|
|
99
|
+
return optionsAccumulator + categorizedOption.options.length;
|
|
100
|
+
} else {
|
|
101
|
+
return optionsAccumulator + 1;
|
|
102
|
+
}
|
|
103
|
+
}, 0);
|
|
104
|
+
}
|
|
105
|
+
|
|
89
106
|
export const Select = forwardRef(function Select<
|
|
90
107
|
Option = unknown,
|
|
91
108
|
IsMulti extends boolean = boolean,
|
|
@@ -108,6 +125,9 @@ export const Select = forwardRef(function Select<
|
|
|
108
125
|
) {
|
|
109
126
|
const {getPopupContainerElement, zIndex} = useSelectControlSettings();
|
|
110
127
|
const [menuOpenState, setMenuOpenState] = useState(rest.defaultMenuIsOpen);
|
|
128
|
+
const optionsCount = useMemo(() => {
|
|
129
|
+
return countOptions<Option, Group>(rest.options);
|
|
130
|
+
}, [rest.options]);
|
|
111
131
|
const onMenuOpenWrapped = useCallback(() => {
|
|
112
132
|
setMenuOpenState(true);
|
|
113
133
|
onMenuOpen && onMenuOpen();
|
|
@@ -176,7 +196,13 @@ export const Select = forwardRef(function Select<
|
|
|
176
196
|
Menu: Menu,
|
|
177
197
|
DropdownIndicator,
|
|
178
198
|
ClearIndicator,
|
|
179
|
-
|
|
199
|
+
/*
|
|
200
|
+
Fast Options break option focus on hover behaviour but help with slow React Select performance with large options list.
|
|
201
|
+
This isolates bug for big lists only
|
|
202
|
+
Read Option definition for more info
|
|
203
|
+
bug here: https://the.fibery.io/SoftDev/bug/Select-option-behaviour-is-incorrect-9413
|
|
204
|
+
*/
|
|
205
|
+
Option: optionsCount > FAST_OPTION_USAGE_THRESHOLD ? Option : OptionSlow,
|
|
180
206
|
MultiValueRemove: ClearIndicator,
|
|
181
207
|
NoOptionsMessage,
|
|
182
208
|
GroupHeading,
|
|
@@ -222,6 +248,9 @@ export function CreatableSelectInner<
|
|
|
222
248
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
223
249
|
ref: any
|
|
224
250
|
) {
|
|
251
|
+
const optionsCount = useMemo(() => {
|
|
252
|
+
return countOptions<Option, Group>(rest.options);
|
|
253
|
+
}, [rest.options]);
|
|
225
254
|
return (
|
|
226
255
|
<BaseCreatableSelect
|
|
227
256
|
ref={ref}
|
|
@@ -239,7 +268,13 @@ export function CreatableSelectInner<
|
|
|
239
268
|
MenuList: virtualized ? WindowedMenuList : reactSelectComponents.MenuList,
|
|
240
269
|
DropdownIndicator,
|
|
241
270
|
ClearIndicator,
|
|
242
|
-
|
|
271
|
+
/*
|
|
272
|
+
Fast Options break option focus on hover behaviour but help with slow React Select performance with large options list.
|
|
273
|
+
This isolates bug for big lists only
|
|
274
|
+
Read Option definition for more info
|
|
275
|
+
bug here: https://the.fibery.io/SoftDev/bug/Select-option-behaviour-is-incorrect-9413
|
|
276
|
+
*/
|
|
277
|
+
Option: optionsCount > FAST_OPTION_USAGE_THRESHOLD ? Option : OptionSlow,
|
|
243
278
|
MultiValueRemove: ClearIndicator,
|
|
244
279
|
NoOptionsMessage,
|
|
245
280
|
GroupHeading,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import {css, cx} from "@linaria/core";
|
|
2
|
-
import {border,
|
|
2
|
+
import {border, space, textStyles} from "../design-system";
|
|
3
3
|
import {inputOverrides} from "../antd/styles";
|
|
4
4
|
import React, {
|
|
5
5
|
ComponentType,
|
|
@@ -12,7 +12,7 @@ import React, {
|
|
|
12
12
|
useRef,
|
|
13
13
|
useState,
|
|
14
14
|
} from "react";
|
|
15
|
-
import {Popup} from "../
|
|
15
|
+
import {Popup} from "../popup";
|
|
16
16
|
import {$TSFixMe} from "../tsfixme";
|
|
17
17
|
import {
|
|
18
18
|
combineStyles,
|
|
@@ -38,7 +38,7 @@ const popupClassName = css`
|
|
|
38
38
|
`;
|
|
39
39
|
|
|
40
40
|
const valueClassName = css`
|
|
41
|
-
border-radius: ${
|
|
41
|
+
border-radius: ${border.radius4}px;
|
|
42
42
|
|
|
43
43
|
&:focus {
|
|
44
44
|
outline: none;
|
|
@@ -81,7 +81,7 @@ export const selectInPopupStyles = {
|
|
|
81
81
|
marginTop: space.s,
|
|
82
82
|
marginBottom: 0,
|
|
83
83
|
boxShadow: "none",
|
|
84
|
-
borderRadius:
|
|
84
|
+
borderRadius: border.radius4,
|
|
85
85
|
}),
|
|
86
86
|
};
|
|
87
87
|
|
|
@@ -2,7 +2,7 @@ import {css} from "@linaria/core";
|
|
|
2
2
|
import type {ControlProps, CSSObjectWithLabel, StylesConfig} from "react-select";
|
|
3
3
|
import {inputOverrides} from "../antd/styles";
|
|
4
4
|
import {layout, space, themeVars} from "../design-system";
|
|
5
|
-
import {OptionNulledStyles} from "./custom-select-partials/option";
|
|
5
|
+
import {OptionNulledStyles, OptionNulledVitualizedStyles} from "./custom-select-partials/option";
|
|
6
6
|
import {MenuNulledStyles} from "./custom-select-partials/menu";
|
|
7
7
|
import {groupHeadingNulledStyles} from "./custom-select-partials/group-heading";
|
|
8
8
|
|
|
@@ -42,7 +42,7 @@ function createControlStyle({
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
export const virtualizedStyles: StylesConfig = {
|
|
45
|
-
option: () => OptionNulledStyles,
|
|
45
|
+
option: () => ({...OptionNulledVitualizedStyles, ...OptionNulledStyles}),
|
|
46
46
|
groupHeading: (provided) => ({
|
|
47
47
|
...provided,
|
|
48
48
|
height: layout.menuItemHeight,
|
|
@@ -64,9 +64,7 @@ export const zIndexStyles = (zIndex: number): StylesConfig => ({
|
|
|
64
64
|
});
|
|
65
65
|
|
|
66
66
|
export const componentsStyles: StylesConfig = {
|
|
67
|
-
option: () =>
|
|
68
|
-
minHeight: layout.menuItemHeight,
|
|
69
|
-
}),
|
|
67
|
+
option: () => OptionNulledStyles,
|
|
70
68
|
placeholder: (provided) => ({
|
|
71
69
|
...provided,
|
|
72
70
|
color: themeVars.disabledTextColor,
|
package/src/toast/primitives.tsx
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
|
+
import {preventDefault} from "@fibery/react/src/prevent-default";
|
|
1
2
|
import {css, cx} from "@linaria/core";
|
|
2
3
|
import * as ToastPrimitives from "@radix-ui/react-toast";
|
|
3
4
|
import {ElementRef, forwardRef} from "react";
|
|
5
|
+
import {IconButton} from "../button/icon-button";
|
|
4
6
|
import {border, space, textStyles, themeVars} from "../design-system";
|
|
5
7
|
import CloseIcon from "../icons/react/Close";
|
|
6
|
-
import {IconButton} from "../Button/icon-button";
|
|
7
|
-
import {Button} from "../button";
|
|
8
|
-
import {ButtonProps} from "../Button/button";
|
|
9
|
-
import {useTheme} from "../theme-provider";
|
|
10
8
|
|
|
11
9
|
export const ToastProvider = ToastPrimitives.Provider;
|
|
12
10
|
|
|
13
11
|
const viewportCss = css`
|
|
14
12
|
position: fixed;
|
|
15
13
|
left: 50%;
|
|
16
|
-
transform: translateX(-50%);
|
|
17
14
|
bottom: 0;
|
|
15
|
+
transform: translateX(-50%);
|
|
16
|
+
|
|
17
|
+
display: flex;
|
|
18
|
+
flex-direction: column-reverse;
|
|
19
|
+
align-items: center;
|
|
20
|
+
gap: ${space.s16}px;
|
|
18
21
|
margin: 0;
|
|
19
22
|
z-index: 1010; // inherited from old toast implementation
|
|
20
|
-
padding:
|
|
23
|
+
padding: 0;
|
|
21
24
|
list-style: none;
|
|
25
|
+
|
|
26
|
+
&:not(:empty) {
|
|
27
|
+
padding: ${space.xl}px; // increased mouse hover area to pause timers of toast
|
|
28
|
+
}
|
|
22
29
|
`;
|
|
23
30
|
|
|
24
31
|
export const ToastViewport = forwardRef<
|
|
@@ -29,6 +36,8 @@ export const ToastViewport = forwardRef<
|
|
|
29
36
|
});
|
|
30
37
|
|
|
31
38
|
const rootCss = css`
|
|
39
|
+
user-select: auto !important;
|
|
40
|
+
|
|
32
41
|
background-color: ${themeVars.colorBgToastDefault};
|
|
33
42
|
padding: ${space.m}px ${space.m * 2}px;
|
|
34
43
|
border-radius: ${border.radius6}px;
|
|
@@ -89,7 +98,17 @@ const rootCss = css`
|
|
|
89
98
|
|
|
90
99
|
export const ToastRoot = forwardRef<ElementRef<typeof ToastPrimitives.Root>, ToastPrimitives.ToastProps>(
|
|
91
100
|
({className, ...props}, ref) => {
|
|
92
|
-
return
|
|
101
|
+
return (
|
|
102
|
+
<ToastPrimitives.Root
|
|
103
|
+
ref={ref}
|
|
104
|
+
className={cx(rootCss, className)}
|
|
105
|
+
onSwipeStart={preventDefault}
|
|
106
|
+
onSwipeMove={preventDefault}
|
|
107
|
+
onSwipeCancel={preventDefault}
|
|
108
|
+
onSwipeEnd={preventDefault}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
93
112
|
}
|
|
94
113
|
);
|
|
95
114
|
|
|
@@ -115,20 +134,6 @@ export const ToastSubtitle = forwardRef<
|
|
|
115
134
|
return <ToastPrimitives.Description ref={ref} className={cx(className, descriptionCss)} {...props} />;
|
|
116
135
|
});
|
|
117
136
|
|
|
118
|
-
export const ToastAction = forwardRef<
|
|
119
|
-
ElementRef<typeof ToastPrimitives.Action>,
|
|
120
|
-
ButtonProps & {altText: ToastPrimitives.ToastActionProps["altText"]}
|
|
121
|
-
>(({children, altText, ...props}, ref) => {
|
|
122
|
-
const theme = useTheme();
|
|
123
|
-
return (
|
|
124
|
-
<ToastPrimitives.Action ref={ref} altText={altText} asChild>
|
|
125
|
-
<Button color={theme.textColor} {...props}>
|
|
126
|
-
{children}
|
|
127
|
-
</Button>
|
|
128
|
-
</ToastPrimitives.Action>
|
|
129
|
-
);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
137
|
export const ToastClose = () => {
|
|
133
138
|
return (
|
|
134
139
|
<ToastPrimitives.Close aria-label="Dismiss" asChild>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import {Action as RadixToastAction, type ToastActionProps} from "@radix-ui/react-toast";
|
|
2
|
+
import {ElementRef, forwardRef} from "react";
|
|
3
|
+
import {ButtonProps} from "../button/button";
|
|
4
|
+
import {Button} from "../button";
|
|
5
|
+
import {useTheme} from "../theme-provider";
|
|
6
|
+
|
|
7
|
+
export const ToastAction = forwardRef<
|
|
8
|
+
ElementRef<typeof RadixToastAction>,
|
|
9
|
+
ButtonProps & {altText: ToastActionProps["altText"]}
|
|
10
|
+
>(({children, altText, ...props}, ref) => {
|
|
11
|
+
const theme = useTheme();
|
|
12
|
+
return (
|
|
13
|
+
<RadixToastAction ref={ref} altText={altText} asChild>
|
|
14
|
+
<Button color={theme.textColor} {...props}>
|
|
15
|
+
{children}
|
|
16
|
+
</Button>
|
|
17
|
+
</RadixToastAction>
|
|
18
|
+
);
|
|
19
|
+
});
|
|
20
|
+
export type ToastActionElement = React.ReactElement<typeof ToastAction>;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import {makePubSub} from "@fibery/helpers/utils/pub-sub";
|
|
2
|
+
import type {ToastProps} from "./toast";
|
|
3
|
+
|
|
4
|
+
type AddToastProps = Pick<ToastProps, "type" | "title" | "subTitle" | "icon" | "action" | "autoClose" | "open"> & {
|
|
5
|
+
id?: string;
|
|
6
|
+
};
|
|
7
|
+
type TypedAddProps = Omit<AddToastProps, "type">;
|
|
8
|
+
|
|
9
|
+
type QueuedToast = AddToastProps & {
|
|
10
|
+
id: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
type ToastQueueOpts = {
|
|
14
|
+
maxActiveToasts?: number;
|
|
15
|
+
};
|
|
16
|
+
export const makeToastQueue = ({maxActiveToasts = 1}: ToastQueueOpts = {}) => {
|
|
17
|
+
const pubSub = makePubSub();
|
|
18
|
+
let queue: QueuedToast[] = [];
|
|
19
|
+
let activeToasts: QueuedToast[] = [];
|
|
20
|
+
|
|
21
|
+
let count = 0;
|
|
22
|
+
|
|
23
|
+
const makeId = () => {
|
|
24
|
+
return `toast-${count++}`;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const updateActiveToasts = () => {
|
|
28
|
+
const freeSlots = maxActiveToasts - activeToasts.length;
|
|
29
|
+
|
|
30
|
+
const newToasts = queue.slice(0, freeSlots);
|
|
31
|
+
queue = queue.slice(freeSlots);
|
|
32
|
+
|
|
33
|
+
activeToasts = activeToasts.concat(newToasts);
|
|
34
|
+
|
|
35
|
+
pubSub.publish();
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const closeActive = (id: string) => {
|
|
39
|
+
activeToasts = activeToasts.map((t) => (t.id === id ? {...t, open: false} : t));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const removeFromQueue = (id: string) => {
|
|
43
|
+
queue = queue.filter((queued) => queued.id !== id);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Add toast to queue
|
|
48
|
+
*
|
|
49
|
+
* Pass custom `id` if you don't want toasts to be duplicated.
|
|
50
|
+
* If toast with same id already exists it will be closed to display fresh more relevant one
|
|
51
|
+
*/
|
|
52
|
+
const add = ({id, ...toast}: AddToastProps, {replace = true} = {}) => {
|
|
53
|
+
if (
|
|
54
|
+
id &&
|
|
55
|
+
!replace &&
|
|
56
|
+
(queue.some((queued) => queued.id === id) || activeToasts.some((active) => active.id === id))
|
|
57
|
+
) {
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (id && replace) {
|
|
62
|
+
closeActive(id);
|
|
63
|
+
removeFromQueue(id);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const t: QueuedToast = {
|
|
67
|
+
id: id || makeId(),
|
|
68
|
+
...toast,
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
queue.push(t);
|
|
72
|
+
|
|
73
|
+
updateActiveToasts();
|
|
74
|
+
|
|
75
|
+
return t.id;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
add,
|
|
80
|
+
/** close active toast, play exit animation */
|
|
81
|
+
close(id: string) {
|
|
82
|
+
closeActive(id);
|
|
83
|
+
pubSub.publish();
|
|
84
|
+
},
|
|
85
|
+
/** remove toast from active ones, should be called after exit animation */
|
|
86
|
+
remove(id: string) {
|
|
87
|
+
activeToasts = activeToasts.filter((t) => t.id !== id);
|
|
88
|
+
|
|
89
|
+
updateActiveToasts();
|
|
90
|
+
},
|
|
91
|
+
getActiveToasts() {
|
|
92
|
+
return activeToasts;
|
|
93
|
+
},
|
|
94
|
+
subscribe: pubSub.subscribe,
|
|
95
|
+
unsubscribe: pubSub.unsubscribe,
|
|
96
|
+
|
|
97
|
+
info(t: TypedAddProps) {
|
|
98
|
+
return add(t);
|
|
99
|
+
},
|
|
100
|
+
success(t: TypedAddProps) {
|
|
101
|
+
return add({
|
|
102
|
+
type: "success",
|
|
103
|
+
...t,
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
error(t: TypedAddProps) {
|
|
107
|
+
return add({
|
|
108
|
+
type: "error",
|
|
109
|
+
...t,
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
loading(t: TypedAddProps) {
|
|
113
|
+
return add({
|
|
114
|
+
type: "loading",
|
|
115
|
+
...t,
|
|
116
|
+
});
|
|
117
|
+
},
|
|
118
|
+
};
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
export type ToastQueue = ReturnType<typeof makeToastQueue>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import {useControllableState} from "@fibery/react/src/use-controllable-state";
|
|
2
|
+
import {css} from "@linaria/core";
|
|
3
|
+
import {useState} from "react";
|
|
4
|
+
import {space, themeVars} from "../design-system";
|
|
5
|
+
import Spinner from "../icons/react/Spinner";
|
|
6
|
+
import SuccessIcon from "../icons/react/Success";
|
|
7
|
+
import WarningTriangle from "../icons/react/WarningTriangle";
|
|
8
|
+
import {ToastClose, ToastRoot, ToastSubtitle, ToastTitle} from "./primitives";
|
|
9
|
+
import type {ToastActionElement} from "./toast-action";
|
|
10
|
+
|
|
11
|
+
type ToastType = "info" | "error" | "success" | "loading";
|
|
12
|
+
|
|
13
|
+
export type ToastProps = {
|
|
14
|
+
title: React.ReactNode;
|
|
15
|
+
subTitle?: React.ReactNode;
|
|
16
|
+
icon?: React.ReactNode;
|
|
17
|
+
action?: ToastActionElement;
|
|
18
|
+
autoClose?: boolean;
|
|
19
|
+
open?: boolean;
|
|
20
|
+
onOpenChange?: (open: boolean) => void;
|
|
21
|
+
|
|
22
|
+
type?: ToastType;
|
|
23
|
+
|
|
24
|
+
/** Is called on exit animation end */
|
|
25
|
+
onRemove?: () => void;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const defaultPropsPerType: Record<ToastType, Partial<ToastProps>> = {
|
|
29
|
+
info: {},
|
|
30
|
+
error: {
|
|
31
|
+
icon: <WarningTriangle color={themeVars.textColor} />,
|
|
32
|
+
autoClose: false,
|
|
33
|
+
},
|
|
34
|
+
loading: {
|
|
35
|
+
icon: <Spinner color={themeVars.textColor} />,
|
|
36
|
+
},
|
|
37
|
+
success: {
|
|
38
|
+
icon: <SuccessIcon color={themeVars.textColor} />,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const rootCss = css`
|
|
43
|
+
min-width: 320px;
|
|
44
|
+
display: flex;
|
|
45
|
+
align-items: center;
|
|
46
|
+
gap: ${space.l}px;
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const messageCss = css`
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-grow: 1;
|
|
52
|
+
flex-direction: column;
|
|
53
|
+
gap: ${space.xs}px;
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const iconCss = css`
|
|
57
|
+
grid-area: icon;
|
|
58
|
+
`;
|
|
59
|
+
|
|
60
|
+
export const Toast: React.FC<ToastProps> = (props) => {
|
|
61
|
+
const defaultProps = defaultPropsPerType[props.type || "info"];
|
|
62
|
+
|
|
63
|
+
const {
|
|
64
|
+
title,
|
|
65
|
+
subTitle,
|
|
66
|
+
icon,
|
|
67
|
+
action,
|
|
68
|
+
autoClose = true,
|
|
69
|
+
open,
|
|
70
|
+
onOpenChange,
|
|
71
|
+
onRemove,
|
|
72
|
+
} = {
|
|
73
|
+
...defaultProps,
|
|
74
|
+
...props,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const [isOpen, setOpen] = useControllableState({value: open, defaultValue: true, onChange: onOpenChange});
|
|
78
|
+
|
|
79
|
+
const [noReducedMotion] = useState(() => matchMedia("(prefers-reduced-motion: no-preference)").matches);
|
|
80
|
+
|
|
81
|
+
const close = autoClose ? null : <ToastClose />;
|
|
82
|
+
|
|
83
|
+
const onCloseProps = noReducedMotion
|
|
84
|
+
? {
|
|
85
|
+
onOpenChange: setOpen,
|
|
86
|
+
// https://github.com/radix-ui/primitives/issues/1020
|
|
87
|
+
onAnimationEndCapture() {
|
|
88
|
+
if (!isOpen) {
|
|
89
|
+
onRemove?.();
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
}
|
|
93
|
+
: {
|
|
94
|
+
onOpenChange(v: boolean) {
|
|
95
|
+
setOpen(v);
|
|
96
|
+
if (!v) {
|
|
97
|
+
onRemove?.();
|
|
98
|
+
}
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<ToastRoot open={open} duration={autoClose ? 5000 : Infinity} className={rootCss} {...onCloseProps}>
|
|
104
|
+
{icon ? <div className={iconCss}>{icon}</div> : null}
|
|
105
|
+
<div className={messageCss}>
|
|
106
|
+
<ToastTitle>{title}</ToastTitle>
|
|
107
|
+
{subTitle ? <ToastSubtitle>{subTitle}</ToastSubtitle> : null}
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{action}
|
|
111
|
+
{close}
|
|
112
|
+
</ToastRoot>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import {createContext} from "@fibery/react/src/create-context";
|
|
2
|
+
import React, {useEffect, useSyncExternalStore} from "react";
|
|
3
|
+
import {createInlineTheme} from "../create-inline-theme";
|
|
4
|
+
import {colors, getThemeColors} from "../design-system";
|
|
5
|
+
import {ThemeProvider, useThemeMode} from "../theme-provider";
|
|
6
|
+
import {ToastProvider, ToastViewport} from "./primitives";
|
|
7
|
+
import {Toast} from "./toast";
|
|
8
|
+
import type {ToastQueue} from "./toast-queue";
|
|
9
|
+
import {useCallbackRef} from "@fibery/react/src/use-callback-ref";
|
|
10
|
+
import {reactNodeToString} from "@fibery/react/src/react-node-to-string";
|
|
11
|
+
|
|
12
|
+
const ToastThemeProvider: React.FC<React.PropsWithChildren> = ({children}) => {
|
|
13
|
+
const themeMode = useThemeMode();
|
|
14
|
+
const theme = getThemeColors(colors.brandColors.blue, themeMode === "dark" ? "light" : "dark");
|
|
15
|
+
const inlineTheme = createInlineTheme(theme, []);
|
|
16
|
+
|
|
17
|
+
return (
|
|
18
|
+
<ThemeProvider theme={theme}>
|
|
19
|
+
<div style={inlineTheme}>{children}</div>
|
|
20
|
+
</ThemeProvider>
|
|
21
|
+
);
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const [Provider, useCtx] = createContext<ToastQueue>("ToastQueue");
|
|
25
|
+
|
|
26
|
+
type OnTrackToast = (args: {title: string; type: string}) => void;
|
|
27
|
+
const TrackToast: React.FC<
|
|
28
|
+
React.PropsWithChildren<{title: React.ReactNode; type: string; onTrackToast: OnTrackToast}>
|
|
29
|
+
> = ({title, type, onTrackToast, children}) => {
|
|
30
|
+
const onTrackCb = useCallbackRef(onTrackToast);
|
|
31
|
+
useEffect(() => {
|
|
32
|
+
onTrackCb({title: reactNodeToString(title), type});
|
|
33
|
+
}, [onTrackCb, title, type]);
|
|
34
|
+
|
|
35
|
+
return children as JSX.Element;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const Toaster: React.FC<React.PropsWithChildren<{toastQueue: ToastQueue; onTrackToast: OnTrackToast}>> = ({
|
|
39
|
+
toastQueue,
|
|
40
|
+
children,
|
|
41
|
+
onTrackToast,
|
|
42
|
+
}) => {
|
|
43
|
+
const toasts = useSyncExternalStore(toastQueue.subscribe, toastQueue.getActiveToasts);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<Provider value={toastQueue}>
|
|
47
|
+
<ToastProvider>
|
|
48
|
+
{children}
|
|
49
|
+
|
|
50
|
+
<ToastThemeProvider>
|
|
51
|
+
{toasts.map(({id, open = true, ...toastProps}) => (
|
|
52
|
+
<TrackToast key={id} title={toastProps.title} type={toastProps.type || "info"} onTrackToast={onTrackToast}>
|
|
53
|
+
<Toast
|
|
54
|
+
{...toastProps}
|
|
55
|
+
open={open}
|
|
56
|
+
onOpenChange={(v) => {
|
|
57
|
+
if (!v) {
|
|
58
|
+
toastQueue.close(id);
|
|
59
|
+
}
|
|
60
|
+
}}
|
|
61
|
+
onRemove={() => toastQueue.remove(id)}
|
|
62
|
+
/>
|
|
63
|
+
</TrackToast>
|
|
64
|
+
))}
|
|
65
|
+
|
|
66
|
+
<ToastViewport />
|
|
67
|
+
</ToastThemeProvider>
|
|
68
|
+
</ToastProvider>
|
|
69
|
+
</Provider>
|
|
70
|
+
);
|
|
71
|
+
};
|
|
72
|
+
export const useToast = useCtx;
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import {OptionProps} from "react-select";
|
|
2
|
-
import {components} from "react-windowed-select";
|
|
3
|
-
import {GroupBase} from "../index";
|
|
4
|
-
import {css, cx} from "@linaria/core";
|
|
5
|
-
import {border, layout, space, textStyles, themeVars, transition} from "../../design-system";
|
|
6
|
-
import {useMemo} from "react";
|
|
7
|
-
|
|
8
|
-
export const OptionNulledStyles = {
|
|
9
|
-
height: layout.menuItemHeight,
|
|
10
|
-
};
|
|
11
|
-
const OptionWrapperClass = css`
|
|
12
|
-
max-width: 100%;
|
|
13
|
-
flex-basis: 100%;
|
|
14
|
-
display: flex;
|
|
15
|
-
align-items: center;
|
|
16
|
-
align-content: center;
|
|
17
|
-
${{...textStyles.regular}};
|
|
18
|
-
color: ${themeVars.textColor};
|
|
19
|
-
background: ${themeVars.colorBgSelectOptionDefault};
|
|
20
|
-
border-radius: ${border.radius6}px;
|
|
21
|
-
padding: 0 ${space.s}px;
|
|
22
|
-
column-gap: ${space.xs}px;
|
|
23
|
-
transition: background-color ${transition}, color ${transition};
|
|
24
|
-
`;
|
|
25
|
-
const OptionRootClass = css`
|
|
26
|
-
padding: 0 ${space.s}px ${space.xxs}px;
|
|
27
|
-
display: flex;
|
|
28
|
-
align-items: stretch;
|
|
29
|
-
justify-content: stretch;
|
|
30
|
-
cursor: pointer;
|
|
31
|
-
&:hover {
|
|
32
|
-
.${OptionWrapperClass} {
|
|
33
|
-
background: ${themeVars.colorBgSelectOptionDefaultHover};
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
`;
|
|
37
|
-
|
|
38
|
-
const DisabledOptionClass = css`
|
|
39
|
-
cursor: not-allowed;
|
|
40
|
-
.${OptionWrapperClass} {
|
|
41
|
-
> * {
|
|
42
|
-
opacity: ${themeVars.opacitySelectOptionDisabled};
|
|
43
|
-
}
|
|
44
|
-
background: ${themeVars.colorBgSelectOptionDisabled};
|
|
45
|
-
}
|
|
46
|
-
&:hover {
|
|
47
|
-
.${OptionWrapperClass} {
|
|
48
|
-
background: ${themeVars.colorBgSelectOptionDisabled};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
`;
|
|
52
|
-
const SelectedOptionClass = css`
|
|
53
|
-
cursor: default;
|
|
54
|
-
.${OptionWrapperClass} {
|
|
55
|
-
background: ${themeVars.colorBgSelectOptionSelected};
|
|
56
|
-
}
|
|
57
|
-
&:hover {
|
|
58
|
-
.${OptionWrapperClass} {
|
|
59
|
-
background: ${themeVars.colorBgSelectOptionSelectedHover};
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
&.${DisabledOptionClass} {
|
|
63
|
-
background: ${themeVars.colorBgSelectOptionSelectedDisabled};
|
|
64
|
-
&:hover {
|
|
65
|
-
background: ${themeVars.colorBgSelectOptionSelectedDisabled};
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
`;
|
|
69
|
-
export function Option<Option, IsMulti extends boolean = false, Group extends GroupBase<Option> = GroupBase<Option>>(
|
|
70
|
-
props: OptionProps<Option, IsMulti, Group>
|
|
71
|
-
) {
|
|
72
|
-
const {children, ...rest} = props;
|
|
73
|
-
const {isDisabled, isFocused} = rest;
|
|
74
|
-
const className = useMemo(
|
|
75
|
-
() =>
|
|
76
|
-
cx(
|
|
77
|
-
rest.innerProps.className,
|
|
78
|
-
OptionRootClass,
|
|
79
|
-
isFocused && SelectedOptionClass, //do not use isSelected as flag to determine if item is selected. It is not always true, bug
|
|
80
|
-
isDisabled && DisabledOptionClass
|
|
81
|
-
),
|
|
82
|
-
[rest.innerProps.className, isDisabled, isFocused]
|
|
83
|
-
);
|
|
84
|
-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
85
|
-
const {onMouseMove, onMouseOver, ...truncatedProps} = rest.innerProps;
|
|
86
|
-
const newProps = Object.assign(rest, {innerProps: truncatedProps});
|
|
87
|
-
return (
|
|
88
|
-
<components.Option {...newProps} className={className}>
|
|
89
|
-
<div className={OptionWrapperClass}>{children}</div>
|
|
90
|
-
</components.Option>
|
|
91
|
-
);
|
|
92
|
-
}
|