@spaced-out/ui-design-system 0.1.26 → 0.1.28

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 (73) hide show
  1. package/.cspell/custom-words.txt +7 -0
  2. package/.storybook/main.js +70 -28
  3. package/.storybook/manager-head.html +4 -0
  4. package/.storybook/manager.js +0 -4
  5. package/.storybook/preview-head.html +32 -6
  6. package/.storybook/preview.js +0 -5
  7. package/CHANGELOG.md +41 -0
  8. package/babel.config.js +1 -0
  9. package/lib/components/Badge/Badge.module.css +1 -0
  10. package/lib/components/ButtonDropdown/ButtonDropdown.js +10 -9
  11. package/lib/components/ButtonDropdown/ButtonDropdown.js.flow +6 -4
  12. package/lib/components/Checkbox/Checkbox.js +3 -7
  13. package/lib/components/Checkbox/Checkbox.js.flow +3 -8
  14. package/lib/components/Chip/Chip.js +1 -1
  15. package/lib/components/Chip/Chip.js.flow +2 -2
  16. package/lib/components/CollapsibleCard/CollapsibleCard.js +3 -0
  17. package/lib/components/CollapsibleCard/CollapsibleCard.js.flow +4 -0
  18. package/lib/components/Dialog/Dialog.js +23 -2
  19. package/lib/components/Dialog/Dialog.js.flow +38 -0
  20. package/lib/components/Dropdown/Dropdown.js +10 -9
  21. package/lib/components/Dropdown/Dropdown.js.flow +6 -4
  22. package/lib/components/FocusManager/FocusManager.js +7 -5
  23. package/lib/components/FocusManager/FocusManager.js.flow +3 -3
  24. package/lib/components/InlineDropdown/InlineDropdown.js +10 -9
  25. package/lib/components/InlineDropdown/InlineDropdown.js.flow +6 -4
  26. package/lib/components/Menu/Menu.js +48 -12
  27. package/lib/components/Menu/Menu.js.flow +102 -9
  28. package/lib/components/Menu/Menu.module.css +10 -0
  29. package/lib/components/Menu/MenuOptionButton.js +21 -4
  30. package/lib/components/Menu/MenuOptionButton.js.flow +21 -0
  31. package/lib/components/Modal/Modal.js +35 -8
  32. package/lib/components/Modal/Modal.js.flow +52 -7
  33. package/lib/components/Modal/Modal.module.css +1 -3
  34. package/lib/components/Panel/Panel.js +21 -1
  35. package/lib/components/Panel/Panel.js.flow +30 -1
  36. package/lib/components/Panel/Panel.module.css +0 -1
  37. package/lib/components/Table/DefaultRow.js +5 -5
  38. package/lib/components/Table/DefaultRow.js.flow +14 -11
  39. package/lib/components/Table/StaticTable.js +5 -1
  40. package/lib/components/Table/StaticTable.js.flow +4 -0
  41. package/lib/components/Table/Table.js.flow +2 -0
  42. package/lib/components/Tabs/TabList/TabDropdown.js +10 -9
  43. package/lib/components/Tabs/TabList/TabDropdown.js.flow +6 -4
  44. package/lib/components/Toast/Toast.js +7 -5
  45. package/lib/components/Toast/Toast.js.flow +5 -3
  46. package/lib/components/Tooltip/Tooltip.js +22 -25
  47. package/lib/components/Tooltip/Tooltip.js.flow +25 -22
  48. package/lib/components/Typeahead/Typeahead.js +10 -9
  49. package/lib/components/Typeahead/Typeahead.js.flow +6 -4
  50. package/lib/hooks/index.js +55 -0
  51. package/lib/hooks/index.js.flow +5 -0
  52. package/lib/hooks/useCopyToClipboard.js +31 -0
  53. package/lib/hooks/useCopyToClipboard.js.flow +31 -0
  54. package/lib/hooks/useInputState.js +23 -0
  55. package/lib/hooks/useInputState.js.flow +28 -0
  56. package/lib/hooks/useLockedBody.js +54 -0
  57. package/lib/hooks/useLockedBody.js.flow +55 -0
  58. package/lib/hooks/useToggle.js +18 -0
  59. package/lib/hooks/useToggle.js.flow +17 -0
  60. package/lib/hooks/useWindowSize.js +32 -0
  61. package/lib/hooks/useWindowSize.js.flow +37 -0
  62. package/lib/styles/typography.module.css +1 -0
  63. package/lib/types/common.js +0 -1
  64. package/lib/utils/index.js +11 -0
  65. package/lib/utils/index.js.flow +1 -0
  66. package/lib/utils/menu.js +57 -2
  67. package/lib/utils/menu.js.flow +109 -1
  68. package/lib/utils/string.js +4 -2
  69. package/lib/utils/string.js.flow +3 -0
  70. package/lib/utils/tokens.js +154 -0
  71. package/lib/utils/tokens.js.flow +228 -0
  72. package/package.json +19 -16
  73. package/.storybook/public/favicon.svg +0 -6
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.Dropdown = void 0;
7
7
  var React = _interopRequireWildcard(require("react"));
