@fibery/ui-kit 1.26.3 → 1.26.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fibery/ui-kit",
3
- "version": "1.26.3",
3
+ "version": "1.26.5",
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.tsx",
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",
@@ -103,8 +104,8 @@
103
104
  "svgo": "2.8.0",
104
105
  "typescript": "5.1.6",
105
106
  "unist-util-reduce": "0.2.2",
106
- "@fibery/babel-preset": "7.4.0",
107
- "@fibery/eslint-config": "8.5.1"
107
+ "@fibery/eslint-config": "8.5.1",
108
+ "@fibery/babel-preset": "7.4.0"
108
109
  },
109
110
  "jest": {
110
111
  "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
+ };
@@ -377,7 +377,9 @@ export const themeColors = {
377
377
  // :hover
378
378
  colorBgMenuItemSelectedHover: [indigo.indigo6, indigoDark.indigo6],
379
379
  // :focus, :focus:hover
380
- colorBgObjectEditorSeparator: [slate.slate4, slateDark.slate4],
380
+ colorBgPinnedFieldsLabel: [slate.slate11, slateDark.slate11],
381
+ colorBgObjectEditorSeparator: [slate.slate12, slateDark.slate12],
382
+ colorBgObjectEditorSeparatorButton: [slate.slate4, slateDark.slate4],
381
383
  colorBgMenuItemSelectedFocused: [indigo.indigo6, indigoDark.indigo6],
382
384
  colorBgFieldEditorContainer: [slate.slate2, slateDark.slate3],
383
385
  colorBgFieldEditorLinkEqualSign: [slate.slate6, slateDark.slate2],
@@ -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
+ };
@@ -28,8 +28,10 @@ import {
28
28
  import {SelectControlSettingsProvider} from "./select-control-settings-context";
29
29
 
30
30
  const offset = [0, space.s4] as [number, number];
31
- const popupClassName = css`
31
+ const popupContainerClassName = css`
32
32
  z-index: 1050;
33
+ `;
34
+ const popupClassName = css`
33
35
  max-width: 360px;
34
36
  padding-top: ${space.s8}px;
35
37
  padding-bottom: ${space.s4}px;
@@ -277,6 +279,7 @@ function SelectInPopoverInner<Option, IsMulti extends boolean, Group extends Gro
277
279
  {Boolean(triggerElement) && (
278
280
  <Popup
279
281
  offset={offset}
282
+ popupContainerClassName={popupContainerClassName}
280
283
  popupClassName={cx(popupClassName, popupStyles)}
281
284
  placement="bottom-start"
282
285
  renderInPortal={renderInPortal}