@fibery/ui-kit 1.24.0 → 1.25.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/package.json +5 -7
  2. package/src/design-system.ts +8 -1
  3. package/src/emoji-picker/primitives/emoji.tsx +2 -0
  4. package/src/icons/ast/AutomationsCancelled.ts +8 -0
  5. package/src/icons/ast/AutomationsDisabled.ts +8 -0
  6. package/src/icons/ast/AutomationsError.ts +8 -0
  7. package/src/icons/ast/AutomationsInProgress.ts +8 -0
  8. package/src/icons/ast/AutomationsNotStarted.ts +8 -0
  9. package/src/icons/ast/AutomationsSuccess.ts +8 -0
  10. package/src/icons/ast/index.tsx +6 -0
  11. package/src/icons/react/AutomationsCancelled.tsx +12 -0
  12. package/src/icons/react/AutomationsDisabled.tsx +12 -0
  13. package/src/icons/react/AutomationsError.tsx +12 -0
  14. package/src/icons/react/AutomationsInProgress.tsx +12 -0
  15. package/src/icons/react/AutomationsNotStarted.tsx +12 -0
  16. package/src/icons/react/AutomationsSuccess.tsx +12 -0
  17. package/src/icons/react/Chat.tsx +1 -1
  18. package/src/icons/react/ChatAi.tsx +1 -1
  19. package/src/icons/react/Headphones.tsx +1 -1
  20. package/src/icons/react/HeadphonesAi.tsx +1 -1
  21. package/src/icons/react/index.tsx +6 -0
  22. package/src/lists/list-row-surface.tsx +162 -22
  23. package/src/select/custom-select-partials/group-heading.tsx +32 -9
  24. package/src/select/custom-select-partials/menu-list-virtualized.tsx +186 -0
  25. package/src/select/custom-select-partials/menu.tsx +1 -2
  26. package/src/select/custom-select-partials/no-option-message.tsx +20 -4
  27. package/src/select/custom-select-partials/option.tsx +20 -26
  28. package/src/select/index.tsx +64 -63
  29. package/src/select/select-in-popover.tsx +14 -12
  30. package/src/select/select-mock-context.tsx +9 -0
  31. package/src/select/styles.ts +1 -3
  32. package/src/select/util.ts +31 -0
  33. package/index.ts +0 -1