8
- var _reactDom = require("@floating-ui/react-dom");
8
+ var _react2 = require("@floating-ui/react");
9
9
  var _color = require("../../styles/variables/_color");
10
10
  var _size = require("../../styles/variables/_size");
11
11
  var _space = require("../../styles/variables/_space");
@@ -34,14 +34,14 @@ const Dropdown = /*#__PURE__*/React.forwardRef((_ref, ref) => {
34
34
  const {
35
35
  x,
36
36
  y,
37
- reference,
38
- floating,
37
+ refs,
39
38
  strategy
40
- } = (0, _reactDom.useFloating)({
39
+ } = (0, _react2.useFloating)({
40
+ open: true,
41
41
  strategy: 'absolute',
42
42
  placement: 'bottom-start',
43
- whileElementsMounted: _reactDom.autoUpdate,
44
- middleware: [(0, _reactDom.flip)(), (0, _reactDom.offset)(parseInt(_space.spaceXXSmall))]
43
+ whileElementsMounted: _react2.autoUpdate,
44
+ middleware: [(0, _react2.flip)(), (0, _react2.offset)(parseInt(_space.spaceXXSmall))]
45
45
  });
46
46
  const onMenuToggle = isOpen => {
47
47
  isOpen ? onMenuOpen && onMenuOpen() : onMenuClose && onMenuClose();
@@ -60,7 +60,7 @@ const Dropdown = /*#__PURE__*/React.forwardRef((_ref, ref) => {
60
60
  className: (0, _classify.classify)(_DropdownModule.default.dropdownContainer, classNames?.wrapper),
61
61
  ref: dropdownRef
62
62
  }, /*#__PURE__*/React.createElement(_Input.Input, _extends({}, inputProps, {
63
- boxRef: reference,
63
+ boxRef: refs.setReference,
64
64
  size: size,
65
65
  placeholder: placeholder,
66
66
  value: dropdownInputText,
@@ -76,7 +76,7 @@ const Dropdown = /*#__PURE__*/React.forwardRef((_ref, ref) => {
76
76
  ref: ref
77
77
  })), isOpen && menu && /*#__PURE__*/React.createElement("div", {
78
78
  onClickCapture: cancelNext,
79
- ref: floating,
79
+ ref: refs.setFloating,
80
80
  style: {
81
81
  position: strategy,
82
82
  top: y ?? _space.spaceNone,
@@ -91,7 +91,8 @@ const Dropdown = /*#__PURE__*/React.forwardRef((_ref, ref) => {
91
91
  clickAway();
92
92
  }
93
93
  },
94
- size: menu.size || size
94
+ size: menu.size || size,
95
+ onTabOut: clickAway
95
96
  }))));
96
97
  });
97
98
  });
@@ -10,7 +10,7 @@ import {
10
10
  offset,
11
11
  // $FlowFixMe[untyped-import]
12
12
  useFloating,
13
- } from '@floating-ui/react-dom';
13
+ } from '@floating-ui/react';
14
14
 
15
15
  import {colorBackgroundTertiary} from '../../styles/variables/_color';
16
16
  import {sizeFluid} from '../../styles/variables/_size';
@@ -57,7 +57,8 @@ export const Dropdown: React$AbstractComponent<
57
57
  ref,
