@fibery/ui-kit 1.26.4 → 1.26.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +11 -7
- package/src/actions-menu/actions-menu-item.tsx +201 -0
- package/src/actions-menu/actions-menu-props.tsx +26 -0
- package/src/actions-menu/actions-menu-separator.tsx +7 -0
- package/src/actions-menu/actions-menu-sub-command-menu.tsx +189 -0
- package/src/actions-menu/actions-menu-sub-menu.tsx +57 -0
- package/src/actions-menu/actions-menu.tsx +66 -0
- package/src/actions-menu/async-actions-menu-item.tsx +33 -0
- package/src/actions-menu/context-actions-menu.tsx +33 -0
- package/src/actions-menu/contexts/actions-menu-context.tsx +38 -0
- package/src/actions-menu/contexts/actions-menu-dangerous-rows.tsx +31 -0
- package/src/actions-menu/index.tsx +19 -0
- package/src/actions-menu/utils/insert-separators.tsx +28 -0
- package/src/dropdown-menu/index.tsx +169 -0
- package/src/error-alert/error-alert-action.tsx +9 -0
- package/src/error-alert/error-alert.tsx +59 -0
- package/src/lists/actions-menu-row-surface.tsx +144 -0
- package/src/lists/list-row-surface.tsx +3 -132
- package/src/select/custom-select-partials/menu-list-virtualized.tsx +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fibery/ui-kit",
|
|
3
|
-
"version": "1.26.
|
|
3
|
+
"version": "1.26.6",
|
|
4
4
|
"private": false,
|
|
5
5
|
"files": [
|
|
6
6
|
"src/antd/styles.ts",
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"src/loading-sausage.tsx",
|
|
18
18
|
"src/icons/**/*.ts*",
|
|
19
19
|
"src/select",
|
|
20
|
-
"src/error-alert
|
|
20
|
+
"src/error-alert",
|
|
21
|
+
"src/actions-menu",
|
|
21
22
|
"src/pallete.ts",
|
|
22
23
|
"src/tooltip.tsx",
|
|
23
24
|
"src/tooltip-if-overflown.tsx",
|
|
@@ -27,7 +28,10 @@
|
|
|
27
28
|
"src/toast/**/*.ts*",
|
|
28
29
|
"src/lists/**/*.ts*",
|
|
29
30
|
"src/a11y-color.ts",
|
|
30
|
-
"src/integration-compact-info-button.tsx"
|
|
31
|
+
"src/integration-compact-info-button.tsx",
|
|
32
|
+
"src/dropdown-menu",
|
|
33
|
+
"src/shortcut-badge",
|
|
34
|
+
"src/tsfixme"
|
|
31
35
|
],
|
|
32
36
|
"license": "UNLICENSED",
|
|
33
37
|
"dependencies": {
|
|
@@ -68,9 +72,9 @@
|
|
|
68
72
|
"react-virtuoso": "4.6.0",
|
|
69
73
|
"screenfull": "6.0.1",
|
|
70
74
|
"ua-parser-js": "0.7.24",
|
|
71
|
-
"@fibery/emoji-data": "2.4.0",
|
|
72
75
|
"@fibery/helpers": "1.2.0",
|
|
73
|
-
"@fibery/react": "1.3.0"
|
|
76
|
+
"@fibery/react": "1.3.0",
|
|
77
|
+
"@fibery/emoji-data": "2.4.0"
|
|
74
78
|
},
|
|
75
79
|
"peerDependencies": {
|
|
76
80
|
"react": "^18.2.0",
|
|
@@ -103,8 +107,8 @@
|
|
|
103
107
|
"svgo": "2.8.0",
|
|
104
108
|
"typescript": "5.1.6",
|
|
105
109
|
"unist-util-reduce": "0.2.2",
|
|
106
|
-
"@fibery/
|
|
107
|
-
"@fibery/
|
|
110
|
+
"@fibery/eslint-config": "8.5.1",
|
|
111
|
+
"@fibery/babel-preset": "7.4.0"
|
|
108
112
|
},
|
|
109
113
|
"jest": {
|
|
110
114
|
"testEnvironment": "jsdom",
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import {stopPropagation} from "@fibery/react/src/stop-propagation";
|
|
2
|
+
import {useCallbackRef} from "@fibery/react/src/use-callback-ref";
|
|
3
|
+
import CheckedIcon from "../icons/react/Checked";
|
|
4
|
+
import {css, cx} from "@linaria/core";
|
|
5
|
+
import {FunctionComponent, memo, ReactNode, useId} from "react";
|
|
6
|
+
import {border, colors, layout, lineHeight, space, textStyles, themeVars} from "../design-system";
|
|
7
|
+
import {IconBaseProps} from "../icons/types";
|
|
8
|
+
import {Spinner} from "../loaders";
|
|
9
|
+
import {ShortcutBadge} from "../shortcut-badge";
|
|
10
|
+
import {useTheme} from "../theme-provider";
|
|
11
|
+
import {useActiveDangerousRow, useSetActiveDangerousRow} from "./contexts/actions-menu-dangerous-rows";
|
|
12
|
+
import {useActionsMenuContext} from "./contexts/actions-menu-context";
|
|
13
|
+
|
|
14
|
+
const hiddenTextCss = css`
|
|
15
|
+
visibility: hidden;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
height: 0;
|
|
18
|
+
`;
|
|
19
|
+
const visibleTextCss = css`
|
|
20
|
+
visibility: visible;
|
|
21
|
+
height: unset;
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const descriptionCss = css`
|
|
25
|
+
${textStyles.small};
|
|
26
|
+
line-height: ${lineHeight.nowrap};
|
|
27
|
+
color: ${themeVars.accentTextColor};
|
|
28
|
+
padding-top: ${space.s2}px;
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const rowCss = css`
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
min-height: ${layout.menuItemHeight}px;
|
|
35
|
+
gap: ${space.s6}px;
|
|
36
|
+
`;
|
|
37
|
+
|
|
38
|
+
const dangerousItemCss = css`
|
|
39
|
+
&.${rowCss} {
|
|
40
|
+
&[data-highlighted] {
|
|
41
|
+
color: ${themeVars.danger};
|
|
42
|
+
background-color: ${themeVars.colorBgActionsMenuItemDangerHover};
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
`;
|
|
46
|
+
|
|
47
|
+
const dangerousConfirmationCss = css`
|
|
48
|
+
&.${rowCss} {
|
|
49
|
+
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
50
|
+
color: ${colors.inversedTextColor};
|
|
51
|
+
border-radius: ${border.radius6}px;
|
|
52
|
+
|
|
53
|
+
&:hover,
|
|
54
|
+
&[data-highlighted] {
|
|
55
|
+
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
56
|
+
color: ${colors.inversedTextColor};
|
|
57
|
+
border-radius: ${border.radius6}px;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
`;
|
|
61
|
+
|
|
62
|
+
const rowWithDescriptionCss = css`
|
|
63
|
+
&.${rowCss} {
|
|
64
|
+
min-height: ${layout.itemWithSubtitleHeight}px;
|
|
65
|
+
}
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const rowContentCss = css`
|
|
69
|
+
flex-grow: 1;
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
const cornerCss = css`
|
|
73
|
+
display: flex;
|
|
74
|
+
align-items: center;
|
|
75
|
+
column-gap: ${space.s12}px;
|
|
76
|
+
`;
|
|
77
|
+
|
|
78
|
+
const firstLetterToLowerCase = (str: string) => {
|
|
79
|
+
return str.charAt(0).toLowerCase() + str.slice(1);
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const RightCorner = ({shortcut, loading, checked}: {shortcut?: string; loading?: boolean; checked?: boolean}) => {
|
|
83
|
+
const theme = useTheme();
|
|
84
|
+
|
|
85
|
+
return loading || shortcut || checked ? (
|
|
86
|
+
<div className={cornerCss}>
|
|
87
|
+
{loading ? <Spinner color={theme.primary} size={16} containerSize={16} /> : null}
|
|
88
|
+
{shortcut ? <ShortcutBadge shortcut={shortcut} /> : null}
|
|
89
|
+
{checked ? <CheckedIcon iconSize={18} color={themeVars.textColor} /> : null}
|
|
90
|
+
</div>
|
|
91
|
+
) : null;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const LeftCorner = ({Icon}: {Icon: FunctionComponent<IconBaseProps> | void}) => {
|
|
95
|
+
return Icon ? (
|
|
96
|
+
<div className={cornerCss}>
|
|
97
|
+
<Icon color={themeVars.iconColor} iconSize={18} />
|
|
98
|
+
</div>
|
|
99
|
+
) : null;
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
export type ActionsMenuItemProps = {
|
|
103
|
+
title?: string;
|
|
104
|
+
/** Event handler called when the user selects an item (via mouse of keyboard).
|
|
105
|
+
* Calling event.preventDefault in this handler will prevent the dropdown menu from closing when selecting that item.
|
|
106
|
+
* */
|
|
107
|
+
onSelect?: (e: Event) => void;
|
|
108
|
+
testId?: string;
|
|
109
|
+
description?: string;
|
|
110
|
+
/** Icon's component */
|
|
111
|
+
Icon?: FunctionComponent<IconBaseProps>;
|
|
112
|
+
/** Hint for keyboard shortcut for this action */
|
|
113
|
+
shortcut?: string;
|
|
114
|
+
disabled?: boolean;
|
|
115
|
+
/** Whether the action requires confirmation */
|
|
116
|
+
dangerous?: boolean;
|
|
117
|
+
loading?: boolean;
|
|
118
|
+
checked?: boolean;
|
|
119
|
+
children: ReactNode;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const ActionsMenuItemComponent: React.FC<
|
|
123
|
+
ActionsMenuItemProps & {id?: string; showWarning?: boolean; onSelect: (e: Event) => void}
|
|
124
|
+
> = memo(function ActionsMenuItemComponent({
|
|
125
|
+
id,
|
|
126
|
+
testId,
|
|
127
|
+
title,
|
|
128
|
+
onSelect,
|
|
129
|
+
description,
|
|
130
|
+
Icon,
|
|
131
|
+
shortcut,
|
|
132
|
+
disabled,
|
|
133
|
+
dangerous,
|
|
134
|
+
showWarning,
|
|
135
|
+
loading,
|
|
136
|
+
checked,
|
|
137
|
+
children,
|
|
138
|
+
}) {
|
|
139
|
+
const {MenuPrimitive} = useActionsMenuContext();
|
|
140
|
+
const setActiveDangerousRow = useSetActiveDangerousRow();
|
|
141
|
+
const handleSelect = async (e: Event) => {
|
|
142
|
+
e.stopPropagation();
|
|
143
|
+
|
|
144
|
+
if (loading || !id) {
|
|
145
|
+
e.preventDefault();
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (dangerous && !showWarning) {
|
|
150
|
+
e.preventDefault();
|
|
151
|
+
setActiveDangerousRow(id);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
setActiveDangerousRow(null);
|
|
156
|
+
|
|
157
|
+
onSelect(e);
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
return (
|
|
161
|
+
<MenuPrimitive.Item
|
|
162
|
+
data-testid={testId}
|
|
163
|
+
onClick={stopPropagation}
|
|
164
|
+
onSelect={handleSelect}
|
|
165
|
+
title={title ? title : ""}
|
|
166
|
+
className={cx(
|
|
167
|
+
rowCss,
|
|
168
|
+
dangerous && dangerousItemCss,
|
|
169
|
+
showWarning && dangerousConfirmationCss,
|
|
170
|
+
description && rowWithDescriptionCss
|
|
171
|
+
)}
|
|
172
|
+
disabled={disabled}
|
|
173
|
+
>
|
|
174
|
+
<LeftCorner Icon={Icon} />
|
|
175
|
+
<div className={rowContentCss}>
|
|
176
|
+
{dangerous && (
|
|
177
|
+
<div className={cx(hiddenTextCss, showWarning && visibleTextCss)}>
|
|
178
|
+
Really {typeof children === "string" ? firstLetterToLowerCase(children) : children}?
|
|
179
|
+
</div>
|
|
180
|
+
)}
|
|
181
|
+
|
|
182
|
+
<div className={cx(showWarning && hiddenTextCss)}>{children}</div>
|
|
183
|
+
|
|
184
|
+
{description && <div className={descriptionCss}>{description}</div>}
|
|
185
|
+
</div>
|
|
186
|
+
<RightCorner shortcut={shortcut} loading={loading} checked={checked} />
|
|
187
|
+
</MenuPrimitive.Item>
|
|
188
|
+
);
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
192
|
+
const noop = () => {};
|
|
193
|
+
|
|
194
|
+
export const ActionsMenuItem: React.FC<ActionsMenuItemProps> = (props) => {
|
|
195
|
+
const id = useId();
|
|
196
|
+
const activeDangerousRow = useActiveDangerousRow();
|
|
197
|
+
|
|
198
|
+
const onSelect = useCallbackRef(props.onSelect || noop);
|
|
199
|
+
|
|
200
|
+
return <ActionsMenuItemComponent {...props} id={id} showWarning={activeDangerousRow === id} onSelect={onSelect} />;
|
|
201
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import {ReactNode} from "react";
|
|
2
|
+
import {ContentProps} from "../dropdown-menu";
|
|
3
|
+
import {LinariaClassName} from "@linaria/core";
|
|
4
|
+
import {DropdownMenuContentProps} from "@radix-ui/react-dropdown-menu";
|
|
5
|
+
|
|
6
|
+
export type ActionsMenuProps = {
|
|
7
|
+
/** Clicking on this node will open actions menu. Make sure that trigger forwards ref AND props to html element */
|
|
8
|
+
trigger: ReactNode;
|
|
9
|
+
children: ReactNode;
|
|
10
|
+
align?: ContentProps["align"];
|
|
11
|
+
disabled?: boolean;
|
|
12
|
+
portalled?: boolean;
|
|
13
|
+
wide?: boolean;
|
|
14
|
+
/** The controlled open state of the ActionsMenu. Must be used in conjunction with onOpenChange. */
|
|
15
|
+
open?: boolean;
|
|
16
|
+
/** Event handler called when the open state of the ActionsMenu changes. */
|
|
17
|
+
onOpenChange?: (state: boolean) => unknown;
|
|
18
|
+
contentStyles?: LinariaClassName;
|
|
19
|
+
modal?: boolean;
|
|
20
|
+
alignOffset?: number;
|
|
21
|
+
side?: ContentProps["side"];
|
|
22
|
+
sideOffset?: number;
|
|
23
|
+
collisionPadding?: number;
|
|
24
|
+
autoFocusOnClose?: boolean;
|
|
25
|
+
sticky?: DropdownMenuContentProps["sticky"];
|
|
26
|
+
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import {$TSFixMe} from "../tsfixme";
|
|
2
|
+
import {useActionsMenuContext} from "./contexts/actions-menu-context";
|
|
3
|
+
|
|
4
|
+
export const ActionsMenuSeparator = (props: $TSFixMe) => {
|
|
5
|
+
const {MenuPrimitive} = useActionsMenuContext();
|
|
6
|
+
return <MenuPrimitive.Separator {...props} />;
|
|
7
|
+
};
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {css, cx} from "@linaria/core";
|
|
2
|
+
import {CommandInput, CommandItem, CommandList, CommandRoot, CommandEmpty} from "cmdk";
|
|
3
|
+
import {border, layout, space, textStyles, themeVars} from "../design-system";
|
|
4
|
+
import Search from "../icons/react/Search";
|
|
5
|
+
import {useActionsMenuContext} from "./contexts/actions-menu-context";
|
|
6
|
+
import {ActionsMenuSubMenu} from "./actions-menu-sub-menu";
|
|
7
|
+
import {styled} from "@linaria/react";
|
|
8
|
+
import {useRef, useState} from "react";
|
|
9
|
+
import {stopPropagation} from "@fibery/react/src/stop-propagation";
|
|
10
|
+
/**
|
|
11
|
+
*
|
|
12
|
+
* Probably commands menu deserves to be extracted to a separate primitive component not connected to ActionsMenu
|
|
13
|
+
* but for now we don't have other use cases for it so no idea how a primitive should look like
|
|
14
|
+
*
|
|
15
|
+
* Feel free to extract it to a separate one
|
|
16
|
+
*
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
const inputContainerCss = css`
|
|
20
|
+
display: flex;
|
|
21
|
+
padding: ${space.s8}px ${space.s12}px;
|
|
22
|
+
align-items: center;
|
|
23
|
+
border-bottom: 1px solid ${themeVars.separatorColor};
|
|
24
|
+
|
|
25
|
+
& input {
|
|
26
|
+
min-width: 120px;
|
|
27
|
+
width: 100%;
|
|
28
|
+
color: ${themeVars.textColor};
|
|
29
|
+
background-color: ${themeVars.transparent};
|
|
30
|
+
margin-left: ${space.s8}px;
|
|
31
|
+
font-size: 13px;
|
|
32
|
+
border: none;
|
|
33
|
+
outline: none;
|
|
34
|
+
resize: none;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
& input::placeholder {
|
|
38
|
+
color: ${themeVars.accentTextColor};
|
|
39
|
+
}
|
|
40
|
+
`;
|
|
41
|
+
|
|
42
|
+
type ActionsMenuSubCommandMenuProps = React.PropsWithChildren<{
|
|
43
|
+
trigger?: React.ReactNode;
|
|
44
|
+
disabled?: boolean;
|
|
45
|
+
filter?: React.ComponentPropsWithoutRef<typeof CommandRoot>["filter"];
|
|
46
|
+
}>;
|
|
47
|
+
export const ActionsMenuSubCommandMenu: React.FC<ActionsMenuSubCommandMenuProps> = ({
|
|
48
|
+
disabled,
|
|
49
|
+
trigger,
|
|
50
|
+
filter,
|
|
51
|
+
children,
|
|
52
|
+
}) => {
|
|
53
|
+
const [open, onOpenChange] = useState(false);
|
|
54
|
+
const triggerRef = useRef(null);
|
|
55
|
+
const openRef = useRef(false);
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<ActionsMenuSubMenu
|
|
59
|
+
disabled={disabled}
|
|
60
|
+
open={open}
|
|
61
|
+
onOpenChange={(v) => {
|
|
62
|
+
openRef.current = v;
|
|
63
|
+
onOpenChange(v);
|
|
64
|
+
}}
|
|
65
|
+
trigger={trigger}
|
|
66
|
+
triggerRef={triggerRef}
|
|
67
|
+
>
|
|
68
|
+
<CommandRoot
|
|
69
|
+
filter={filter}
|
|
70
|
+
onClick={stopPropagation}
|
|
71
|
+
onBlur={(e) => {
|
|
72
|
+
/**
|
|
73
|
+
* Submenu Trigger tries to steal the focus from input on every onPointerMove
|
|
74
|
+
* to avoid this we lock focus on input and do not let it move to back to triger while menu is still open
|
|
75
|
+
*
|
|
76
|
+
* We use openRef here because react state will be updated only on next render
|
|
77
|
+
*/
|
|
78
|
+
if (openRef.current && e.target.hasAttribute("cmdk-input") && e.relatedTarget === triggerRef.current) {
|
|
79
|
+
e.target.focus();
|
|
80
|
+
}
|
|
81
|
+
}}
|
|
82
|
+
>
|
|
83
|
+
{children}
|
|
84
|
+
</CommandRoot>
|
|
85
|
+
</ActionsMenuSubMenu>
|
|
86
|
+
);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
type ActionsMenuSubCommandMenuInputProps = {
|
|
90
|
+
placeholder?: string;
|
|
91
|
+
autoFocus?: boolean;
|
|
92
|
+
};
|
|
93
|
+
export const ActionsMenuSubCommandMenuInput: React.FC<ActionsMenuSubCommandMenuInputProps> = ({
|
|
94
|
+
placeholder,
|
|
95
|
+
autoFocus = true,
|
|
96
|
+
}) => {
|
|
97
|
+
return (
|
|
98
|
+
<div className={inputContainerCss}>
|
|
99
|
+
<Search iconSize={16} aria-hidden />
|
|
100
|
+
<CommandInput
|
|
101
|
+
onKeyDown={(e) => {
|
|
102
|
+
// prevent submenu from closing if input is not empty
|
|
103
|
+
if ((e.target as HTMLInputElement).value.length > 0) {
|
|
104
|
+
e.stopPropagation();
|
|
105
|
+
}
|
|
106
|
+
}}
|
|
107
|
+
placeholder={placeholder}
|
|
108
|
+
autoFocus={autoFocus}
|
|
109
|
+
/>
|
|
110
|
+
</div>
|
|
111
|
+
);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
export const ActionsMenuSubCommandMenuList = styled(CommandList)`
|
|
115
|
+
overflow-y: scroll;
|
|
116
|
+
width: 250px;
|
|
117
|
+
height: var(--cmdk-list-height);
|
|
118
|
+
max-height: 600px;
|
|
119
|
+
transition: height 100ms ease;
|
|
120
|
+
margin-top: ${space.s4}px;
|
|
121
|
+
`;
|
|
122
|
+
|
|
123
|
+
const commandItemCss = css`
|
|
124
|
+
display: flex;
|
|
125
|
+
align-items: center;
|
|
126
|
+
gap: ${space.s6}px;
|
|
127
|
+
padding: 0 ${space.s12}px;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
min-height: ${layout.menuItemHeight}px;
|
|
130
|
+
margin: 0 ${space.s4}px;
|
|
131
|
+
|
|
132
|
+
&[data-selected] {
|
|
133
|
+
margin: 0 4px;
|
|
134
|
+
background-color: ${themeVars.colorBgActionsMenuItemHover};
|
|
135
|
+
border-radius: ${border.radius6}px;
|
|
136
|
+
}
|
|
137
|
+
`;
|
|
138
|
+
const commandItemTitleCss = css`
|
|
139
|
+
white-space: nowrap;
|
|
140
|
+
text-overflow: ellipsis;
|
|
141
|
+
overflow: hidden;
|
|
142
|
+
`;
|
|
143
|
+
|
|
144
|
+
type ActionsMenuSubCommandMenuItemProps = React.ComponentPropsWithoutRef<typeof CommandItem> & {
|
|
145
|
+
icon?: React.ReactNode;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
export const ActionsMenuSubCommandMenuItem: React.FC<ActionsMenuSubCommandMenuItemProps> = ({
|
|
149
|
+
onSelect,
|
|
150
|
+
icon,
|
|
151
|
+
children,
|
|
152
|
+
className,
|
|
153
|
+
...props
|
|
154
|
+
}) => {
|
|
155
|
+
const {close} = useActionsMenuContext();
|
|
156
|
+
const [showTitle, setShowTitle] = useState(false);
|
|
157
|
+
|
|
158
|
+
const onSelectItem = (value: string) => {
|
|
159
|
+
onSelect?.(value);
|
|
160
|
+
close();
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<CommandItem className={cx(commandItemCss, className)} onSelect={onSelectItem} {...props}>
|
|
165
|
+
{icon}
|
|
166
|
+
<span
|
|
167
|
+
title={showTitle ? (children as string) : ""}
|
|
168
|
+
onMouseEnter={(e) => {
|
|
169
|
+
const target = e.target as HTMLElement;
|
|
170
|
+
if (typeof children === "string" && target.offsetWidth < target.scrollWidth) {
|
|
171
|
+
setShowTitle(true);
|
|
172
|
+
}
|
|
173
|
+
}}
|
|
174
|
+
className={commandItemTitleCss}
|
|
175
|
+
>
|
|
176
|
+
{children}
|
|
177
|
+
</span>
|
|
178
|
+
</CommandItem>
|
|
179
|
+
);
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
export const ActionsMenuSubCommandMenuEmpty = styled(CommandEmpty)`
|
|
183
|
+
${textStyles.small}
|
|
184
|
+
display: flex;
|
|
185
|
+
align-items: center;
|
|
186
|
+
padding: 0 ${space.s12}px;
|
|
187
|
+
min-height: ${layout.itemHeight}px;
|
|
188
|
+
color: ${themeVars.accentTextColor};
|
|
189
|
+
`;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import {stopPropagation} from "@fibery/react/src/stop-propagation";
|
|
2
|
+
import {css} from "@linaria/core";
|
|
3
|
+
import {ReactNode} from "react";
|
|
4
|
+
import {space, themeVars} from "../../src/design-system";
|
|
5
|
+
import ArrowRight from "../icons/react/ArrowRight";
|
|
6
|
+
import {useActionsMenuContext} from "./contexts/actions-menu-context";
|
|
7
|
+
|
|
8
|
+
const subTriggerClass = css`
|
|
9
|
+
display: flex;
|
|
10
|
+
justify-content: space-between;
|
|
11
|
+
align-items: center;
|
|
12
|
+
gap: ${space.s8}px;
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
const arrowClass = css`
|
|
16
|
+
flex-grow: 0;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
type Props = React.PropsWithChildren<{
|
|
20
|
+
trigger: ReactNode;
|
|
21
|
+
triggerRef?: React.RefObject<HTMLDivElement>;
|
|
22
|
+
open?: boolean;
|
|
23
|
+
onOpenChange?: (v: boolean) => void;
|
|
24
|
+
disabled?: boolean;
|
|
25
|
+
}>;
|
|
26
|
+
|
|
27
|
+
export const ActionsMenuSubMenu: React.FC<Props> = ({
|
|
28
|
+
open,
|
|
29
|
+
onOpenChange,
|
|
30
|
+
trigger,
|
|
31
|
+
triggerRef,
|
|
32
|
+
children,
|
|
33
|
+
disabled = false,
|
|
34
|
+
}) => {
|
|
35
|
+
const {MenuPrimitive} = useActionsMenuContext();
|
|
36
|
+
|
|
37
|
+
return (
|
|
38
|
+
<MenuPrimitive.Sub open={open} onOpenChange={onOpenChange}>
|
|
39
|
+
<MenuPrimitive.SubTrigger
|
|
40
|
+
ref={triggerRef}
|
|
41
|
+
className={subTriggerClass}
|
|
42
|
+
disabled={disabled}
|
|
43
|
+
onClick={stopPropagation}
|
|
44
|
+
>
|
|
45
|
+
{trigger}
|
|
46
|
+
<div className={arrowClass}>
|
|
47
|
+
<ArrowRight
|
|
48
|
+
color={disabled ? themeVars.disabledTextColor : themeVars.iconColor}
|
|
49
|
+
containerSize={10}
|
|
50
|
+
aria-hidden
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
</MenuPrimitive.SubTrigger>
|
|
54
|
+
<MenuPrimitive.SubContent>{children}</MenuPrimitive.SubContent>
|
|
55
|
+
</MenuPrimitive.Sub>
|
|
56
|
+
);
|
|
57
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import {useControllableState} from "@fibery/react/src/use-controllable-state";
|
|
2
|
+
import {css, cx} from "@linaria/core";
|
|
3
|
+
import * as DropdownMenu from "../dropdown-menu";
|
|
4
|
+
import {ActionsMenuContextProvider} from "./contexts/actions-menu-context";
|
|
5
|
+
import {ActionsMenuDangerousRowsProvider} from "./contexts/actions-menu-dangerous-rows";
|
|
6
|
+
import {ActionsMenuProps} from "./actions-menu-props";
|
|
7
|
+
import {preventDefault} from "@fibery/react/src/prevent-default";
|
|
8
|
+
|
|
9
|
+
export const preventDefaultAndStopPropagation = (event: React.MouseEvent) => {
|
|
10
|
+
event.preventDefault();
|
|
11
|
+
event.stopPropagation();
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const wideCss = css`
|
|
15
|
+
min-width: 320px;
|
|
16
|
+
`;
|
|
17
|
+
|
|
18
|
+
export const ActionsMenu = ({
|
|
19
|
+
trigger,
|
|
20
|
+
wide,
|
|
21
|
+
align = "start",
|
|
22
|
+
alignOffset,
|
|
23
|
+
side,
|
|
24
|
+
sideOffset,
|
|
25
|
+
collisionPadding,
|
|
26
|
+
open,
|
|
27
|
+
onOpenChange,
|
|
28
|
+
children,
|
|
29
|
+
portalled,
|
|
30
|
+
disabled,
|
|
31
|
+
contentStyles,
|
|
32
|
+
modal,
|
|
33
|
+
autoFocusOnClose,
|
|
34
|
+
sticky,
|
|
35
|
+
}: ActionsMenuProps): JSX.Element => {
|
|
36
|
+
const [isOpen = false, setOpen] = useControllableState({value: open, onChange: onOpenChange});
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<ActionsMenuContextProvider setOpen={setOpen} MenuPrimitive={DropdownMenu}>
|
|
40
|
+
<DropdownMenu.Root open={isOpen} onOpenChange={setOpen} modal={modal || false}>
|
|
41
|
+
<DropdownMenu.Trigger
|
|
42
|
+
// backward compatibility for dropdown styles
|
|
43
|
+
className={isOpen ? "actions-visible" : "actions-hidden"}
|
|
44
|
+
onClick={preventDefaultAndStopPropagation}
|
|
45
|
+
disabled={disabled}
|
|
46
|
+
asChild
|
|
47
|
+
>
|
|
48
|
+
{trigger}
|
|
49
|
+
</DropdownMenu.Trigger>
|
|
50
|
+
<DropdownMenu.Content
|
|
51
|
+
align={align}
|
|
52
|
+
sticky={sticky}
|
|
53
|
+
alignOffset={alignOffset}
|
|
54
|
+
side={side}
|
|
55
|
+
sideOffset={sideOffset}
|
|
56
|
+
className={cx(wide && wideCss, contentStyles)}
|
|
57
|
+
portalled={portalled}
|
|
58
|
+
collisionPadding={collisionPadding}
|
|
59
|
+
onCloseAutoFocus={autoFocusOnClose ? undefined : preventDefault}
|
|
60
|
+
>
|
|
61
|
+
<ActionsMenuDangerousRowsProvider open={isOpen}>{children}</ActionsMenuDangerousRowsProvider>
|
|
62
|
+
</DropdownMenu.Content>
|
|
63
|
+
</DropdownMenu.Root>
|
|
64
|
+
</ActionsMenuContextProvider>
|
|
65
|
+
);
|
|
66
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import {useState} from "react";
|
|
2
|
+
import {ActionsMenuItem, ActionsMenuItemProps} from "./actions-menu-item";
|
|
3
|
+
import {useActionsMenuContext} from "./contexts/actions-menu-context";
|
|
4
|
+
|
|
5
|
+
type AsyncActionsMenuItemProps = ActionsMenuItemProps & {
|
|
6
|
+
inProgressTitle?: string;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
export const AsyncActionsMenuItem = (props: AsyncActionsMenuItemProps) => {
|
|
10
|
+
const ctx = useActionsMenuContext();
|
|
11
|
+
const [inProgress, setInProgress] = useState(false);
|
|
12
|
+
|
|
13
|
+
const inProgressTitle = props.inProgressTitle || props.children;
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<ActionsMenuItem
|
|
17
|
+
{...props}
|
|
18
|
+
loading={inProgress}
|
|
19
|
+
onSelect={async (e) => {
|
|
20
|
+
try {
|
|
21
|
+
e.preventDefault();
|
|
22
|
+
setInProgress(true);
|
|
23
|
+
await props.onSelect?.(e);
|
|
24
|
+
} finally {
|
|
25
|
+
setInProgress(false);
|
|
26
|
+
ctx.close();
|
|
27
|
+
}
|
|
28
|
+
}}
|
|
29
|
+
>
|
|
30
|
+
{inProgress ? inProgressTitle : props.children}
|
|
31
|
+
</ActionsMenuItem>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import _ from "lodash";
|
|
2
|
+
import {useControllableState} from "@fibery/react/src/use-controllable-state";
|
|
3
|
+
import {ActionsMenuContextProvider} from "./contexts/actions-menu-context";
|
|
4
|
+
import * as ContextMenu from "../context-menu";
|
|
5
|
+
import {ActionsMenuDangerousRowsProvider} from "./contexts/actions-menu-dangerous-rows";
|
|
6
|
+
import {ActionsMenuProps} from "./actions-menu-props";
|
|
7
|
+
import {preventDefault} from "@fibery/react/src/prevent-default";
|
|
8
|
+
|
|
9
|
+
export const ContextActionsMenu = ({
|
|
10
|
+
open,
|
|
11
|
+
onOpenChange = _.noop,
|
|
12
|
+
trigger,
|
|
13
|
+
children,
|
|
14
|
+
disabled,
|
|
15
|
+
autoFocusOnClose = true,
|
|
16
|
+
}: ActionsMenuProps): JSX.Element => {
|
|
17
|
+
const [isOpen = false, setOpen] = useControllableState({value: open, onChange: onOpenChange});
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<ActionsMenuContextProvider setOpen={setOpen} MenuPrimitive={ContextMenu}>
|
|
21
|
+
<ContextMenu.Root onOpenChange={onOpenChange}>
|
|
22
|
+
<ContextMenu.Trigger disabled={disabled} asChild>
|
|
23
|
+
{trigger}
|
|
24
|
+
</ContextMenu.Trigger>
|
|
25
|
+
<ContextMenu.Portal>
|
|
26
|
+
<ContextMenu.Content onCloseAutoFocus={autoFocusOnClose ? undefined : preventDefault}>
|
|
27
|
+
<ActionsMenuDangerousRowsProvider open={isOpen}>{children}</ActionsMenuDangerousRowsProvider>
|
|
28
|
+
</ContextMenu.Content>
|
|
29
|
+
</ContextMenu.Portal>
|
|
30
|
+
</ContextMenu.Root>
|
|
31
|
+
</ActionsMenuContextProvider>
|
|
32
|
+
);
|
|
33
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import {createContext} from "@fibery/react/src/create-context";
|
|
2
|
+
import {useMemo} from "react";
|
|
3
|
+
|
|
4
|
+
type MenuPrimitive = {
|
|
5
|
+
Item: React.ElementType;
|
|
6
|
+
Label: React.ElementType;
|
|
7
|
+
Group: React.ElementType;
|
|
8
|
+
Separator: React.ElementType;
|
|
9
|
+
Sub: React.ElementType;
|
|
10
|
+
SubContent: React.ElementType;
|
|
11
|
+
SubTrigger: React.ElementType;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type ActionsMenuContext = {
|
|
15
|
+
close: () => void;
|
|
16
|
+
MenuPrimitive: MenuPrimitive;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const [Provider, useContext] = createContext<ActionsMenuContext>("ActionsMenuContext");
|
|
20
|
+
|
|
21
|
+
type Props = React.PropsWithChildren<{
|
|
22
|
+
setOpen: (value: boolean) => void;
|
|
23
|
+
MenuPrimitive: MenuPrimitive;
|
|
24
|
+
}>;
|
|
25
|
+
|
|
26
|
+
export const ActionsMenuContextProvider: React.FC<Props> = ({setOpen, MenuPrimitive, children}) => {
|
|
27
|
+
const context = useMemo<ActionsMenuContext>(
|
|
28
|
+
() => ({
|
|
29
|
+
close: () => setOpen(false),
|
|
30
|
+
MenuPrimitive,
|
|
31
|
+
}),
|
|
32
|
+
[setOpen, MenuPrimitive]
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
return <Provider value={context}>{children}</Provider>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export const useActionsMenuContext = useContext;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import {createContext} from "@fibery/react/src/create-context";
|
|
2
|
+
import {ReactNode, useEffect, useState} from "react";
|
|
3
|
+
|
|
4
|
+
type RowKey = string | null;
|
|
5
|
+
type SetRowKey = (state: RowKey) => unknown;
|
|
6
|
+
|
|
7
|
+
const [ActiveDangerousRowProvider, useActiveDangerousRow] = createContext<RowKey>("ActiveDangerousRow");
|
|
8
|
+
const [SetActiveDangerousRowProvider, useSetActiveDangerousRow] = createContext<SetRowKey>("SetActiveDangerousRow");
|
|
9
|
+
|
|
10
|
+
type Props = {
|
|
11
|
+
open: boolean;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const ActionsMenuDangerousRowsProvider = ({open, children}: Props): JSX.Element => {
|
|
16
|
+
const [activeDangerousRow, setActiveDangerousRow] = useState<RowKey>(null);
|
|
17
|
+
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (!open) {
|
|
20
|
+
setActiveDangerousRow(null);
|
|
21
|
+
}
|
|
22
|
+
}, [open]);
|
|
23
|
+
|
|
24
|
+
return (
|
|
25
|
+
<SetActiveDangerousRowProvider value={setActiveDangerousRow}>
|
|
26
|
+
<ActiveDangerousRowProvider value={activeDangerousRow}>{children}</ActiveDangerousRowProvider>
|
|
27
|
+
</SetActiveDangerousRowProvider>
|
|
28
|
+
);
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export {useActiveDangerousRow, useSetActiveDangerousRow};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {useActionsMenuContext} from "./contexts/actions-menu-context";
|
|
2
|
+
import {$TSFixMe} from "../tsfixme";
|
|
3
|
+
|
|
4
|
+
export {ActionsMenu} from "./actions-menu";
|
|
5
|
+
export {ActionsMenuItem} from "./actions-menu-item";
|
|
6
|
+
export {AsyncActionsMenuItem} from "./async-actions-menu-item";
|
|
7
|
+
export {ActionsMenuSubMenu} from "./actions-menu-sub-menu";
|
|
8
|
+
export {useActionsMenuContext} from "./contexts/actions-menu-context";
|
|
9
|
+
export {ActionsMenuSeparator} from "./actions-menu-separator";
|
|
10
|
+
export {insertSeparators} from "./utils/insert-separators";
|
|
11
|
+
|
|
12
|
+
export const ActionsMenuGroup = (props: $TSFixMe) => {
|
|
13
|
+
const {MenuPrimitive} = useActionsMenuContext();
|
|
14
|
+
return <MenuPrimitive.Group {...props} />;
|
|
15
|
+
};
|
|
16
|
+
export const ActionsMenuLabel = (props: $TSFixMe) => {
|
|
17
|
+
const {MenuPrimitive} = useActionsMenuContext();
|
|
18
|
+
return <MenuPrimitive.Label {...props} />;
|
|
19
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {ActionsMenuSeparator} from "../actions-menu-separator";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Inserts ActionMenuSeparator between sections with items
|
|
5
|
+
*
|
|
6
|
+
* If section is single ReactNode instead of array
|
|
7
|
+
* then separator won't be inserted before the node, it's now node's responsibility
|
|
8
|
+
* (e.g. it's used mostly for ButtonActions where you can't decide whether you need a separator before actual render and fetch)
|
|
9
|
+
*/
|
|
10
|
+
export const insertSeparators = (sections: (React.ReactNode[] | React.ReactNode)[]) => {
|
|
11
|
+
const nodes: React.ReactNode[] = [];
|
|
12
|
+
|
|
13
|
+
sections.forEach((section, idx) => {
|
|
14
|
+
if (Array.isArray(section)) {
|
|
15
|
+
const filtered = section.filter(Boolean);
|
|
16
|
+
if (nodes.length > 0 && filtered.length > 0) {
|
|
17
|
+
// it's ok to use key here as index. Separators don't have any state and sections are usually statically defined
|
|
18
|
+
nodes.push(<ActionsMenuSeparator key={`separator-${idx}`} />);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
nodes.push(...filtered);
|
|
22
|
+
} else {
|
|
23
|
+
nodes.push(section);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
return nodes;
|
|
28
|
+
};
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import {Wrap} from "@fibery/react/src/wrap";
|
|
2
|
+
import {css, cx} from "@linaria/core";
|
|
3
|
+
import {styled} from "@linaria/react";
|
|
4
|
+
import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu";
|
|
5
|
+
import {forwardRef} from "react";
|
|
6
|
+
import {border, layout, space, textStyles, themeVars} from "../design-system";
|
|
7
|
+
|
|
8
|
+
export const Root = (dropdownMenuProps: DropdownMenuPrimitive.DropdownMenuProps) => (
|
|
9
|
+
<DropdownMenuPrimitive.Root dir="ltr" modal={false} {...dropdownMenuProps} />
|
|
10
|
+
);
|
|
11
|
+
|
|
12
|
+
export const Trigger = DropdownMenuPrimitive.Trigger;
|
|
13
|
+
|
|
14
|
+
const rowItemStyles = {
|
|
15
|
+
...textStyles.regular,
|
|
16
|
+
display: "flex",
|
|
17
|
+
alignItems: "center",
|
|
18
|
+
minHeight: layout.menuItemHeight,
|
|
19
|
+
outline: "none",
|
|
20
|
+
borderRadius: border.radius6,
|
|
21
|
+
padding: `0px ${space.s12}px`,
|
|
22
|
+
margin: `0px ${space.s4}px`,
|
|
23
|
+
cursor: "pointer",
|
|
24
|
+
|
|
25
|
+
"&:hover,&[data-highlighted]": {
|
|
26
|
+
marginLeft: 4,
|
|
27
|
+
marginRight: 4,
|
|
28
|
+
backgroundColor: themeVars.colorBgActionsMenuItemHover,
|
|
29
|
+
borderRadius: border.radius6,
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"&[data-disabled]": {
|
|
33
|
+
cursor: "default",
|
|
34
|
+
opacity: 0.4,
|
|
35
|
+
|
|
36
|
+
"&:hover": {
|
|
37
|
+
backgroundColor: "unset",
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const dropdownMenuContentStyles = css`
|
|
43
|
+
@keyframes slideDown {
|
|
44
|
+
from {
|
|
45
|
+
opacity: 0;
|
|
46
|
+
transform: translateY(-2px);
|
|
47
|
+
}
|
|
48
|
+
to {
|
|
49
|
+
opacity: 1;
|
|
50
|
+
transform: translateY(0);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
@keyframes slideUp {
|
|
54
|
+
from {
|
|
55
|
+
opacity: 0;
|
|
56
|
+
transform: translateY(2px);
|
|
57
|
+
}
|
|
58
|
+
to {
|
|
59
|
+
opacity: 1;
|
|
60
|
+
transform: translateY(0);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
overflow-y: auto;
|
|
65
|
+
overflow-x: hidden;
|
|
66
|
+
|
|
67
|
+
/* ant dropdown have z-index 1000 and sometimes we need to open dropdown from ant's one
|
|
68
|
+
* e.g. backlog actions on calendar
|
|
69
|
+
* TODO: use more reliable z-index strategy
|
|
70
|
+
*/
|
|
71
|
+
z-index: 1001;
|
|
72
|
+
|
|
73
|
+
max-height: min(var(--radix-dropdown-menu-content-available-height), 692px);
|
|
74
|
+
padding-top: ${space.s4}px;
|
|
75
|
+
padding-bottom: ${space.s4}px;
|
|
76
|
+
min-width: 200px;
|
|
77
|
+
box-shadow: ${themeVars.actionMenuShadow};
|
|
78
|
+
border-radius: ${border.radius8}px;
|
|
79
|
+
backdrop-filter: ${themeVars.effectBgActionsMenu};
|
|
80
|
+
background-color: ${themeVars.colorBgActionsMenu};
|
|
81
|
+
|
|
82
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
83
|
+
animation-duration: 400ms;
|
|
84
|
+
animation-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
|
|
85
|
+
animation-fill-mode: forwards;
|
|
86
|
+
will-change: transform, opacity;
|
|
87
|
+
&[data-state="open"] {
|
|
88
|
+
&[data-side="top"] {
|
|
89
|
+
animation-name: slideUp;
|
|
90
|
+
}
|
|
91
|
+
&[data-side="bottom"] {
|
|
92
|
+
animation-name: slideDown;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
|
|
98
|
+
export type ContentProps = DropdownMenuPrimitive.DropdownMenuContentProps & {
|
|
99
|
+
/* render content in portal. NOTE: Changing this prop will lead to re-mount */
|
|
100
|
+
portalled?: boolean;
|
|
101
|
+
collisionPadding?: number;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const onContentKeyDown = (e: React.KeyboardEvent) => {
|
|
105
|
+
// Escape is attached to document that's why we can't stop propagation of it
|
|
106
|
+
// For other cases we'd like to avoid accidental key handlers e.g. in Inbox
|
|
107
|
+
// (we can chage it in the future ref. https://fibery.slack.com/archives/C05CH6YTSKW/p1692271916224909 https://fibery.slack.com/archives/C05CH6YTSKW/p1692272716431819)
|
|
108
|
+
if (e.key !== "Escape") {
|
|
109
|
+
e.stopPropagation();
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
export const Content = forwardRef<HTMLDivElement, ContentProps>(function DropdownMenuContent(
|
|
114
|
+
{className, portalled = true, collisionPadding = space.s12, ...rest},
|
|
115
|
+
ref
|
|
116
|
+
) {
|
|
117
|
+
return (
|
|
118
|
+
<Wrap if={portalled} with={DropdownMenuPrimitive.Portal}>
|
|
119
|
+
<DropdownMenuPrimitive.Content
|
|
120
|
+
onKeyDown={onContentKeyDown}
|
|
121
|
+
ref={ref}
|
|
122
|
+
className={cx(dropdownMenuContentStyles, className)}
|
|
123
|
+
side="bottom"
|
|
124
|
+
align="start"
|
|
125
|
+
collisionPadding={collisionPadding}
|
|
126
|
+
sideOffset={space.s4}
|
|
127
|
+
{...rest}
|
|
128
|
+
/>
|
|
129
|
+
</Wrap>
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
export const Sub = DropdownMenuPrimitive.Sub;
|
|
134
|
+
|
|
135
|
+
export const SubContent = forwardRef<HTMLDivElement, DropdownMenuPrimitive.DropdownMenuSubContentProps>(
|
|
136
|
+
function DropdownMenuSubContent({className, ...rest}, ref) {
|
|
137
|
+
return (
|
|
138
|
+
<DropdownMenuPrimitive.Portal>
|
|
139
|
+
<DropdownMenuPrimitive.SubContent ref={ref} className={cx(dropdownMenuContentStyles, className)} {...rest} />
|
|
140
|
+
</DropdownMenuPrimitive.Portal>
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
export const SubTrigger = styled(DropdownMenuPrimitive.SubTrigger)`
|
|
146
|
+
${rowItemStyles}
|
|
147
|
+
`;
|
|
148
|
+
|
|
149
|
+
export const Item = styled(DropdownMenuPrimitive.Item)`
|
|
150
|
+
${rowItemStyles}
|
|
151
|
+
`;
|
|
152
|
+
|
|
153
|
+
export const Separator = styled(DropdownMenuPrimitive.Separator)`
|
|
154
|
+
${{
|
|
155
|
+
backgroundColor: themeVars.separatorColor,
|
|
156
|
+
height: 1,
|
|
157
|
+
margin: `${space.s4}px 0`,
|
|
158
|
+
}}
|
|
159
|
+
`;
|
|
160
|
+
|
|
161
|
+
export const Group = DropdownMenuPrimitive.Group;
|
|
162
|
+
|
|
163
|
+
export const Label = styled(DropdownMenuPrimitive.Label)`
|
|
164
|
+
${{
|
|
165
|
+
...textStyles.small,
|
|
166
|
+
color: themeVars.accentTextColor,
|
|
167
|
+
padding: `${space.s6}px ${space.s12}px`,
|
|
168
|
+
}}
|
|
169
|
+
`;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import {Button, ButtonProps} from "../button/button";
|
|
2
|
+
import {useTheme} from "../theme-provider";
|
|
3
|
+
|
|
4
|
+
export const ErrorAlertAction: React.FC<ButtonProps> = (props) => {
|
|
5
|
+
const theme = useTheme();
|
|
6
|
+
|
|
7
|
+
return <Button size=":button-size/small" color={theme.textColor} {...props} />;
|
|
8
|
+
};
|
|
9
|
+
export type ErrorAlertActionElement = React.ReactElement<typeof ErrorAlertAction>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {css} from "@linaria/core";
|
|
2
|
+
import {IconButton} from "../button/icon-button";
|
|
3
|
+
import {border, layout, space, textStyles, themeVars} from "../design-system";
|
|
4
|
+
import CloseIcon from "../icons/react/Close";
|
|
5
|
+
import WarningIcon from "../icons/react/WarningTriangle";
|
|
6
|
+
import {useTheme} from "../theme-provider";
|
|
7
|
+
import {ErrorAlertActionElement} from "./error-alert-action";
|
|
8
|
+
|
|
9
|
+
const errorAlert = css`
|
|
10
|
+
background-color: ${themeVars.errorBgColor};
|
|
11
|
+
padding: ${space.s8}px ${space.s8}px ${space.s8}px ${space.s12}px;
|
|
12
|
+
min-height: ${layout.itemHeight}px;
|
|
13
|
+
border-radius: ${border.radius6}px;
|
|
14
|
+
display: flex;
|
|
15
|
+
gap: ${space.s8}px;
|
|
16
|
+
align-items: center;
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const infoStyle = css`
|
|
20
|
+
color: ${themeVars.textColor};
|
|
21
|
+
flex-grow: 1;
|
|
22
|
+
|
|
23
|
+
display: flex;
|
|
24
|
+
flex-direction: column;
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
const messageStyle = css`
|
|
28
|
+
${textStyles.regular};
|
|
29
|
+
`;
|
|
30
|
+
|
|
31
|
+
const descriptionStyle = css`
|
|
32
|
+
${textStyles.small};
|
|
33
|
+
`;
|
|
34
|
+
|
|
35
|
+
export type ErrorAlertProps = {
|
|
36
|
+
message: string;
|
|
37
|
+
description?: React.ReactNode;
|
|
38
|
+
onClose?: () => void;
|
|
39
|
+
action?: ErrorAlertActionElement;
|
|
40
|
+
};
|
|
41
|
+
export const ErrorAlert: React.FC<ErrorAlertProps> = ({message, description, action, onClose}: ErrorAlertProps) => {
|
|
42
|
+
const theme = useTheme();
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className={errorAlert}>
|
|
46
|
+
<WarningIcon color={theme.errorButtonColor} iconSize={16} containerSize={16} />
|
|
47
|
+
<div className={infoStyle}>
|
|
48
|
+
<span className={messageStyle}>{message}</span>
|
|
49
|
+
{description ? <span className={descriptionStyle}>{description}</span> : null}
|
|
50
|
+
</div>
|
|
51
|
+
{action}
|
|
52
|
+
{onClose ? (
|
|
53
|
+
<IconButton onClick={onClose} aria-label="Hide error">
|
|
54
|
+
<CloseIcon iconSize={16} />
|
|
55
|
+
</IconButton>
|
|
56
|
+
) : null}
|
|
57
|
+
</div>
|
|
58
|
+
);
|
|
59
|
+
};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import {stopPropagation} from "@fibery/react/src/stop-propagation";
|
|
2
|
+
import {useCallbackRef} from "@fibery/react/src/use-callback-ref";
|
|
3
|
+
import {css, cx} from "@linaria/core";
|
|
4
|
+
import _ from "lodash";
|
|
5
|
+
import {forwardRef, useId} from "react";
|
|
6
|
+
import {useActionsMenuContext} from "../actions-menu";
|
|
7
|
+
import {ActionsMenuItemProps} from "../actions-menu/actions-menu-item";
|
|
8
|
+
import {useActiveDangerousRow, useSetActiveDangerousRow} from "../actions-menu/contexts/actions-menu-dangerous-rows";
|
|
9
|
+
import {colors, layout, lineHeight, space, textStyles, themeVars} from "../design-system";
|
|
10
|
+
import {
|
|
11
|
+
ListRowContent,
|
|
12
|
+
ListRowContentProps,
|
|
13
|
+
ListRowSurfaceProps,
|
|
14
|
+
NATUAL_FOCUS_CLASS,
|
|
15
|
+
NATUAL_HOVER_CLASS,
|
|
16
|
+
NATURAL_HOVER,
|
|
17
|
+
listRowSurfaceStylesBase,
|
|
18
|
+
} from "./list-row-surface";
|
|
19
|
+
|
|
20
|
+
interface ActionMenuItemSurfaceProps
|
|
21
|
+
extends Omit<ActionsMenuItemProps, "description">,
|
|
22
|
+
Omit<ListRowSurfaceProps, "children" | "darker" | "onSelect" | "id"> {}
|
|
23
|
+
|
|
24
|
+
export const actionMenuItemSurface = css`
|
|
25
|
+
margin: 0 ${space.s4}px;
|
|
26
|
+
background-color: ${themeVars.colorBgListItemGeneral};
|
|
27
|
+
${NATURAL_HOVER},
|
|
28
|
+
&[data-highlighted] {
|
|
29
|
+
background-color: ${themeVars.colorBgActionsMenuItemHover};
|
|
30
|
+
}
|
|
31
|
+
&.done,
|
|
32
|
+
&[data-disabled] {
|
|
33
|
+
cursor: not-allowed;
|
|
34
|
+
color: ${themeVars.colorTextListItemGeneralDisabled};
|
|
35
|
+
background-color: ${themeVars.colorBgListItemGeneralDisabled};
|
|
36
|
+
${NATURAL_HOVER}, &.hover > * {
|
|
37
|
+
opacity: ${themeVars.opacityListItemGeneralDisabled};
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
&.selected {
|
|
41
|
+
cursor: default;
|
|
42
|
+
background-color: ${themeVars.colorBgActionsMenuItemSelected};
|
|
43
|
+
${NATURAL_HOVER},&.hover, &[data-highlighted] {
|
|
44
|
+
background-color: ${themeVars.colorBgActionsMenuItemSelectedHover};
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
&.dangerous {
|
|
48
|
+
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
49
|
+
${NATURAL_HOVER},
|
|
50
|
+
&.hover,
|
|
51
|
+
&[data-highlighted] {
|
|
52
|
+
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
&.warm {
|
|
56
|
+
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
57
|
+
color: ${colors.inversedTextColor};
|
|
58
|
+
${NATURAL_HOVER}, &.hover,
|
|
59
|
+
&[data-highlighted] {
|
|
60
|
+
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
61
|
+
color: ${colors.inversedTextColor};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
export const ActionMenuItemSurface = forwardRef<HTMLDivElement, ActionMenuItemSurfaceProps>(
|
|
67
|
+
({testId, className, loading, selected, hovered, focused, title, disabled, children, dangerous, ...props}, ref) => {
|
|
68
|
+
const id = useId();
|
|
69
|
+
const activeDangerousRow = useActiveDangerousRow();
|
|
70
|
+
const showWarning = activeDangerousRow === id;
|
|
71
|
+
const onSelect = useCallbackRef(props.onSelect || _.noop);
|
|
72
|
+
const {MenuPrimitive} = useActionsMenuContext();
|
|
73
|
+
const setActiveDangerousRow = useSetActiveDangerousRow();
|
|
74
|
+
const handleSelect = (e: Event) => {
|
|
75
|
+
e.stopPropagation();
|
|
76
|
+
|
|
77
|
+
if (loading || !id) {
|
|
78
|
+
e.preventDefault();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (dangerous && !showWarning) {
|
|
83
|
+
e.preventDefault();
|
|
84
|
+
setActiveDangerousRow(id);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
setActiveDangerousRow(null);
|
|
89
|
+
|
|
90
|
+
onSelect(e);
|
|
91
|
+
};
|
|
92
|
+
return (
|
|
93
|
+
<MenuPrimitive.Item
|
|
94
|
+
ref={ref}
|
|
95
|
+
data-testid={testId}
|
|
96
|
+
onClick={stopPropagation}
|
|
97
|
+
onSelect={handleSelect}
|
|
98
|
+
title={title ? title : ""}
|
|
99
|
+
className={cx(
|
|
100
|
+
listRowSurfaceStylesBase,
|
|
101
|
+
actionMenuItemSurface,
|
|
102
|
+
className,
|
|
103
|
+
hovered === undefined && NATUAL_HOVER_CLASS,
|
|
104
|
+
focused === undefined && NATUAL_FOCUS_CLASS,
|
|
105
|
+
focused && "focus",
|
|
106
|
+
disabled && "done",
|
|
107
|
+
selected && "selected",
|
|
108
|
+
dangerous && "dangerous",
|
|
109
|
+
showWarning && "warn"
|
|
110
|
+
)}
|
|
111
|
+
disabled={disabled}
|
|
112
|
+
>
|
|
113
|
+
{children}
|
|
114
|
+
</MenuPrimitive.Item>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
interface ActionMenuItemContentProps extends ListRowContentProps {
|
|
120
|
+
description?: string;
|
|
121
|
+
}
|
|
122
|
+
const actionMenuItemDescriptionClass = css`
|
|
123
|
+
${textStyles.small};
|
|
124
|
+
line-height: ${lineHeight.nowrap};
|
|
125
|
+
color: ${themeVars.accentTextColor};
|
|
126
|
+
padding: ${space.s2}px ${space.s8}px 0;
|
|
127
|
+
`;
|
|
128
|
+
const actionMenuItemContentClass = css`
|
|
129
|
+
line-height: ${layout.menuItemHeight}px;
|
|
130
|
+
padding: 0 ${space.s8}px;
|
|
131
|
+
`;
|
|
132
|
+
const actionMenuItemWithDescription = css`
|
|
133
|
+
min-height: ${layout.itemWithSubtitleHeight}px;
|
|
134
|
+
`;
|
|
135
|
+
export const ActionMenuItemContent = forwardRef<HTMLDivElement, ActionMenuItemContentProps>(
|
|
136
|
+
({contentClassName, description, ...rest}, ref) => {
|
|
137
|
+
return (
|
|
138
|
+
<div className={cx(description && actionMenuItemWithDescription)}>
|
|
139
|
+
<ListRowContent contentClassName={cx(contentClassName, actionMenuItemContentClass)} ref={ref} {...rest} />
|
|
140
|
+
{description ? <div className={actionMenuItemDescriptionClass}>{description}</div> : null}
|
|
141
|
+
</div>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
);
|
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
import {ForwardedRef, forwardRef, HTMLAttributes, PropsWithChildren, ReactNode, useId} from "react";
|
|
2
|
-
import {border, colors, layout, lineHeight, space, textStyles, themeVars} from "../design-system";
|
|
3
1
|
import {css, cx} from "@linaria/core";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {stopPropagation} from "@fibery/react/src/stop-propagation";
|
|
7
|
-
import {useActiveDangerousRow, useSetActiveDangerousRow} from "../actions-menu/contexts/actions-menu-dangerous-rows";
|
|
8
|
-
import _ from "lodash";
|
|
9
|
-
import {useCallbackRef} from "@fibery/react/src/use-callback-ref";
|
|
2
|
+
import {ForwardedRef, forwardRef, HTMLAttributes, PropsWithChildren, ReactNode} from "react";
|
|
3
|
+
import {border, space, textStyles, themeVars} from "../design-system";
|
|
10
4
|
|
|
11
5
|
interface ListRowSurfaceBase extends HTMLAttributes<HTMLDivElement> {
|
|
12
6
|
testId?: string;
|
|
@@ -18,9 +12,7 @@ export interface ListRowSurfaceProps extends ListRowSurfaceBase {
|
|
|
18
12
|
focused?: boolean;
|
|
19
13
|
hovered?: boolean;
|
|
20
14
|
}
|
|
21
|
-
|
|
22
|
-
extends Omit<ActionsMenuItemProps, "description">,
|
|
23
|
-
Omit<ListRowSurfaceProps, "children" | "darker" | "onSelect" | "id"> {}
|
|
15
|
+
|
|
24
16
|
export const NATUAL_HOVER_CLASS = "natural-hover";
|
|
25
17
|
export const NATUAL_FOCUS_CLASS = "natural-focus";
|
|
26
18
|
export const NATURAL_HOVER = `&.${NATUAL_HOVER_CLASS}:hover`;
|
|
@@ -152,48 +144,6 @@ export const listRowSurfaceStatesDark = css`
|
|
|
152
144
|
}
|
|
153
145
|
`;
|
|
154
146
|
|
|
155
|
-
export const actionMenuItemSurface = css`
|
|
156
|
-
margin: 0 ${space.s4}px;
|
|
157
|
-
background-color: ${themeVars.colorBgListItemGeneral};
|
|
158
|
-
${NATURAL_HOVER},
|
|
159
|
-
&[data-highlighted] {
|
|
160
|
-
background-color: ${themeVars.colorBgActionsMenuItemHover};
|
|
161
|
-
}
|
|
162
|
-
&.done,
|
|
163
|
-
&[data-disabled] {
|
|
164
|
-
cursor: not-allowed;
|
|
165
|
-
color: ${themeVars.colorTextListItemGeneralDisabled};
|
|
166
|
-
background-color: ${themeVars.colorBgListItemGeneralDisabled};
|
|
167
|
-
${NATURAL_HOVER}, &.hover > * {
|
|
168
|
-
opacity: ${themeVars.opacityListItemGeneralDisabled};
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
&.selected {
|
|
172
|
-
cursor: default;
|
|
173
|
-
background-color: ${themeVars.colorBgActionsMenuItemSelected};
|
|
174
|
-
${NATURAL_HOVER},&.hover, &[data-highlighted] {
|
|
175
|
-
background-color: ${themeVars.colorBgActionsMenuItemSelectedHover};
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
&.dangerous {
|
|
179
|
-
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
180
|
-
${NATURAL_HOVER},
|
|
181
|
-
&.hover,
|
|
182
|
-
&[data-highlighted] {
|
|
183
|
-
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
&.warm {
|
|
187
|
-
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
188
|
-
color: ${colors.inversedTextColor};
|
|
189
|
-
${NATURAL_HOVER}, &.hover,
|
|
190
|
-
&[data-highlighted] {
|
|
191
|
-
background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
|
|
192
|
-
color: ${colors.inversedTextColor};
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
`;
|
|
196
|
-
|
|
197
147
|
/**
|
|
198
148
|
* This should be responsible for all flat list items existing in UI in Fibery. Options, action menus and etc.
|
|
199
149
|
* You can enforce it's state with props focused, hovered, selected, done (use it for disabled too)
|
|
@@ -222,58 +172,6 @@ export const ListRowSurface = forwardRef<HTMLDivElement, ListRowSurfaceProps>(
|
|
|
222
172
|
);
|
|
223
173
|
}
|
|
224
174
|
);
|
|
225
|
-
export const ActionMenuItemSurface = forwardRef<HTMLDivElement, ActionMenuItemSurfaceProps>(
|
|
226
|
-
({testId, className, loading, selected, hovered, focused, title, disabled, children, dangerous, ...props}, ref) => {
|
|
227
|
-
const id = useId();
|
|
228
|
-
const activeDangerousRow = useActiveDangerousRow();
|
|
229
|
-
const showWarning = activeDangerousRow === id;
|
|
230
|
-
const onSelect = useCallbackRef(props.onSelect || _.noop);
|
|
231
|
-
const {MenuPrimitive} = useActionsMenuContext();
|
|
232
|
-
const setActiveDangerousRow = useSetActiveDangerousRow();
|
|
233
|
-
const handleSelect = (e: Event) => {
|
|
234
|
-
e.stopPropagation();
|
|
235
|
-
|
|
236
|
-
if (loading || !id) {
|
|
237
|
-
e.preventDefault();
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (dangerous && !showWarning) {
|
|
242
|
-
e.preventDefault();
|
|
243
|
-
setActiveDangerousRow(id);
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
setActiveDangerousRow(null);
|
|
248
|
-
|
|
249
|
-
onSelect(e);
|
|
250
|
-
};
|
|
251
|
-
return (
|
|
252
|
-
<MenuPrimitive.Item
|
|
253
|
-
ref={ref}
|
|
254
|
-
data-testid={testId}
|
|
255
|
-
onClick={stopPropagation}
|
|
256
|
-
onSelect={handleSelect}
|
|
257
|
-
title={title ? title : ""}
|
|
258
|
-
className={cx(
|
|
259
|
-
listRowSurfaceStylesBase,
|
|
260
|
-
actionMenuItemSurface,
|
|
261
|
-
className,
|
|
262
|
-
hovered === undefined && NATUAL_HOVER_CLASS,
|
|
263
|
-
focused === undefined && NATUAL_FOCUS_CLASS,
|
|
264
|
-
focused && "focus",
|
|
265
|
-
disabled && "done",
|
|
266
|
-
selected && "selected",
|
|
267
|
-
dangerous && "dangerous",
|
|
268
|
-
showWarning && "warn"
|
|
269
|
-
)}
|
|
270
|
-
disabled={disabled}
|
|
271
|
-
>
|
|
272
|
-
{children}
|
|
273
|
-
</MenuPrimitive.Item>
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
);
|
|
277
175
|
|
|
278
176
|
export interface ListRowContentProps extends PropsWithChildren<Omit<HTMLAttributes<HTMLDivElement>, "className">> {
|
|
279
177
|
rootClassName?: string;
|
|
@@ -322,30 +220,3 @@ export const ListRowContent = forwardRef<HTMLDivElement, ListRowContentProps>(
|
|
|
322
220
|
);
|
|
323
221
|
}
|
|
324
222
|
);
|
|
325
|
-
|
|
326
|
-
interface ActionMenuItemContentProps extends ListRowContentProps {
|
|
327
|
-
description?: string;
|
|
328
|
-
}
|
|
329
|
-
const actionMenuItemDescriptionClass = css`
|
|
330
|
-
${textStyles.small};
|
|
331
|
-
line-height: ${lineHeight.nowrap};
|
|
332
|
-
color: ${themeVars.accentTextColor};
|
|
333
|
-
padding: ${space.s2}px ${space.s8}px 0;
|
|
334
|
-
`;
|
|
335
|
-
const actionMenuItemContentClass = css`
|
|
336
|
-
line-height: ${layout.menuItemHeight}px;
|
|
337
|
-
padding: 0 ${space.s8}px;
|
|
338
|
-
`;
|
|
339
|
-
const actionMenuItemWithDescription = css`
|
|
340
|
-
min-height: ${layout.itemWithSubtitleHeight}px;
|
|
341
|
-
`;
|
|
342
|
-
export const ActionMenuItemContent = forwardRef<HTMLDivElement, ActionMenuItemContentProps>(
|
|
343
|
-
({contentClassName, description, ...rest}, ref) => {
|
|
344
|
-
return (
|
|
345
|
-
<div className={cx(description && actionMenuItemWithDescription)}>
|
|
346
|
-
<ListRowContent contentClassName={cx(contentClassName, actionMenuItemContentClass)} ref={ref} {...rest} />
|
|
347
|
-
{description ? <div className={actionMenuItemDescriptionClass}>{description}</div> : null}
|
|
348
|
-
</div>
|
|
349
|
-
);
|
|
350
|
-
}
|
|
351
|
-
);
|
|
@@ -131,7 +131,7 @@ export function MenuListVirtualized<
|
|
|
131
131
|
const menuHeight = Math.min(flattenChildren.length * layout.menuItemHeight, maxHeight);
|
|
132
132
|
//scrolling to focused option
|
|
133
133
|
useEffect(() => {
|
|
134
|
-
let to:
|
|
134
|
+
let to: number | undefined;
|
|
135
135
|
if (initialFocusedOption && focusedOption && virtuoso.current) {
|
|
136
136
|
const index = flattenChildren.findIndex((child) => {
|
|
137
137
|
if (isObject(child) && "props" in child) {
|
|
@@ -144,7 +144,7 @@ export function MenuListVirtualized<
|
|
|
144
144
|
if (index >= 0) {
|
|
145
145
|
const focusedOptionPosition = index * OPTION_HEIGHT;
|
|
146
146
|
virtuoso.current.getState((state) => {
|
|
147
|
-
to = setTimeout(() => {
|
|
147
|
+
to = window.setTimeout(() => {
|
|
148
148
|
if (!virtuoso.current) {
|
|
149
149
|
return;
|
|
150
150
|
}
|
|
@@ -168,7 +168,7 @@ export function MenuListVirtualized<
|
|
|
168
168
|
}
|
|
169
169
|
}
|
|
170
170
|
return () => {
|
|
171
|
-
clearTimeout(to);
|
|
171
|
+
window.clearTimeout(to);
|
|
172
172
|
};
|
|
173
173
|
}, [focusedOption, flattenChildren, menuHeight, initialFocusedOption, virtuoso]);
|
|
174
174
|
return (
|