package/package.json CHANGED
@@ -1,7 +1,6 @@
1
1
  {
2
2
  "name": "@fibery/ui-kit",
3
- "version": "1.24.0",
4
- "main": "index.ts",
3
+ "version": "1.25.0",
5
4
  "private": false,
6
5
  "files": [
7
6
  "src/antd/styles.ts",
@@ -67,12 +66,11 @@
67
66
  "react-select": "5.3.2",
68
67
  "react-select-country-list": "2.2.1",
69
68
  "react-virtuoso": "4.6.0",
70
- "react-windowed-select": "5.0.0",
71
69
  "screenfull": "6.0.1",
72
70
  "ua-parser-js": "0.7.24",
73
71
  "@fibery/emoji-data": "2.3.0",
74
- "@fibery/helpers": "1.2.0",
75
- "@fibery/react": "1.3.0"
72
+ "@fibery/react": "1.3.0",
73
+ "@fibery/helpers": "1.2.0"
76
74
  },
77
75
  "peerDependencies": {
78
76
  "react": "^18.2.0",
@@ -105,8 +103,8 @@
105
103
  "svgo": "2.8.0",
106
104
  "typescript": "5.1.6",
107
105
  "unist-util-reduce": "0.2.2",
108
- "@fibery/eslint-config": "8.5.0",
109
- "@fibery/babel-preset": "7.3.0"
106
+ "@fibery/babel-preset": "7.3.0",
107
+ "@fibery/eslint-config": "8.5.0"
110
108
  },
111
109
  "jest": {
112
110
  "testEnvironment": "jsdom",
@@ -235,7 +235,13 @@ export const themeColors = {
235
235
  checkboxColor: [slate.slate10, slateDark.slate10],
236
236
  colorBgActionsMenu: [getOpacities(whiteA.whiteA0).opacity85, getOpacities(slateDark.slate4).opacity85],
237
237
  colorBgActionsMenuItemHover: [getOpacities(slate.slate11).opacity10, getOpacities(slateDark.slate11).opacity10],
238
+ colorBgActionsMenuItemSelected: [getOpacities(indigo.indigo4).opacity80, getOpacities(indigoDark.indigo4).opacity80],
239
+ colorBgActionsMenuItemSelectedHover: [
240
+ getOpacities(indigo.indigo5).opacity80,
241
+ getOpacities(indigoDark.indigo6).opacity80,
242
+ ],
238
243
  colorBgActionsMenuItemDangerHover: [getOpacities(red.red9).opacity10, getOpacities(redDark.red10).opacity10],
244
+
239
245
  colorBgActionsMenuItemDangerActive: [getOpacities(red.red9).opacity80, getOpacities(redDark.red10).opacity80],
240
246
  actionMenuBg: [whiteA.whiteA0, slateDark.slate4],
241
247
  actionMenuInnerBg: [whiteA.whiteA0, slateDark.slate5],
@@ -251,7 +257,7 @@ export const themeColors = {
251
257
  `0 0 0 1px ${getOpacities(slate.slate10).opacity10}, 0 6px 8px -4px ${getOpacities(slate.slate10).opacity30}`,
252
258
  `0 0 0 1px ${getOpacities(slateDark.slate2).opacity10}, 0 6px 8px -4px ${getOpacities(slateDark.slate2).opacity30}`,
253
259
  ],
254
- separatorColor: [getOpacities(slate.slate4).opacity50, getOpacities(slateDark.slate8).opacity50],
260
+ separatorColor: [getOpacities(blackA.blackA0).opacity5, getOpacities(whiteA.whiteA0).opacity5],
255
261
  separatorBreadcrumbsColor: [slate.slate8, slateDark.slate8],
256
262
  iconColor: [slate.slate10, slateDark.slate10],
257
263
  appIconColor: [getOpacities(slate.slate2).opacity90, getOpacities(slate.slate2).opacity70],
@@ -725,6 +731,7 @@ export const space = {
725
731
  s12: 12,
726
732
  s14: 14,
727
733
  s16: 16,
734
+ s18: 18,
728
735
  s20: 20,
729
736
  s24: 24,
730
737
  s28: 28,
@@ -23,12 +23,14 @@ const emojiInlineImgCss = css`
23
23
  vertical-align: -0.1em;
24
24
  object-fit: contain;
25
25
  overflow: hidden;
26
+ -webkit-touch-callout: none;
26
27
  `;
27
28
  const emojiSizedImgCss = css`
28
29
  width: 100%;
29
30
  height: 100%;
30
31
  object-fit: contain;
31
32
  overflow: hidden;
33
+ -webkit-touch-callout: none;
32
34
  `;
33
35
 
34
36
  // This is needed to keep text-selection working for image based emojis Details: https://the.fibery.io/SoftDev/bug/Improve-selection-text-with-emojis-8963
@@ -0,0 +1,8 @@
1
+
2
+ // This icon file is generated automatically.
3
+
4
+ import { IconDefinition } from '../types';
5
+
6
+ const AutomationsCancelled: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"width":20,"height":20,"fill":"none"},"children":[{"type":"element","tagName":"path","properties":{"d":"M10.001 9.166a.833.833 0 0 0-.833.834v3.333a.833.833 0 0 0 1.667 0V10A.834.834 0 0 0 10 9.166Zm.317-3.266a.833.833 0 0 0-.633 0 .833.833 0 0 0-.275.175.958.958 0 0 0-.175.275.833.833 0 0 0 .175.908.958.958 0 0 0 .275.175.833.833 0 0 0 1.15-.766.874.874 0 0 0-.242-.592.834.834 0 0 0-.275-.175Zm-.317-4.234a8.333 8.333 0 1 0 0 16.667 8.333 8.333 0 0 0 0-16.666Zm0 15a6.667 6.667 0 1 1 0-13.333 6.667 6.667 0 0 1 0 13.333Z","fill":"#6E7A83"},"children":[]}],"metadata":""}]},"name":"automations-cancelled"};
7
+
8
+ export default AutomationsCancelled;
@@ -0,0 +1,8 @@
1
+
2
+ // This icon file is generated automatically.
3
+
4
+ import { IconDefinition } from '../types';
5
+
6
+ const AutomationsDisabled: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"width":20,"height":20,"fill":"none"},"children":[{"type":"element","tagName":"path","properties":{"fillRule":"evenodd","clipRule":"evenodd","d":"M8.891 1.572a8.57 8.57 0 0 1 2.218 0 1 1 0 1 1-.259 1.983 6.57 6.57 0 0 0-1.7 0 1 1 0 0 1-.259-1.983ZM6.227 3.44a1 1 0 0 1-.184 1.402c-.45.347-.854.75-1.2 1.201a1 1 0 0 1-1.586-1.218 8.545 8.545 0 0 1 1.568-1.568 1 1 0 0 1 1.402.183Zm7.546 0a1 1 0 0 1 1.402-.183c.589.452 1.116.98 1.568 1.568a1 1 0 0 1-1.585 1.218 6.546 6.546 0 0 0-1.201-1.2 1 1 0 0 1-.184-1.403ZM2.693 8.03a1 1 0 0 1 .862 1.12 6.57 6.57 0 0 0 0 1.701 1 1 0 0 1-1.983.259 8.57 8.57 0 0 1 0-2.218 1 1 0 0 1 1.12-.862Zm14.614 0a1 1 0 0 1 1.121.862 8.57 8.57 0 0 1 0 2.218 1 1 0 1 1-1.983-.259 6.578 6.578 0 0 0 0-1.7 1 1 0 0 1 .862-1.121ZM3.44 13.773a1 1 0 0 1 1.402.184c.347.45.75.854 1.201 1.2a1 1 0 1 1-1.218 1.586 8.543 8.543 0 0 1-1.568-1.568 1 1 0 0 1 .183-1.402Zm13.12 0a1 1 0 0 1 .183 1.402 8.542 8.542 0 0 1-1.568 1.568 1 1 0 0 1-1.218-1.585c.45-.347.854-.75 1.2-1.201a1 1 0 0 1 1.403-.184Zm-8.531 3.534a1 1 0 0 1 1.12-.862 6.578 6.578 0 0 0 1.701 0 1 1 0 0 1 .259 1.983 8.57 8.57 0 0 1-2.218 0 1 1 0 0 1-.862-1.12Z","fill":"#6E7A83"},"children":[]}],"metadata":""}]},"name":"automations-disabled"};
7
+
8
+ export default AutomationsDisabled;
@@ -0,0 +1,8 @@
1
+
2
+ // This icon file is generated automatically.
3
+
4
+ import { IconDefinition } from '../types';
5
+
6
+ const AutomationsError: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"width":20,"height":20,"fill":"none"},"children":[{"type":"element","tagName":"path","properties":{"d":"M13.092 6.909a.834.834 0 0 0-1.184 0L10 8.825 8.092 6.91a.837.837 0 1 0-1.183 1.183L8.825 10 6.91 11.91a.834.834 0 0 0 .27 1.365.833.833 0 0 0 .913-.182L10 11.175l1.909 1.917a.834.834 0 0 0 1.365-.271.834.834 0 0 0-.182-.912L11.175 10l1.917-1.908a.833.833 0 0 0 0-1.183Zm2.8-2.8A8.334 8.334 0 1 0 4.109 15.892 8.334 8.334 0 1 0 15.892 4.109Zm-1.175 10.608A6.667 6.667 0 1 1 16.667 10a6.626 6.626 0 0 1-1.95 4.717Z","fill":"#E5484D"},"children":[]}],"metadata":""}]},"name":"automations-error"};
7
+
8
+ export default AutomationsError;
@@ -0,0 +1,8 @@
1
+
2
+ // This icon file is generated automatically.
3
+
4
+ import { IconDefinition } from '../types';
5
+
6
+ const AutomationsInProgress: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"width":20,"height":20,"fill":"none"},"children":[{"type":"element","tagName":"path","properties":{"d":"M13.335 8.558 9.168 6.15a1.667 1.667 0 0 0-2.5 1.441v4.817a1.666 1.666 0 0 0 2.5 1.442l4.167-2.409a1.667 1.667 0 0 0 0-2.883ZM12.5 10l-4.166 2.408V7.592L12.5 10Zm-2.5-8.333a8.333 8.333 0 1 0 0 16.666 8.333 8.333 0 0 0 0-16.666Zm0 15a6.667 6.667 0 1 1 0-13.334 6.667 6.667 0 0 1 0 13.333Z","fill":"#6E7A83"},"children":[]}],"metadata":""}]},"name":"automations-in-progress"};
7
+
8
+ export default AutomationsInProgress;
@@ -0,0 +1,8 @@
1
+
2
+ // This icon file is generated automatically.
3
+
4
+ import { IconDefinition } from '../types';
5
+
6
+ const AutomationsNotStarted: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"width":20,"height":20,"fill":"none"},"children":[{"type":"element","tagName":"path","properties":{"d":"M8.335 5.833a.833.833 0 0 0-.834.833v6.667a.833.833 0 0 0 1.667 0V6.666a.833.833 0 0 0-.833-.833ZM10 1.666a8.333 8.333 0 1 0 0 16.667 8.333 8.333 0 0 0 0-16.666Zm0 15a6.667 6.667 0 1 1 0-13.333 6.667 6.667 0 0 1 0 13.333Zm1.667-10.833a.833.833 0 0 0-.833.833v6.667a.833.833 0 0 0 1.666 0V6.666a.833.833 0 0 0-.833-.833Z","fill":"#6E7A83"},"children":[]}],"metadata":""}]},"name":"automations-not-started"};
7
+
8
+ export default AutomationsNotStarted;
@@ -0,0 +1,8 @@
1
+
2
+ // This icon file is generated automatically.
3
+
4
+ import { IconDefinition } from '../types';
5
+
6
+ const AutomationsSuccess: IconDefinition = {"icon":{"type":"root","children":[{"type":"element","tagName":"svg","properties":{"width":20,"height":20,"fill":"none"},"children":[{"type":"element","tagName":"path","properties":{"d":"m12.268 7.325-3.575 3.583-1.375-1.375a.833.833 0 1 0-1.175 1.175l1.958 1.967a.831.831 0 0 0 .592.241.833.833 0 0 0 .583-.241l4.167-4.167a.833.833 0 1 0-1.175-1.183Zm-2.267-5.659a8.333 8.333 0 1 0 0 16.667 8.333 8.333 0 0 0 0-16.666Zm0 15a6.667 6.667 0 1 1 0-13.333 6.667 6.667 0 0 1 0 13.333Z","fill":"#6E7A83"},"children":[]}],"metadata":""}]},"name":"automations-success"};
7
+
8
+ export default AutomationsSuccess;
@@ -31,6 +31,12 @@ export { default as ArrowRight } from './ArrowRight';
31
31
  export { default as ArrowTop } from './ArrowTop';
32
32
  export { default as AskForInput } from './AskForInput';
33
33
  export { default as Atom } from './Atom';
34
+ export { default as AutomationsCancelled } from './AutomationsCancelled';
35
+ export { default as AutomationsDisabled } from './AutomationsDisabled';
36
+ export { default as AutomationsError } from './AutomationsError';
37
+ export { default as AutomationsInProgress } from './AutomationsInProgress';
38
+ export { default as AutomationsNotStarted } from './AutomationsNotStarted';
39
+ export { default as AutomationsSuccess } from './AutomationsSuccess';
34
40
  export { default as Back } from './Back';
35
41
  export { default as BacklogIcon } from './BacklogIcon';
36
42
  export { default as BatchActionsIcon } from './BatchActionsIcon';