58
58
  ): React.Node => {
59
59
  const dropdownRef = React.useRef();
60
- const {x, y, reference, floating, strategy} = useFloating({
60
+ const {x, y, refs, strategy} = useFloating({
61
+ open: true,
61
62
  strategy: 'absolute',
62
63
  placement: 'bottom-start',
63
64
  whileElementsMounted: autoUpdate,
@@ -78,7 +79,7 @@ export const Dropdown: React$AbstractComponent<
78
79
  >
79
80
  <Input
80
81
  {...inputProps}
81
- boxRef={reference}
82
+ boxRef={refs.setReference}
82
83
  size={size}
83
84
  placeholder={placeholder}
84
85
  value={dropdownInputText}
@@ -95,7 +96,7 @@ export const Dropdown: React$AbstractComponent<
95
96
  {isOpen && menu && (
96
97
  <div
97
98
  onClickCapture={cancelNext}
98
- ref={floating}
99
+ ref={refs.setFloating}
99
100
  style={{
100
101
  position: strategy,
101
102
  top: y ?? spaceNone,
@@ -116,6 +117,7 @@ export const Dropdown: React$AbstractComponent<
116
117
  }
117
118
  }}
118
119
  size={menu.size || size}
120
+ onTabOut={clickAway}
119
121
  />
120
122
  </div>
121
123
  )}
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.FocusManager = void 0;
7
7
  var React = _interopRequireWildcard(require("react"));
8
- var _reactDomInteractions = require("@floating-ui/react-dom-interactions");
8
+ var _react2 = require("@floating-ui/react");
9
9
  var _classify = _interopRequireDefault(require("../../utils/classify"));
10
10
  var _FocusManagerModule = _interopRequireDefault(require("./FocusManager.module.css"));
11
11
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -14,20 +14,22 @@ function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj &&
14
14
  function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
15
15
  const FocusManager = props => {
16
16
  const {
17
- floating,
17
+ refs,
18
18
  context
19
- } = (0, _reactDomInteractions.useFloating)();
19
+ } = (0, _react2.useFloating)({
20
+ open: true
21
+ });
20
22
  const {
21
23
  classNames,
22
24
  children,
23
25
  initialFocus = -1,
24
26
  ...restFloatingFocusManagerProps
25
27
  } = props;
26
- return /*#__PURE__*/React.createElement(_reactDomInteractions.FloatingFocusManager, _extends({
28
+ return /*#__PURE__*/React.createElement(_react2.FloatingFocusManager, _extends({
27
29
  context: context,
28
30
  initialFocus: initialFocus
29
31
  }, restFloatingFocusManagerProps), /*#__PURE__*/React.createElement("div", {
30
- ref: floating,
32
+ ref: refs.setFloating,
31
33
  "data-testid": "FocusManager",
32
34
  className: (0, _classify.default)(_FocusManagerModule.default.wrapper, classNames?.wrapper)
33
35
  }, children));
@@ -6,7 +6,7 @@ import {
6
6
  FloatingFocusManager,
7
7
  // $FlowFixMe[untyped-import]
8
8
  useFloating,
9
- } from '@floating-ui/react-dom-interactions';
9
+ } from '@floating-ui/react';
10
10
 
11
11
  import classify from '../../utils/classify';
12
12
 
@@ -24,7 +24,7 @@ export type FocusManagerProps = {
24
24
  };
25
25
 
26
26
  export const FocusManager = (props: FocusManagerProps): React.Node => {
27
- const {floating, context} = useFloating();
27
+ const {refs, context} = useFloating({open: true});
28
28
  const {
29
29
  classNames,
30
30
  children,
@@ -39,7 +39,7 @@ export const FocusManager = (props: FocusManagerProps): React.Node => {
39
39
  {...restFloatingFocusManagerProps}
40
40
  >
41
41
  <div
42
- ref={floating}
42
+ ref={refs.setFloating}
43
43
  data-testid="FocusManager"
44
44
  className={classify(css.wrapper, classNames?.wrapper)}
45
45
  >
@@ -5,7 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.InlineDropdown = void 0;
7
7
  var React = _interopRequireWildcard(require("react"));
8
- var _reactDom = require("@floating-ui/react-dom");
8
+ var _react2 = require("@floating-ui/react");
9
9
  var _space = require("../../styles/variables/_space");
10
10
  var _classify = require("../../utils/classify");
11
11
  var _clickAway = require("../../utils/click-away");
@@ -36,14 +36,14 @@ const InlineDropdown = /*#__PURE__*/React.forwardRef((_ref, ref) => {
36
36
  const {
37
37
  x,
38
38
  y,
39
- reference,
40
- floating,
39
+ refs,
41
40
  strategy
42
- } = (0, _reactDom.useFloating)({
41
+ } = (0, _react2.useFloating)({
42
+ open: true,
43
43
  strategy: 'absolute',
44
44
  placement: anchorPosition,
45
- whileElementsMounted: _reactDom.autoUpdate,
46
- middleware: [(0, _reactDom.shift)(), (0, _reactDom.flip)(), (0, _reactDom.offset)(parseInt(_space.spaceXXSmall))]
45
+ whileElementsMounted: _react2.autoUpdate,
46
+ middleware: [(0, _react2.shift)(), (0, _react2.flip)(), (0, _react2.offset)(parseInt(_space.spaceXXSmall))]
47
47
  });
48
48
  const onMenuToggle = isOpen => {
49
49
  isOpen ? onMenuOpen && onMenuOpen() : onMenuClose && onMenuClose();
@@ -63,7 +63,7 @@ const InlineDropdown = /*#__PURE__*/React.forwardRef((_ref, ref) => {
63
63
  ref: menuBtnRef
64
64
  }, /*#__PURE__*/React.createElement(_Button.UnstyledButton, _extends({}, restButtonProps, {
65
65
  disabled: disabled,
66
- ref: reference,
66
+ ref: refs.setReference,
67
67
  onClick: e => {
68
68
  e.stopPropagation();
69
69
  onOpen();
@@ -80,7 +80,7 @@ const InlineDropdown = /*#__PURE__*/React.forwardRef((_ref, ref) => {
80
80
  })
81
81
  })), isOpen && menu && /*#__PURE__*/React.createElement("div", {
82
82
  onClickCapture: cancelNext,
83
- ref: floating,
83
+ ref: refs.setFloating,
84
84
  style: {
85
85
  display: 'flex',
86
86
  position: strategy,
@@ -94,7 +94,8 @@ const InlineDropdown = /*#__PURE__*/React.forwardRef((_ref, ref) => {
94
94
  clickAway();
95
95
  }
96
96
  },
97
- size: menu.size || 'medium'
97
+ size: menu.size || 'medium',
98
+ onTabOut: clickAway
98
99
  }))));
99
100
  });
100
101
  });
@@ -12,7 +12,7 @@ import {
12
12
  shift,
13
13
  // $FlowFixMe[untyped-import]
14
14
  useFloating,
15
- } from '@floating-ui/react-dom';
15
+ } from '@floating-ui/react';
16
16
 
17
17
  import {spaceNone, spaceXXSmall} from '../../styles/variables/_space';
18
18
  import {classify} from '../../utils/classify';
@@ -66,7 +66,8 @@ export const InlineDropdown: React$AbstractComponent<
66
66
  ): React.Node => {
67
67
  const menuBtnRef = React.useRef(null);
68
68
  React.useImperativeHandle(ref, () => menuBtnRef.current);
69
- const {x, y, reference, floating, strategy} = useFloating({
69
+ const {x, y, refs, strategy} = useFloating({
70
+ open: true,
70
71
  strategy: 'absolute',
71
72
  placement: anchorPosition,
72
73
  whileElementsMounted: autoUpdate,
@@ -91,7 +92,7 @@ export const InlineDropdown: React$AbstractComponent<
91
92
  <UnstyledButton
92
93
  {...restButtonProps}
93
94
  disabled={disabled}
94
- ref={reference}
95
+ ref={refs.setReference}
95
96
  onClick={(e) => {
96
97
  e.stopPropagation();
97
98
  onOpen();
@@ -118,7 +119,7 @@ export const InlineDropdown: React$AbstractComponent<
118
119
  {isOpen && menu && (
119
120
  <div
120
121
  onClickCapture={cancelNext}
121
- ref={floating}
122
+ ref={refs.setFloating}
122
123
  style={{
123
124
  display: 'flex',
124
125
  position: strategy,
@@ -138,6 +139,7 @@ export const InlineDropdown: React$AbstractComponent<
138
139
  }
139
140
  }}
140
141
  size={menu.size || 'medium'}
142
+ onTabOut={clickAway}
141
143
  />
142
144
  </div>
143
145
  )}
@@ -6,6 +6,9 @@ Object.defineProperty(exports, "__esModule", {
6
6
  exports.Menu = void 0;
7
7
  var React = _interopRequireWildcard(require("react"));
8
8
  var _classify = require("../../utils/classify");
9
+ var _menu = require("../../utils/menu");
10
+ var _SearchInput = require("../SearchInput");
11
+ var _Text = require("../Text");
9
12
  var _MenuOptionButton = require("./MenuOptionButton");
10
13
  var _MenuModule = _interopRequireDefault(require("./Menu.module.css"));
11
14
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
@@ -18,44 +21,69 @@ const RenderOption = _ref => {
18
21
  composeOptions,
19
22
  groupTitleOptions,
20
23
  classNames,
24
+ searchText = '',
21
25
  ...restProps
22
26
  } = _ref;
27
+ const {
28
+ allowSearch
29
+ } = restProps;
23
30
  if (options && Array.isArray(options) && options.length) {
24
- return /*#__PURE__*/React.createElement(React.Fragment, null, options.map(option => /*#__PURE__*/React.createElement(React.Fragment, {
31
+ const optionsFiltered = !allowSearch ? options : (0, _menu.getFilteredOptionsFromSearchText)(options, searchText);
32
+ const resultText = !allowSearch ? '' : (0, _menu.getFilteredOptionsResultText)(optionsFiltered);
33
+ return /*#__PURE__*/React.createElement(React.Fragment, null, allowSearch && /*#__PURE__*/React.createElement(_Text.FormLabelSmall, {
34
+ className: _MenuModule.default.filterOptionsResultText,
35
+ color: "tertiary"
36
+ }, resultText), optionsFiltered.map((option, idx) => /*#__PURE__*/React.createElement(React.Fragment, {
25
37
  key: option.key
26
38
  }, /*#__PURE__*/React.createElement(_MenuOptionButton.MenuOptionButton, _extends({
27
39
  option: option,
28
40
  classNames: classNames
29
- }, restProps)))));
41
+ }, restProps, {
42
+ isLastItem: idx === optionsFiltered.length - 1
43
+ })))));
30
44
  }
31
45
  if (composeOptions && Array.isArray(composeOptions) && composeOptions.length) {
32
- return /*#__PURE__*/React.createElement(React.Fragment, null, composeOptions.map((composeMenuOptions, index) =>
46
+ const optionsFiltered = !allowSearch ? composeOptions : (0, _menu.getFilteredComposeOptionsFromSearchText)(composeOptions, searchText);
47
+ const resultText = !allowSearch ? '' : (0, _menu.getFilteredComposeOptionsResultText)(optionsFiltered);
48
+ return /*#__PURE__*/React.createElement(React.Fragment, null, allowSearch && /*#__PURE__*/React.createElement(_Text.FormLabelSmall, {
49
+ className: _MenuModule.default.filterOptionsResultText,
50
+ color: "tertiary"
51
+ }, resultText), optionsFiltered.map((composeMenuOptions, index) =>
33
52
  /*#__PURE__*/
34
53
  // eslint-disable-next-line react/no-array-index-key
35
54
  React.createElement("span", {
36
- className: _MenuModule.default.menuDivider,
37
- key: index
38
- }, composeMenuOptions.map(option => /*#__PURE__*/React.createElement(React.Fragment, {
55
+ key: index,
56
+ className: _MenuModule.default.menuDivider
57
+ }, composeMenuOptions.map((option, idx) => /*#__PURE__*/React.createElement(React.Fragment, {
39
58
  key: option.key
40
59
  }, /*#__PURE__*/React.createElement(_MenuOptionButton.MenuOptionButton, _extends({
41
60
  option: option,
42
61
  classNames: classNames
43
- }, restProps)))))));
62
+ }, restProps, {
63
+ isLastItem: index === optionsFiltered.length - 1 && idx === composeMenuOptions.length - 1
64
+ })))))));
44
65
  }
45
66
  if (groupTitleOptions && Array.isArray(groupTitleOptions) && groupTitleOptions.length) {
46
- return /*#__PURE__*/React.createElement(React.Fragment, null, groupTitleOptions.map((optionsGroup, index) =>
67
+ const optionsFiltered = !allowSearch ? groupTitleOptions : (0, _menu.getFilteredGroupTitleOptionsFromSearchText)(groupTitleOptions, searchText);
68
+ const resultText = !allowSearch ? '' : (0, _menu.getFilteredGroupTitleOptionsResultText)(optionsFiltered);
69
+ return /*#__PURE__*/React.createElement(React.Fragment, null, allowSearch && /*#__PURE__*/React.createElement(_Text.FormLabelSmall, {
70
+ className: _MenuModule.default.filterOptionsResultText,
71
+ color: "tertiary"
72
+ }, resultText), optionsFiltered.map((optionsGroup, index) =>
47
73
  /*#__PURE__*/
48
74
  // eslint-disable-next-line react/no-array-index-key
49
75
  React.createElement(React.Fragment, {
50
76
  key: index
51
77
  }, !!optionsGroup.groupTitle && /*#__PURE__*/React.createElement("div", {
52
78
  className: (0, _classify.classify)(_MenuModule.default.groupTitleWrapper, classNames?.groupTitle)
53
- }, optionsGroup.groupTitle), optionsGroup.options?.map(option => /*#__PURE__*/React.createElement(React.Fragment, {
79
+ }, optionsGroup.groupTitle), optionsGroup.options?.map((option, idx) => /*#__PURE__*/React.createElement(React.Fragment, {
54
80
  key: option.key
55
81
  }, /*#__PURE__*/React.createElement(_MenuOptionButton.MenuOptionButton, _extends({
56
82
  option: option,
57
83
  classNames: classNames
58
- }, restProps)))))));
84
+ }, restProps, {
85
+ isLastItem: index === optionsFiltered.length - 1 && idx === (optionsGroup.options && optionsGroup.options.length - 1)
86
+ })))))));
59
87
  }
60
88
  return /*#__PURE__*/React.createElement(React.Fragment, null);
61
89
  };
@@ -64,8 +92,10 @@ const Menu = /*#__PURE__*/React.forwardRef((props, ref) => {
64
92
  classNames,
65
93
  size = 'medium',
66
94
  width,
67
- isFluid = true
95
+ isFluid = true,
96
+ allowSearch
68
97
  } = props;
98
+ const [searchText, setSearchText] = React.useState('');
69
99
  return /*#__PURE__*/React.createElement("div", {
70
100
  className: (0, _classify.classify)(_MenuModule.default.menuCard, {
71
101
  [_MenuModule.default.fluid]: isFluid,
@@ -76,6 +106,12 @@ const Menu = /*#__PURE__*/React.forwardRef((props, ref) => {
76
106
  width
77
107
  },
78
108
  ref: ref
79
- }, /*#__PURE__*/React.createElement(RenderOption, props));
109
+ }, allowSearch && /*#__PURE__*/React.createElement(_SearchInput.SearchInput, {
110
+ value: searchText,
111
+ onChange: e => setSearchText(e.target.value),
112
+ onClear: () => setSearchText('')
113
+ }), /*#__PURE__*/React.createElement(RenderOption, _extends({}, props, {
114
+ searchText: searchText
115
+ })));
80
116
  });
81
117
  exports.Menu = Menu;
@@ -2,7 +2,17 @@
2
2
  import * as React from 'react';
3
3
 
4
4
  import {classify} from '../../utils/classify';
5
+ import {
6
+ getFilteredComposeOptionsFromSearchText,
7
+ getFilteredComposeOptionsResultText,
8
+ getFilteredGroupTitleOptionsFromSearchText,
9
+ getFilteredGroupTitleOptionsResultText,
10
+ getFilteredOptionsFromSearchText,
11
+ getFilteredOptionsResultText,
12
+ } from '../../utils/menu';
5
13
  import type {IconType} from '../Icon/Icon';
14
+ import {SearchInput} from '../SearchInput';
15
+ import {FormLabelSmall} from '../Text';
6
16
 
7
17
  import {MenuOptionButton} from './MenuOptionButton';
8
18
 
@@ -43,6 +53,10 @@ export type BaseMenuProps = {
43
53
  width?: string,
44
54
  menuDisabled?: boolean,
45
55
  isFluid?: boolean,
56
+ // onTabOut is a callback function that is called when
57
+ // the user navigates outside of the menu using the tab key.
58
+ onTabOut?: () => mixed,
59
+ allowSearch?: boolean,
46
60
  };
47
61
 
48
62
  export type MenuOptionTypes = {
@@ -65,22 +79,45 @@ export type MenuProps = {
65
79
  ...MenuOptionTypes,
66
80
  };
67
81
 
82
+ export type RenderOptionProps = {
83
+ ...MenuProps,
84
+ searchText?: string,
85
+ };
86
+
68
87
  const RenderOption = ({
69
88
  options,
70
89
  composeOptions,
71
90
  groupTitleOptions,
72
91
  classNames,
92
+ searchText = '',
73
93
  ...restProps
74
- }: MenuProps): React.Node => {
94
+ }: RenderOptionProps): React.Node => {
95
+ const {allowSearch} = restProps;
75
96
  if (options && Array.isArray(options) && options.length) {
97
+ const optionsFiltered = !allowSearch
98
+ ? options
99
+ : getFilteredOptionsFromSearchText(options, searchText);
100
+ const resultText = !allowSearch
101
+ ? ''
102
+ : getFilteredOptionsResultText(optionsFiltered);
103
+
76
104
  return (
77
105
  <>
78
- {options.map((option) => (
106
+ {allowSearch && (
107
+ <FormLabelSmall
108
+ className={css.filterOptionsResultText}
109
+ color="tertiary"
110
+ >
111
+ {resultText}
112
+ </FormLabelSmall>
113
+ )}
114
+ {optionsFiltered.map((option, idx) => (
79
115
  <React.Fragment key={option.key}>
80
116
  <MenuOptionButton
81
117
  option={option}
82
118
  classNames={classNames}
83
119
  {...restProps}
120
+ isLastItem={idx === optionsFiltered.length - 1}
84
121
  />
85
122
  </React.Fragment>
86
123
  ))}
@@ -92,17 +129,36 @@ const RenderOption = ({
92
129
  Array.isArray(composeOptions) &&
93
130
  composeOptions.length
94
131
  ) {
132
+ const optionsFiltered = !allowSearch
133
+ ? composeOptions
134
+ : getFilteredComposeOptionsFromSearchText(composeOptions, searchText);
135
+ const resultText = !allowSearch
136
+ ? ''
137
+ : getFilteredComposeOptionsResultText(optionsFiltered);
138
+
95
139
  return (
96
140
  <>
97
- {composeOptions.map((composeMenuOptions, index) => (
141
+ {allowSearch && (
142
+ <FormLabelSmall
143
+ className={css.filterOptionsResultText}
144
+ color="tertiary"
145
+ >
146
+ {resultText}
147
+ </FormLabelSmall>
148
+ )}
149
+ {optionsFiltered.map((composeMenuOptions, index) => (
98
150
  // eslint-disable-next-line react/no-array-index-key
99
- <span className={css.menuDivider} key={index}>
100
- {composeMenuOptions.map((option) => (
151
+ <span key={index} className={css.menuDivider}>
152
+ {composeMenuOptions.map((option, idx) => (
101
153
  <React.Fragment key={option.key}>
102
154
  <MenuOptionButton
103
155
  option={option}
104
156
  classNames={classNames}
105
157
  {...restProps}
158
+ isLastItem={
159
+ index === optionsFiltered.length - 1 &&
160
+ idx === composeMenuOptions.length - 1
161
+ }
106
162
  />
107
163
  </React.Fragment>
108
164
  ))}
@@ -116,9 +172,27 @@ const RenderOption = ({
116
172
  Array.isArray(groupTitleOptions) &&
117
173
  groupTitleOptions.length
118
174
  ) {
175
+ const optionsFiltered = !allowSearch
176
+ ? groupTitleOptions
177
+ : getFilteredGroupTitleOptionsFromSearchText(
178
+ groupTitleOptions,
179
+ searchText,
180
+ );
181
+ const resultText = !allowSearch
182
+ ? ''
183
+ : getFilteredGroupTitleOptionsResultText(optionsFiltered);
184
+
119
185
  return (
120
186
  <>
121
- {groupTitleOptions.map((optionsGroup, index) => (
187
+ {allowSearch && (
188
+ <FormLabelSmall
189
+ className={css.filterOptionsResultText}
190
+ color="tertiary"
191
+ >
192
+ {resultText}
193
+ </FormLabelSmall>
194
+ )}
195
+ {optionsFiltered.map((optionsGroup, index) => (
122
196
  // eslint-disable-next-line react/no-array-index-key
123
197
  <React.Fragment key={index}>
124
198
  {!!optionsGroup.groupTitle && (
@@ -132,12 +206,17 @@ const RenderOption = ({
132
206
  </div>
133
207
  )}
134
208
 
135
- {optionsGroup.options?.map((option) => (
209
+ {optionsGroup.options?.map((option, idx) => (
136
210
  <React.Fragment key={option.key}>
137
211
  <MenuOptionButton
138
212
  option={option}
139
213
  classNames={classNames}
140
214
  {...restProps}
215
+ isLastItem={
216
+ index === optionsFiltered.length - 1 &&
217
+ idx ===
218
+ (optionsGroup.options && optionsGroup.options.length - 1)
219
+ }
141
220
  />
142
221
  </React.Fragment>
143
222
  ))}
@@ -152,7 +231,14 @@ const RenderOption = ({
152
231
  export const Menu: React$AbstractComponent<MenuProps, HTMLDivElement> =
153
232
  React.forwardRef<MenuProps, HTMLDivElement>(
154
233
  (props: MenuProps, ref): React.Node => {
155
- const {classNames, size = 'medium', width, isFluid = true} = props;
234
+ const {
235
+ classNames,
236
+ size = 'medium',
237
+ width,
238
+ isFluid = true,
239
+ allowSearch,
240
+ } = props;
241
+ const [searchText, setSearchText] = React.useState('');
156
242
 
157
243
  return (
158
244
  <div
@@ -168,7 +254,14 @@ export const Menu: React$AbstractComponent<MenuProps, HTMLDivElement> =
168
254
  style={{width}}
169
255
  ref={ref}
170
256
  >
171
- <RenderOption {...props} />
257
+ {allowSearch && (
258
+ <SearchInput
259
+ value={searchText}
260
+ onChange={(e) => setSearchText(e.target.value)}
261
+ onClear={() => setSearchText('')}
262
+ />
263
+ )}
264
+ <RenderOption {...props} searchText={searchText} />
172
265
  </div>
173
266
  );
174
267
  },
@@ -191,6 +191,10 @@
191
191
  padding-bottom: spaceXSmall;
192
192
  }
193
193
 
194
+ .hideDivider {
195
+ border-top: none;
196
+ }
197
+
194
198
  .menuDivider:first-child {
195
199
  padding-top: spaceNone;
196
200
  }
@@ -213,3 +217,9 @@
213
217
  color: colorTextTertiary;
214
218
  padding-top: spaceSmall;
215
219
  }
220
+
221
+ .filterOptionsResultText {
222
+ margin-top: calc(spaceXSmall * 2);
223
+ margin-bottom: spaceXSmall;
224
+ margin-left: spaceSmall;
225
+ }
@@ -15,8 +15,9 @@ var _MenuModule = _interopRequireDefault(require("./Menu.module.css"));
15
15
  function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
16
16
  function _getRequireWildcardCache(nodeInterop) { if (typeof WeakMap !== "function") return null; var cacheBabelInterop = new WeakMap(); var cacheNodeInterop = new WeakMap(); return (_getRequireWildcardCache = function (nodeInterop) { return nodeInterop ? cacheNodeInterop : cacheBabelInterop; })(nodeInterop); }
17
17
  function _interopRequireWildcard(obj, nodeInterop) { if (!nodeInterop && obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(nodeInterop); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (key !== "default" && Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
18
-
18
+ function _extends() { _extends = Object.assign ? Object.assign.bind() : function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
19
19
  const MenuOptionButton = props => {
20
+ const lastMenuItemRef = React.useRef(null);
20
21
  const {
21
22
  option,
22
23
  size,
@@ -25,7 +26,9 @@ const MenuOptionButton = props => {
25
26
  menuDisabled,
26
27
  classNames,
27
28
  optionsVariant = 'normal',
28
- selectedKeys
29
+ selectedKeys,
30
+ isLastItem,
31
+ onTabOut
29
32
  } = props;
30
33
  const {
31
34
  key,
@@ -50,7 +53,19 @@ const MenuOptionButton = props => {
50
53
  React.useEffect(() => {
51
54
  setButtonSize(optionSize || size);
52
55
  }, [optionSize, size]);
53
- return /*#__PURE__*/React.createElement(_Button.UnstyledButton, {
56
+ React.useEffect(() => {
57
+ const handleKeyDown = event => {
58
+ if (event.key === 'Tab' && !event.shiftKey) {
59
+ // Tab pressed without shift key, calling tab out callback
60
+ onTabOut?.();
61
+ }
62
+ };
63
+ lastMenuItemRef.current?.addEventListener('keydown', handleKeyDown);
64
+ return () => {
65
+ lastMenuItemRef.current?.removeEventListener('keydown', handleKeyDown);
66
+ };
67
+ }, [isLastItem]);
68
+ return /*#__PURE__*/React.createElement(_Button.UnstyledButton, _extends({
54
69
  className: (0, _classify.classify)(_MenuModule.default.option, {
55
70
  [_MenuModule.default.selected]: isSelected() || key === selectedOption?.key,
56
71
  [_MenuModule.default.optionSmall]: buttonSize === 'small',
@@ -62,7 +77,9 @@ const MenuOptionButton = props => {
62
77
  disabled: menuDisabled || disabled,
63
78
  onClick: () => onSelect && onSelect(option),
64
79
  autoFocus: selectedOption?.key === key
65
- }, optionVariant === 'checkbox' && /*#__PURE__*/React.createElement(_Checkbox.Checkbox, {
80
+ }, isLastItem ? {
81
+ ref: lastMenuItemRef
82
+ } : {}), optionVariant === 'checkbox' && /*#__PURE__*/React.createElement(_Checkbox.Checkbox, {
66
83
  tabIndex: -1,
67
84
  disabled: menuDisabled || disabled,
68
85
  checked: isSelected()