@@ -0,0 +1,12 @@
1
+ // This icon file is generated automatically.
2
+
3
+ import AutomationsCancelledSvg from '../ast/AutomationsCancelled';
4
+ import { Icon } from '../Icon';
5
+ import { IconBaseProps } from '../types';
6
+
7
+ const AutomationsCancelled = (
8
+ props: IconBaseProps,
9
+ ): JSX.Element => <Icon {...props} icon={AutomationsCancelledSvg} />;
10
+
11
+ AutomationsCancelled.displayName = 'AutomationsCancelled';
12
+ export default AutomationsCancelled;
@@ -0,0 +1,12 @@
1
+ // This icon file is generated automatically.
2
+
3
+ import AutomationsDisabledSvg from '../ast/AutomationsDisabled';
4
+ import { Icon } from '../Icon';
5
+ import { IconBaseProps } from '../types';
6
+
7
+ const AutomationsDisabled = (
8
+ props: IconBaseProps,
9
+ ): JSX.Element => <Icon {...props} icon={AutomationsDisabledSvg} />;
10
+
11
+ AutomationsDisabled.displayName = 'AutomationsDisabled';
12
+ export default AutomationsDisabled;
@@ -0,0 +1,12 @@
1
+ // This icon file is generated automatically.
2
+
3
+ import AutomationsErrorSvg from '../ast/AutomationsError';
4
+ import { Icon } from '../Icon';
5
+ import { IconBaseProps } from '../types';
6
+
7
+ const AutomationsError = (
8
+ props: IconBaseProps,
9
+ ): JSX.Element => <Icon {...props} icon={AutomationsErrorSvg} />;
10
+
11
+ AutomationsError.displayName = 'AutomationsError';
12
+ export default AutomationsError;
@@ -0,0 +1,12 @@
1
+ // This icon file is generated automatically.
2
+
3
+ import AutomationsInProgressSvg from '../ast/AutomationsInProgress';
4
+ import { Icon } from '../Icon';
5
+ import { IconBaseProps } from '../types';
6
+
7
+ const AutomationsInProgress = (
8
+ props: IconBaseProps,
9
+ ): JSX.Element => <Icon {...props} icon={AutomationsInProgressSvg} />;
10
+
11
+ AutomationsInProgress.displayName = 'AutomationsInProgress';
12
+ export default AutomationsInProgress;
@@ -0,0 +1,12 @@
1
+ // This icon file is generated automatically.
2
+
3
+ import AutomationsNotStartedSvg from '../ast/AutomationsNotStarted';
4
+ import { Icon } from '../Icon';
5
+ import { IconBaseProps } from '../types';
6
+
7
+ const AutomationsNotStarted = (
8
+ props: IconBaseProps,
9
+ ): JSX.Element => <Icon {...props} icon={AutomationsNotStartedSvg} />;
10
+
11
+ AutomationsNotStarted.displayName = 'AutomationsNotStarted';
12
+ export default AutomationsNotStarted;
@@ -0,0 +1,12 @@
1
+ // This icon file is generated automatically.
2
+
3
+ import AutomationsSuccessSvg from '../ast/AutomationsSuccess';
4
+ import { Icon } from '../Icon';
5
+ import { IconBaseProps } from '../types';
6
+
7
+ const AutomationsSuccess = (
8
+ props: IconBaseProps,
9
+ ): JSX.Element => <Icon {...props} icon={AutomationsSuccessSvg} />;
10
+
11
+ AutomationsSuccess.displayName = 'AutomationsSuccess';
12
+ export default AutomationsSuccess;
@@ -9,4 +9,4 @@ const Chat = (
9
9
  ): JSX.Element => <Icon {...props} icon={ChatSvg} />;
10
10
 
11
11
  Chat.displayName = 'Chat';
12
- export default Chat;
12
+ export default Chat;
@@ -9,4 +9,4 @@ const ChatAi = (
9
9
  ): JSX.Element => <Icon {...props} icon={ChatAiSvg} />;
10
10
 
11
11
  ChatAi.displayName = 'ChatAi';
12
- export default ChatAi;
12
+ export default ChatAi;
@@ -9,4 +9,4 @@ const Headphones = (
9
9
  ): JSX.Element => <Icon {...props} icon={HeadphonesSvg} />;
10
10
 
11
11
  Headphones.displayName = 'Headphones';
12
- export default Headphones;
12
+ export default Headphones;
@@ -9,4 +9,4 @@ const HeadphonesAi = (
9
9
  ): JSX.Element => <Icon {...props} icon={HeadphonesAiSvg} />;
10
10
 
11
11
  HeadphonesAi.displayName = 'HeadphonesAi';
12
- export default HeadphonesAi;
12
+ export default HeadphonesAi;
@@ -31,6 +31,12 @@ export { default as ArrowRight } from './ArrowRight';
31
31
  export { default as ArrowTop } from './ArrowTop';
32
32
  export { default as AskForInput } from './AskForInput';
33
33
  export { default as Atom } from './Atom';
34
+ export { default as AutomationsCancelled } from './AutomationsCancelled';
35
+ export { default as AutomationsDisabled } from './AutomationsDisabled';
36
+ export { default as AutomationsError } from './AutomationsError';
37
+ export { default as AutomationsInProgress } from './AutomationsInProgress';
38
+ export { default as AutomationsNotStarted } from './AutomationsNotStarted';
39
+ export { default as AutomationsSuccess } from './AutomationsSuccess';
34
40
  export { default as Back } from './Back';
35
41
  export { default as BacklogIcon } from './BacklogIcon';
36
42
  export { default as BatchActionsIcon } from './BatchActionsIcon';
@@ -1,19 +1,33 @@
1
1
  import {ForwardedRef, forwardRef, HTMLAttributes, PropsWithChildren, ReactNode} from "react";
2
- import {border, space, textStyles, themeVars} from "../design-system";
2
+ import {border, colors, layout, lineHeight, space, textStyles, themeVars} from "../design-system";
3
3
  import {css, cx} from "@linaria/core";
4
+ import {ActionsMenuItemProps} from "../actions-menu/actions-menu-item";
5
+ import {useActionsMenuContext} from "../actions-menu";
6
+ import {stopPropagation} from "@fibery/react/src/stop-propagation";
7
+ import {useSetActiveDangerousRow} from "../actions-menu/contexts/actions-menu-dangerous-rows";
8
+ import _ from "lodash";
4
9
 
5
- export interface ListRowProps extends HTMLAttributes<HTMLDivElement> {
10
+ interface ListRowSurfaceBase extends HTMLAttributes<HTMLDivElement> {
11
+ testId?: string;
12
+ }
13
+ export interface ListRowSurfaceProps extends ListRowSurfaceBase {
6
14
  darker?: boolean;
7
- className?: string;
8
15
  selected?: boolean;
9
16
  done?: boolean;
10
17
  focused?: boolean;
11
18
  hovered?: boolean;
12
19
  }
20
+ interface ActionMenuItemSurfaceProps
21
+ extends Omit<ActionsMenuItemProps, "description">,
22
+ Omit<ListRowSurfaceProps, "children" | "darker" | "onSelect"> {
23
+ id?: string;
24
+ showWarning?: boolean;
25
+ onSelect?: (e: Event) => void;
26
+ }
13
27
  export const NATUAL_HOVER_CLASS = "natural-hover";
14
28
  export const NATUAL_FOCUS_CLASS = "natural-focus";
15
- const NATURAL_HOVER = `&.${NATUAL_HOVER_CLASS}:hover`;
16
- const NATURAL_FOCUS = `&.${NATUAL_FOCUS_CLASS}:focus`;
29
+ export const NATURAL_HOVER = `&.${NATUAL_HOVER_CLASS}:hover`;
30
+ export const NATURAL_FOCUS = `&.${NATUAL_FOCUS_CLASS}:focus`;
17
31
 
18
32
  export const listRowSurfaceStylesBase = css`
19
33
  max-width: 100%;
@@ -59,7 +73,9 @@ export const listRowSurfaceStatesDefault = css`
59
73
  cursor: pointer;
60
74
  background-color: ${themeVars.colorBgListItemGeneralHover};
61
75
  }
62
-
76
+ &[data-highlighted] {
77
+ background-color: ${themeVars.colorBgListItemGeneralFocus};
78
+ }
63
79
  ${NATURAL_FOCUS}, &.focus {
64
80
  background-color: ${themeVars.colorBgListItemGeneralFocus};
65
81
  ${NATURAL_HOVER},
@@ -67,7 +83,8 @@ export const listRowSurfaceStatesDefault = css`
67
83
  background-color: ${themeVars.colorBgListItemGeneralFocus};
68
84
  }
69
85
  }
70
- &.done {
86
+ &.done,
87
+ &[data-disabled] {
71
88
  cursor: not-allowed;
72
89
  color: ${themeVars.colorTextListItemGeneralDisabled};
73
90
  background-color: ${themeVars.colorBgListItemGeneralDisabled};
@@ -139,12 +156,54 @@ export const listRowSurfaceStatesDark = css`
139
156
  }
140
157
  `;
141
158
 
159
+ export const actionMenuItemSurface = css`
160
+ margin: 0 ${space.s4}px;
161
+ background-color: ${themeVars.colorBgListItemGeneral};
162
+ ${NATURAL_HOVER},
163
+ &[data-highlighted] {
164
+ background-color: ${themeVars.colorBgActionsMenuItemHover};
165
+ }
166
+ &.done,
167
+ &[data-disabled] {
168
+ cursor: not-allowed;
169
+ color: ${themeVars.colorTextListItemGeneralDisabled};
170
+ background-color: ${themeVars.colorBgListItemGeneralDisabled};
171
+ ${NATURAL_HOVER}, &.hover > * {
172
+ opacity: ${themeVars.opacityListItemGeneralDisabled};
173
+ }
174
+ }
175
+ &.selected {
176
+ cursor: default;
177
+ background-color: ${themeVars.colorBgActionsMenuItemSelected};
178
+ ${NATURAL_HOVER},&.hover, &[data-highlighted] {
179
+ background-color: ${themeVars.colorBgActionsMenuItemSelectedHover};
180
+ }
181
+ }
182
+ &.dangerous {
183
+ background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
184
+ ${NATURAL_HOVER},
185
+ &.hover,
186
+ &[data-highlighted] {
187
+ background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
188
+ }
189
+ }
190
+ &.warm {
191
+ background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
192
+ color: ${colors.inversedTextColor};
193
+ ${NATURAL_HOVER}, &.hover,
194
+ &[data-highlighted] {
195
+ background-color: ${themeVars.colorBgActionsMenuItemDangerActive};
196
+ color: ${colors.inversedTextColor};
197
+ }
198
+ }
199
+ `;
200
+
142
201
  /**
143
202
  * This should be responsible for all flat list items existing in UI in Fibery. Options, action menus and etc.
144
203
  * You can enforce it's state with props focused, hovered, selected, done (use it for disabled too)
145
204
  */
146
205
 
147
- export const ListRowSurface = forwardRef<HTMLDivElement, ListRowProps>(
206
+ export const ListRowSurface = forwardRef<HTMLDivElement, ListRowSurfaceProps>(
148
207
  ({selected, focused, darker, hovered, done, className, children, ...rest}, ref) => {
149
208
  return (
150
209
  <div
@@ -167,8 +226,73 @@ export const ListRowSurface = forwardRef<HTMLDivElement, ListRowProps>(
167
226
  );
168
227
  }
169
228
  );
229
+ export const ActionMenuItemSurface = forwardRef<HTMLDivElement, ActionMenuItemSurfaceProps>(
230
+ (
231
+ {
232
+ testId,
233
+ className,
234
+ loading,
235
+ id,
236
+ selected,
237
+ hovered,
238
+ focused,
239
+ title,
240
+ disabled,
241
+ children,
242
+ dangerous,
243
+ showWarning,
244
+ onSelect = _.noop,
245
+ },
246
+ ref
247
+ ) => {
248
+ const {MenuPrimitive} = useActionsMenuContext();
249
+ const setActiveDangerousRow = useSetActiveDangerousRow();
250
+ const handleSelect = async (e: Event) => {
251
+ e.stopPropagation();
170
252
 
171
- export interface ListRowContentProps extends PropsWithChildren<HTMLAttributes<HTMLDivElement>> {
253
+ if (loading || !id) {
254
+ e.preventDefault();
255
+ return;
256
+ }
257
+
258
+ if (dangerous && !showWarning) {
259
+ e.preventDefault();
260
+ setActiveDangerousRow(id);
261
+ return;
262
+ }
263
+
264
+ setActiveDangerousRow(null);
265
+
266
+ onSelect(e);
267
+ };
268
+ return (
269
+ <MenuPrimitive.Item
270
+ ref={ref}
271
+ data-testid={testId}
272
+ onClick={stopPropagation}
273
+ onSelect={handleSelect}
274
+ title={title ? title : ""}
275
+ className={cx(
276
+ listRowSurfaceStylesBase,
277
+ actionMenuItemSurface,
278
+ className,
279
+ hovered === undefined && NATUAL_HOVER_CLASS,
280
+ focused === undefined && NATUAL_FOCUS_CLASS,
281
+ focused && "focus",
282
+ disabled && "done",
283
+ selected && "selected",
284
+ dangerous && "dangerous",
285
+ showWarning && "warn"
286
+ )}
287
+ disabled={disabled}
288
+ >
289
+ {children}
290
+ </MenuPrimitive.Item>
291
+ );
292
+ }
293
+ );
294
+
295
+ export interface ListRowContentProps extends PropsWithChildren<Omit<HTMLAttributes<HTMLDivElement>, "className">> {
172
296
  rootClassName?: string;
173
297
  before?: ReactNode;
174
298
  after?: ReactNode;
@@ -201,22 +325,11 @@ const ListRowContentSegment = css`
201
325
  `;
202
326
  export const ListRowContent = forwardRef<HTMLDivElement, ListRowContentProps>(
203
327
  (
204
- {
205
- before,
206
- after,
207
- className,
208
- contentRef,
209
- rootClassName,
210
- afterClassName,
211
- beforeClassName,
212
- contentClassName,
213
- children,
214
- ...rest
215
- },
328
+ {before, after, contentRef, rootClassName, afterClassName, beforeClassName, contentClassName, children, ...rest},
216
329
  ref
217
330
  ) => {
218
331
  return (
219
- <div {...rest} ref={ref} className={cx(ListRowContentRoot, rootClassName, className)}>
332
+ <div {...rest} ref={ref} className={cx(ListRowContentRoot, rootClassName)}>
220
333
  {before ? <div className={cx(ListRowLayoutSegment, beforeClassName)}>{before}</div> : null}
221
334
  <div ref={contentRef} className={cx(ListRowContentSegment, contentClassName)}>
222
335
  {children}
@@ -226,3 +339,30 @@ export const ListRowContent = forwardRef<HTMLDivElement, ListRowContentProps>(
226
339
  );
227
340
  }
228
341
  );
342
+
343
+ interface ActionMenuItemContentProps extends ListRowContentProps {
344
+ description?: string;
345
+ }
346
+ const actionMenuItemDescriptionClass = css`
347
+ ${textStyles.small};
348
+ line-height: ${lineHeight.nowrap};
349
+ color: ${themeVars.accentTextColor};
350
+ padding: ${space.s2}px ${space.s8}px 0;
351
+ `;
352
+ const actionMenuItemContentClass = css`
353
+ line-height: ${layout.menuItemHeight}px;
354
+ padding: 0 ${space.s8}px;
355
+ `;
356
+ const actionMenuItemWithDescription = css`
357
+ min-height: ${layout.itemWithSubtitleHeight}px;
358
+ `;
359
+ export const ActionMenuItemContent = forwardRef<HTMLDivElement, ActionMenuItemContentProps>(
360
+ ({contentClassName, description, ...rest}, ref) => {
361
+ return (
362
+ <div className={cx(description && actionMenuItemWithDescription)}>
363
+ <ListRowContent contentClassName={cx(contentClassName, actionMenuItemContentClass)} ref={ref} {...rest} />
364
+ {description ? <div className={actionMenuItemDescriptionClass}>{description}</div> : null}
365
+ </div>
366
+ );
367
+ }
368
+ );
@@ -1,24 +1,38 @@
1
1
  import {GroupBase} from "../index";
2
- import {components} from "react-windowed-select";
3
- import {GroupHeadingProps} from "react-select";
2
+ import {GroupHeadingProps, components} from "react-select";
4
3
  import {css} from "@linaria/core";
5
4
  import {layout, space, textStyles, themeVars} from "../../design-system";
6
5
  import React from "react";
6
+ import {isPureTextChildren} from "../util";
7
+ import {TooltipIfOverflown} from "../../tooltip-if-overflown";
8
+
7
9
  export const groupHeadingNulledStyles = {
8
10
  paddingLeft: "",
9
11
  paddingRight: "",
10
12
  paddingTop: "",
13
+ marginBottom: "",
14
+ display: "",
11
15
  margin: "",
12
16
  ...textStyles.small,
13
17
  color: "",
14
18
  height: layout.menuItemHeight,
15
19
  };
16
20
  const groupHeadingClass = css`
21
+ min-height: ${layout.menuItemHeight}px;
22
+ color: ${themeVars.disabledTextColor};
23
+ padding: 0 ${space.s6}px 0 ${space.s8}px;
24
+ display: flex;
25
+ align-content: flex-end;
26
+ align-items: flex-end;
17
27
  white-space: nowrap;
18
28
  overflow: hidden;
19
29
  text-overflow: ellipsis;
20
- padding: ${space.s8 + space.s4}px ${space.s8}px ${space.s12}px ${space.s8}px;
21
- color: ${themeVars.disabledTextColor};
30
+ `;
31
+ const groupHeadingContentClass = css`
32
+ white-space: nowrap;
33
+ flex-basis: 100%;
34
+ overflow: hidden;
35
+ text-overflow: ellipsis;
22
36
  `;
23
37
  const groupDividerContainerStyle = css`
24
38
  padding: ${space.s12}px 0;
@@ -42,13 +56,22 @@ export function GroupHeading<
42
56
  if (props.data.separator) {
43
57
  return <GroupDivider />;
44
58
  }
45
-
46
- if (!children) {
47
- return null;
48
- }
59
+ const isPureText = isPureTextChildren(children);
49
60
  return (
50
61
  <components.GroupHeading {...props} className={groupHeadingClass}>
51
- {children}
62
+ {isPureText ? (
63
+ <TooltipIfOverflown title={children} side={"right"}>
64
+ {({setNode}) => {
65
+ return (
66
+ <div ref={setNode} className={groupHeadingContentClass}>
67
+ {children}
68
+ </div>
69
+ );
70
+ }}
71
+ </TooltipIfOverflown>
72
+ ) : (
73
+ <div className={groupHeadingContentClass}>{children}</div>
74
+ )}
52
75
  </components.GroupHeading>
53
76
  );
54
77
  }
@@ -0,0 +1,186 @@
1
+ import {GroupHeadingProps, GroupProps, MenuListProps} from "react-select";
2
+ import {ItemContent, Virtuoso, VirtuosoHandle} from "react-virtuoso";
3
+ import {Children, ComponentType, ReactNode, useContext, useEffect, useRef} from "react";
4
+ import {GroupBase, ReactSelectRefContext} from "../util";
5
+ import {layout} from "../../../src/design-system";
6
+ import _, {isFunction, isObject} from "lodash";
7
+
8
+ const OPTION_HEIGHT = layout.menuItemHeight;
9
+ // this code is logic stolen from windowed-select SCROLL_INTO_VIEW_CHECK_FIX prevents list scrolling when focusing
10
+ // partially visible option. set to 0 to scroll into view any partially hidden focused option every time
11
+ // windowed select default is .33. We've got some precision here, can go with .2
12
+ const MINIMAL_VISIBLE_PORTION_TO_SCROLL_INTO_VIEW = 0.2;
13
+ const SCROLL_INTO_VIEW_CHECK_FIX = OPTION_HEIGHT - OPTION_HEIGHT * MINIMAL_VISIBLE_PORTION_TO_SCROLL_INTO_VIEW;
14
+ function isGroupChild<Option, isMulty extends boolean, Group extends GroupBase<Option> = GroupBase<Option>>(
15
+ props: unknown
16
+ ): props is GroupProps<Option, isMulty, Group> {
17
+ return (
18
+ isObject(props) &&
19
+ "options" in props &&
20
+ "Heading" in props &&
21
+ isFunction(props.Heading) &&
22
+ "headingProps" in props &&
23
+ isObject(props.headingProps)
24
+ );
25
+ }
26
+ const flattenGroupChildren: (
27
+ children: ReactNode,
28
+ renderGroup: (Component: ComponentType<GroupHeadingProps>, props: GroupProps["headingProps"]) => ReactNode
29
+ ) => ReactNode[] = (children, renderGroup) => {
30
+ return _.compact(
31
+ Children.toArray(children)
32
+ .map((child) => {
33
+ if (isObject(child) && "props" in child && isObject(child.props)) {
34
+ const {props} = child;
35
+ if (isGroupChild(props)) {
36
+ //well, it's a group
37
+ const children = _.compact(Children.toArray(props.children));
38
+ if (!children.length) {
39
+ // do not render empty groups
40
+ return [];
41
+ }
42
+ const heading = renderGroup(props.Heading, props.headingProps);
43
+ return [heading, ...children];
44
+ }
45
+ }
46
+ //if not a group, just keep child as is
47
+ return child;
48
+ })
49
+ .flat()
50
+ );
51
+ };
52
+ const drawListElement: ItemContent<ReactNode, never> = (index, child) => child;
53
+ export function MenuListVirtualized<
54
+ Option,
55
+ IsMulti extends boolean = boolean,
56
+ Group extends GroupBase<Option> = GroupBase<Option>
57
+ >({
58
+ children,
59
+ focusedOption,
60
+ innerRef,
61
+ maxHeight,
62
+ theme,
63
+ getStyles,
64
+ cx,
65
+ selectProps,
66
+ }: MenuListProps<Option, IsMulti, Group>) {
67
+ const virtuoso = useRef<VirtuosoHandle>(null);
68
+ // options props come non-filtered and we have insufficient info (props) to render groups and options from here.
69
+ // Also you never know what contents also possible as list content.
70
+ // that's why it is better working with children and find options and groups among them (hope internal API does not change)
71
+ // The best strategy to scroll to index and to evade height calculations is to
72
+ // deconstruct group into it's header and children (options) and render then side by side without group-wrapping div
73
+ // then we just use integrated scrollToIndex fromVirtuoso API to scroll to desired option (most reliable variant)
74
+ // that's why Group is just a dummy component that does nothing: to minimize impact of Group component being rendered inside ReactSelect
75
+ const renderGroup = (Component: ComponentType<GroupHeadingProps>, headingProps: GroupProps["headingProps"]) => {
76
+ const content = selectProps.formatGroupLabel
77
+ ? selectProps.formatGroupLabel(headingProps.data as Group)
78
+ : headingProps.data.label;
79
+ return content ? (
80
+ <Component
81
+ cx={cx}
82
+ selectProps={selectProps as unknown as GroupHeadingProps["selectProps"]}
83
+ theme={theme}
84
+ getStyles={getStyles}
85
+ {...headingProps}
86
+ >
87
+ {content}
88
+ </Component>
89
+ ) : null;
90
+ };
91
+ const select = useContext(ReactSelectRefContext);
92
+
93
+ const flattenChildren = flattenGroupChildren(children, renderGroup);
94
+ //fix for select not having focused option when open. if no focused item present, at least first option must be focused.
95
+ if (select.current && flattenChildren.length && !focusedOption) {
96
+ let fitstExistingOption: unknown;
97
+ let selectedOption: unknown;
98
+ //if an options exists, but there is no focused option, looking for first selected / existing option
99
+ for (let i = 0; i < flattenChildren.length; i++) {
100
+ const child = flattenChildren[i];
101
+ if (
102
+ isObject(child) &&
103
+ "props" in child &&
104
+ isObject(child.props) &&
105
+ "type" in child.props &&
106
+ child.props.type === "option" &&
107
+ "isSelected" in child.props
108
+ ) {
109
+ //well it is an option, check if it has data (just for ts and be sure)
110
+ if ("data" in child.props && isObject(child.props.data)) {
111
+ if ("isSelected" in child.props && child.props.isSelected) {
112
+ //setting selected option data as focused option, wrapping up
113
+ selectedOption = child.props.data;
114
+ break;
115
+ }
116
+ if (!fitstExistingOption) {
117
+ fitstExistingOption = child.props.data;
118
+ }
119
+ }
120
+ }
121
+ }
122
+ if (selectedOption) {
123
+ select.current.setState({focusedOption: selectedOption});
124
+ } else if (fitstExistingOption) {
125
+ select.current.setState({focusedOption: fitstExistingOption});
126
+ }
127
+ }
128
+ const menuHeight = Math.min(flattenChildren.length * layout.menuItemHeight, maxHeight);
129
+ //scrolling to focused option
130
+ useEffect(() => {
131
+ let to: ReturnType<typeof setTimeout> | undefined;
132
+ if (focusedOption && virtuoso.current) {
133
+ const index = flattenChildren.findIndex((child) => {
134
+ if (isObject(child) && "props" in child) {
135
+ if (isObject(child.props) && "data" in child.props) {
136
+ return child.props.data === focusedOption;
137
+ }
138
+ }
139
+ return false;
140
+ });
141
+ if (index >= 0) {
142
+ const focusedOptionPosition = index * OPTION_HEIGHT;
143
+ virtuoso.current.getState((state) => {
144
+ to = setTimeout(() => {
145
+ if (!virtuoso.current) {
146
+ return;
147
+ }
148
+ if (state.scrollTop - SCROLL_INTO_VIEW_CHECK_FIX > focusedOptionPosition) {
149
+ virtuoso.current.scrollToIndex({index: index, behavior: "auto"});
150
+ } else if (
151
+ state.scrollTop + menuHeight <
152
+ focusedOptionPosition + OPTION_HEIGHT - SCROLL_INTO_VIEW_CHECK_FIX
153
+ ) {
154
+ virtuoso.current.scrollToIndex({
155
+ index: index,
156
+ align: "end",
157
+ behavior: "auto",
158
+ });
159
+ }
160
+ }, 1);
161
+ // this just makes it smooth. Because of constant rerendering (or IDK why) Virtuoso state and actual scroll
162
+ // value desync, like actual element do not scroll last frame, this stabilises the behaviour and force sync it
163
+ virtuoso.current?.scrollTo({top: state.scrollTop});
164
+ });
165
+ }
166
+ }
167
+ return () => {
168
+ clearTimeout(to);
169
+ };
170
+ }, [focusedOption, flattenChildren, menuHeight, virtuoso]);
171
+ return (
172
+ // Needed for correct display inside portals and etc.
173
+ <div>
174
+ <Virtuoso
175
+ ref={virtuoso}
176
+ // in reality only HTMLDiveElement | null will appear here. We do not use Virtuoso's useWindowScroll prop
177
+ scrollerRef={innerRef as (ref: HTMLElement | Window | null) => void}
178
+ style={{height: menuHeight}}
179
+ fixedItemHeight={layout.menuItemHeight}
180
+ data={flattenChildren}
181
+ totalCount={flattenChildren.length}
182
+ itemContent={drawListElement}
183
+ />
184
+ </div>
185
+ );
186
+ }
@@ -1,7 +1,6 @@
1
1
  import {GroupBase} from "../index";
2
- import {CSSObjectWithLabel, MenuProps} from "react-select";
2
+ import {CSSObjectWithLabel, MenuProps, components} from "react-select";
3
3
  import cn from "classnames";
4
- import {components} from "react-windowed-select";
5
4
  import {css} from "@linaria/core";
6
5
  import {border, space, themeVars} from "../../design-system";
7
6
 
@@ -1,10 +1,26 @@
1
1
  import React, {ReactNode} from "react";
2
+ import {ListRowContent} from "../../lists/list-row-surface";
2
3
  import {css} from "@linaria/core";
3
- import {space} from "../../design-system";
4
+ import {space, layout, textStyles} from "../../design-system";
4
5
 
5
- const NopOptionMessageClass = css`
6
- padding: ${space.s12 + 2}px ${space.s8}px;
6
+ const noOptionMessageClass = css`
7
+ padding: 0 ${space.s6}px;
8
+ cursor: default;
9
+ user-select: none;
7
10
  `;
11
+ const noOptionMessageContentClass = css`
12
+ padding: 0 ${space.s6}px;
13
+ line-height: ${layout.menuItemHeight}px;
14
+ white-space: nowrap;
15
+ overflow: hidden;
16
+ text-overflow: ellipsis;
17
+ ${{...textStyles.regular}}
18
+ `;
19
+
8
20
  export const NoOptionsMessage = ({children}: {children: ReactNode}) => {
9
- return <div className={NopOptionMessageClass}>{children}</div>;
21
+ return (
22
+ <div className={noOptionMessageClass}>
23
+ <ListRowContent contentClassName={noOptionMessageContentClass}>{children}</ListRowContent>
24
+ </div>
25
+ );
10
26
  };
@@ -1,12 +1,12 @@
1
- import {OptionProps} from "react-select";
2
- import {components} from "react-windowed-select";
1
+ import {OptionProps, components} from "react-select";
3
2
  import {GroupBase} from "../index";
4
3
  import {css} from "@linaria/core";
5
4
  import {layout, space} from "../../design-system";
6
5
  import {ListRowContent, ListRowSurface} from "../../lists/list-row-surface";
7
6
  import cn from "classnames";
8
- import {Children, ReactNode, useMemo} from "react";
7
+ import {ComponentProps, useMemo} from "react";
9
8
  import {TooltipIfOverflown} from "../../tooltip-if-overflown";
9
+ import {isPureTextChildren} from "../util";
10
10
 
11
11
  export const OptionNulledVitualizedStyles = {
12
12
  height: layout.menuItemHeight,
@@ -31,11 +31,9 @@ const OptionRootClass = css`
31
31
  max-width: 100%;
32
32
  `;
33
33
  const OptionWithTextContent = css`
34
- padding-left: 0 ${space.s6}px;
34
+ padding-left: ${space.s6}px;
35
35
  `;
36
- function isOptionContentIsPureText(children: ReactNode) {
37
- return !Children.toArray(children).find((child) => !(typeof child === "string" || typeof child === "number"));
38
- }
36
+
39
37
  /**
40
38
  * Attention! Do not use this option for potentially long lists inside React Select
41
39
  * Why is this option slow? Read here https://the.fibery.io/SoftDev/bug/Select-option-behaviour-is-incorrect-9413
@@ -46,16 +44,19 @@ export function OptionSlow<
46
44
  Group extends GroupBase<Option> = GroupBase<Option>
47
45
  >({children, ...rest}: OptionProps<Option, IsMulti, Group>) {
48
46
  const {isDisabled, isSelected, isFocused} = rest;
49
- const isPureText = useMemo(() => isOptionContentIsPureText(children), [children]);
47
+ const isPureText = useMemo(() => isPureTextChildren(children), [children]);
48
+ const surfaceProps: ComponentProps<typeof ListRowSurface> = {
49
+ focused: isFocused,
50
+ selected: isSelected,
51
+ done: isDisabled,
52
+ };
53
+ if ("onMouseOver" in rest.innerProps) {
54
+ surfaceProps.hovered = false; // disable hover if the option is actually slow
55
+ }
50
56
  return (
51
57
  <components.Option {...rest} className={cn(rest.className, OptionRootClass)}>
52
- {isPureText ? (
53
- <ListRowSurface
54
- focused={isFocused}
55
- selected={isSelected}
56
- hovered={rest.innerProps.onMouseOver ? undefined : false} //if onMouseOver is not passed (fast option used) then enabling natural hover by passing undefined so we not forcing non-hover state
57
- done={isDisabled}
58
- >
58
+ <ListRowSurface {...surfaceProps}>
59
+ {isPureText ? (
59
60
  <TooltipIfOverflown title={children} side={"right"} sideOffset={space.s6} visible={isFocused}>
60
61
  {({setNode}) => {
61
62
  return (
@@ -65,17 +66,10 @@ export function OptionSlow<
65
66
  );
66
67
  }}
67
68
  </TooltipIfOverflown>
68
- </ListRowSurface>
69
- ) : (
70
- <ListRowSurface
71
- focused={isFocused}
72
- selected={isSelected}
73
- hovered={rest.innerProps.onMouseOver ? undefined : false} //if onMouseOver is not passed (fast option used) then enabling natural hover by passing undefined so we not forcing non-hover state
74
- done={isDisabled}
75
- >
76
- <ListRowContent>{children}</ListRowContent>
77
- </ListRowSurface>
78
- )}
69
+ ) : (
70
+ children
71
+ )}
72
+ </ListRowSurface>
79
73
  </components.Option>
80
74
  );
81
75
  }
@@ -1,9 +1,8 @@
1
- import {forwardRef, useCallback, useMemo, useState} from "react";
1
+ import {forwardRef, RefObject, useCallback, useMemo, useRef, useState} from "react";
2
2
  import BaseSelect, {
3
3
  ActionMeta,
4
4
  components as reactSelectComponents,
5
5
  createFilter,
6
- GroupBase as ReactSelectGroupBase,
7
6
  MenuListProps,
8
7
  MenuProps,
9
8
  mergeStyles,
@@ -14,12 +13,12 @@ import BaseSelect, {
14
13
  OptionsOrGroups,
15
14
  Props as BaseSelectProps,
16
15
  PropsValue,
16
+ SelectInstance,
17
17
  SingleValue,
18
18
  SingleValueProps,
19
19
  StylesConfig,
20
20
  } from "react-select";
21
21
  import BaseCreatableSelect, {CreatableProps} from "react-select/creatable";
22
- import WindowedSelect, {components, WindowedMenuList} from "react-windowed-select";
23
22
  import {componentsStyles, singleLineComponentsStyle, virtualizedStyles, zIndexStyles} from "./styles";
24
23
  import {useSelectControlSettings} from "./select-control-settings-context";
25
24
  import {Option, OptionSlow} from "./custom-select-partials/option";
@@ -28,9 +27,9 @@ import {GroupHeading} from "./custom-select-partials/group-heading";
28
27
  import {DropdownIndicator} from "./custom-select-partials/drop-down-indicator";
29
28
  import {NoOptionsMessage} from "./custom-select-partials/no-option-message";
30
29
  import {ClearIndicator} from "./custom-select-partials/clear-indicator";
31
- import {isObject} from "lodash";
32
-
33
- type GroupBase<T> = ReactSelectGroupBase<T> & {separator?: boolean};
30
+ import {countOptions, GroupBase, ReactSelectRefContext} from "./util";
31
+ import {MenuListVirtualized} from "./custom-select-partials/menu-list-virtualized";
32
+ import {useComposedRefs} from "@fibery/react/src/use-composed-refs";
34
33
 
35
34
  export type {
36
35
  OptionsOrGroups,
@@ -47,10 +46,13 @@ export type {
47
46
  OnChangeValue,
48
47
  PropsValue,
49
48
  };
50
- export {WindowedMenuList};
51
-
52
- export {components};
49
+ export {reactSelectComponents as components};
50
+ export {MenuListVirtualized};
53
51
 
52
+ //Group dummy for virtualized option-lists. For performance.
53
+ function GroupVirtualized() {
54
+ return null;
55
+ }
54
56
  export function combineStyles<
55
57
  Option,
56
58
  IsMulti extends boolean = boolean,
@@ -70,6 +72,7 @@ export type SelectProps<
70
72
  isCollectionMode?: IsMulti;
71
73
  disabled?: boolean;
72
74
  };
75
+
73
76
  export function SingleRowSelect<
74
77
  Option = unknown,
75
78
  IsMulti extends boolean = boolean,
@@ -87,22 +90,6 @@ export function SingleRowSelect<
87
90
  }
88
91
 
89
92
  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
-
106
93
  /**
107
94
  * Draws a select input.
108
95
  * Please use formatOptionLabel instead of Option component override. Select has smart optimisations for large lists
@@ -175,8 +162,6 @@ export const Select = forwardRef(function Select<
175
162
  [onKeyDown, menuOpenState]
176
163
  );
177
164
 
178
- const SelectComponent = (virtualized ? WindowedSelect : BaseSelect) as unknown as typeof BaseSelect;
179
-
180
165
  const combinedStyles = useMemo(() => {
181
166
  return combineStyles<Option, IsMulti, Group>([
182
167
  componentsStyles,
@@ -185,41 +170,51 @@ export const Select = forwardRef(function Select<
185
170
  styles,
186
171
  ] as StylesConfig<Option, IsMulti, Group>[]);
187
172
  }, [styles, virtualized, zIndex]);
188
-
173
+ const contextRef = useRef<SelectInstance<Option, IsMulti, Group> | null>(null);
174
+ const combined = useComposedRefs(forwardedRef, contextRef);
189
175
  return (
190
- <SelectComponent
191
- ref={forwardedRef}
192
- // There are places, where it is useful to override menuPortalTarget globally for all selects.
193
- // When you are already in some popup. e.g. user input in buttons, relation filter popover.
194
- menuPortalTarget={(getPopupContainerElement && getPopupContainerElement()) || menuPortalTarget}
195
- menuPlacement={"auto"}
196
- styles={combinedStyles}
197
- isMulti={isCollectionMode}
198
- backspaceRemovesValue={isCollectionMode}
199
- tabSelectsValue={false}
200
- components={{
201
- Menu: Menu,
202
- DropdownIndicator,
203
- ClearIndicator,
204
- /*
205
- Fast Options break option focus on hover behaviour but help with slow React Select performance with large options list.
206
- This isolates bug for big lists only
207
- Read Option definition for more info
208
- bug here: https://the.fibery.io/SoftDev/bug/Select-option-behaviour-is-incorrect-9413
209
- */
210
- Option: optionsCount > FAST_OPTION_USAGE_THRESHOLD ? Option : OptionSlow,
211
- MultiValueRemove: ClearIndicator,
212
- NoOptionsMessage,
213
- GroupHeading,
214
- ...components,
215
- }}
216
- {...rest}
217
- onKeyDown={handleKeyDown}
218
- onMenuOpen={onMenuOpenWrapped}
219
- onMenuClose={onMenuCloseWrapped}
220
- onChange={rest.disabled ? undefined : onChange}
221
- isDisabled={rest.disabled}
222
- />
176
+ <ReactSelectRefContext.Provider value={contextRef as RefObject<SelectInstance>}>
177
+ <BaseSelect<Option, IsMulti, Group>
178
+ ref={combined}
179
+ // There are places, where it is useful to override menuPortalTarget globally for all selects.
180
+ // When you are already in some popup. e.g. user input in buttons, relation filter popover.
181
+ menuPortalTarget={(getPopupContainerElement && getPopupContainerElement()) || menuPortalTarget}
182
+ menuPlacement={"auto"}
183
+ styles={combinedStyles}
184
+ isMulti={isCollectionMode}
185
+ backspaceRemovesValue={isCollectionMode}
186
+ tabSelectsValue={false}
187
+ components={{
188
+ Menu: Menu,
189
+ DropdownIndicator,
190
+ ClearIndicator,
191
+ /*
192
+ Fast Options break option focus on hover behaviour but help with slow React Select performance with large options list.
193
+ This isolates bug for big lists only
194
+ Read Option definition for more info
195
+ bug here: https://the.fibery.io/SoftDev/bug/Select-option-behaviour-is-incorrect-9413
196
+ */
197
+ MultiValueRemove: ClearIndicator,
198
+ NoOptionsMessage,
199
+ GroupHeading,
200
+ Option: optionsCount > FAST_OPTION_USAGE_THRESHOLD ? Option : OptionSlow,
201
+ ...components,
202
+ //Using dummy for virtualized lists. Virtualized lists render Groups differently (look MenuListVirtualized).
203
+ Group: virtualized ? GroupVirtualized : components?.Group ? components.Group : reactSelectComponents.Group,
204
+ MenuList: virtualized
205
+ ? MenuListVirtualized
206
+ : components?.MenuList
207
+ ? components.MenuList
208
+ : reactSelectComponents.MenuList,
209
+ }}
210
+ {...rest}
211
+ onKeyDown={handleKeyDown}
212
+ onMenuOpen={onMenuOpenWrapped}
213
+ onMenuClose={onMenuCloseWrapped}
214
+ onChange={rest.disabled ? undefined : onChange}
215
+ isDisabled={rest.disabled}
216
+ />
217
+ </ReactSelectRefContext.Provider>
223
218
  );
224
219
  // eslint-disable-next-line no-use-before-define
225
220
  }) as <Option = unknown, IsMulti extends boolean = boolean, Group extends GroupBase<Option> = GroupBase<Option>>(
@@ -256,6 +251,7 @@ export function CreatableSelectInner<
256
251
  const optionsCount = useMemo(() => {
257
252
  return countOptions<Option, Group>(rest.options);
258
253
  }, [rest.options]);
254
+
259
255
  return (
260
256
  <BaseCreatableSelect
261
257
  ref={ref}
@@ -270,7 +266,6 @@ export function CreatableSelectInner<
270
266
  tabSelectsValue={false}
271
267
  components={{
272
268
  Menu: Menu,
273
- MenuList: virtualized ? WindowedMenuList : reactSelectComponents.MenuList,
274
269
  DropdownIndicator,
275
270
  ClearIndicator,
276
271
  /*
@@ -279,11 +274,17 @@ export function CreatableSelectInner<
279
274
  Read Option definition for more info
280
275
  bug here: https://the.fibery.io/SoftDev/bug/Select-option-behaviour-is-incorrect-9413
281
276
  */
282
- Option: optionsCount > FAST_OPTION_USAGE_THRESHOLD ? Option : OptionSlow,
283
277
  MultiValueRemove: ClearIndicator,
284
278
  NoOptionsMessage,
285
279
  GroupHeading,
280
+ Option: optionsCount > FAST_OPTION_USAGE_THRESHOLD ? Option : OptionSlow,
286
281
  ...components,
282
+ Group: virtualized ? GroupVirtualized : components?.Group ? components.Group : reactSelectComponents.Group,
283
+ MenuList: virtualized
284
+ ? MenuListVirtualized
285
+ : components?.MenuList
286
+ ? components.MenuList
287
+ : reactSelectComponents.MenuList,
287
288
  }}
288
289
  {...rest}
289
290
  isDisabled={rest.disabled}
@@ -117,18 +117,13 @@ type Props<Option, IsMulti extends boolean, Group extends GroupBase<Option>> = {
117
117
  autoFocus?: boolean;
118
118
  defaultMenuIsOpen?: boolean;
119
119
  placeholder?: string;
120
- onInputChange?: (s: string, actionType: {action: string}) => void;
121
120
  noOptionsMessage: () => string | JSX.Element | null;
122
121
  options: OptionsOrGroups<Option, Group>;
123
122
  filterOption?: SelectProps<Option>["filterOption"];
124
123
  inputValue?: string;
125
124
  disabled?: boolean;
126
- onFocus?: () => void;
127
125
  isCollectionMode?: IsMulti;
128
126
  isClearable?: boolean;
129
- onChange: (newValue: OnChangeValue<Option, IsMulti>, actionMeta: ActionMeta<Option>) => void;
130
- onMenuOpen: () => void;
131
- onMenuClose: () => void | boolean;
132
127
  renderValue?: () => ReactNode;
133
128
  title?: string;
134
129
  maxMenuHeight?: number;
@@ -139,6 +134,11 @@ type Props<Option, IsMulti extends boolean, Group extends GroupBase<Option>> = {
139
134
  formatGroupLabel?: (group: Group) => ReactNode;
140
135
  formatOptionLabel?: (option: Option) => ReactNode;
141
136
  components?: ComponentsConfig;
137
+ onInputChange?: (s: string, actionType: {action: string}) => void;
138
+ onFocus?: () => void;
139
+ onChange: (newValue: OnChangeValue<Option, IsMulti>, actionMeta: ActionMeta<Option>) => void;
140
+ onMenuOpen?: () => void;
141
+ onMenuClose?: () => void | boolean;
142
142
  };
143
143
 
144
144
  function SelectInPopoverInner<Option, IsMulti extends boolean, Group extends GroupBase<Option>>(
@@ -151,18 +151,13 @@ function SelectInPopoverInner<Option, IsMulti extends boolean, Group extends Gro
151
151
  autoFocus,
152
152
  defaultMenuIsOpen,
153
153
  placeholder,
154
- onInputChange,
155
154
  noOptionsMessage,
156
155
  options,
157
156
  filterOption,
158
157
  inputValue,
159
158
  disabled,
160
- onFocus,
161
159
  isCollectionMode,
162
160
  isClearable = true,
163
- onChange,
164
- onMenuOpen,
165
- onMenuClose,
166
161
  renderValue,
167
162
  title,
168
163
  maxMenuHeight,
@@ -173,6 +168,11 @@ function SelectInPopoverInner<Option, IsMulti extends boolean, Group extends Gro
173
168
  formatGroupLabel,
174
169
  formatOptionLabel,
175
170
  components = {},
171
+ onInputChange,
172
+ onFocus,
173
+ onChange,
174
+ onMenuOpen,
175
+ onMenuClose,
176
176
  } = props;
177
177
  const [triggerElement, setTriggerElement] = useState<HTMLDivElement | null>(null);
178
178
  const [visible, setVisible] = useState(menuIsOpen);
@@ -198,7 +198,7 @@ function SelectInPopoverInner<Option, IsMulti extends boolean, Group extends Gro
198
198
 
199
199
  const onHide = useCallback(
200
200
  (restoreFocus = true) => {
201
- const wasClosed = onMenuClose();
201
+ const wasClosed = onMenuClose && onMenuClose();
202
202
  if (wasClosed === false) {
203
203
  return;
204
204
  }
@@ -231,7 +231,9 @@ function SelectInPopoverInner<Option, IsMulti extends boolean, Group extends Gro
231
231
  clearTimeout(blurTimeoutRef.current);
232
232
  }
233
233
  setVisible(true);
234
- onMenuOpen();
234
+ if (onMenuOpen) {
235
+ onMenuOpen();
236
+ }
235
237
  }, [onMenuOpen]);
236
238
 
237
239
  const styles = useMemo(() => {
@@ -0,0 +1,9 @@
1
+ import {PropsWithChildren} from "react";
2
+ import {VirtuosoMockContext} from "react-virtuoso";
3
+ export function SelectMockContext({children}: PropsWithChildren) {
4
+ return (
5
+ <VirtuosoMockContext.Provider value={{viewportHeight: 1000, itemHeight: 32}}>
6
+ {children}
7
+ </VirtuosoMockContext.Provider>
8
+ );
9
+ }
@@ -45,10 +45,8 @@ export const virtualizedStyles: StylesConfig = {
45
45
  option: () => ({...OptionNulledVitualizedStyles, ...OptionNulledStyles}),
46
46
  groupHeading: (provided) => ({
47
47
  ...provided,
48
+ ...groupHeadingNulledStyles,
48
49
  height: layout.menuItemHeight,
49
- whiteSpace: "nowrap",
50
- overflow: "hidden",
51
- textOverflow: "ellipsis",
52
50
  }),
53
51
  };
54
52
 
@@ -0,0 +1,31 @@
1
+ import {GroupBase as ReactSelectGroupBase, OptionsOrGroups, SelectInstance} from "react-select";
2
+ import {isObject} from "lodash";
3
+ import {Children, createContext, ReactNode, RefObject} from "react";
4
+ /**
5
+ * API used by ReactSelect internal partials (at least VirtualizedMenuList).
6
+ */
7
+ export const ReactSelectRefContext = createContext<RefObject<SelectInstance>>({current: null});
8
+ export type GroupBase<T> = ReactSelectGroupBase<T> & {separator?: boolean};
9
+ export function isGroup<Option, Group extends GroupBase<Option>>(
10
+ optionOrGroup: Option | Group
11
+ ): optionOrGroup is Group {
12
+ return isObject(optionOrGroup) && "options" in optionOrGroup;
13
+ }
14
+
15
+ export function countOptions<Option, Group extends GroupBase<Option>>(
16
+ categorizedOptions?: OptionsOrGroups<Option, Group>
17
+ ) {
18
+ if (!categorizedOptions) {
19
+ return 0;
20
+ }
21
+ return categorizedOptions.reduce<number>((optionsAccumulator, categorizedOption) => {
22
+ if (isGroup<Option, Group>(categorizedOption)) {
23
+ return optionsAccumulator + categorizedOption.options.length;
24
+ } else {
25
+ return optionsAccumulator + 1;
26
+ }
27
+ }, 0);
28
+ }
29
+ export function isPureTextChildren(children: ReactNode) {
30
+ return !Children.toArray(children).find((child) => !(typeof child === "string" || typeof child === "number"));
31
+ }
package/index.ts DELETED
@@ -1 +0,0 @@
1
- export * from "./src/design-system";