@kaizen/components 1.79.10 → 1.80.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/src/Filter/FilterMultiSelect/FilterMultiSelect.cjs +11 -4
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.cjs +1 -1
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css.cjs +9 -0
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.cjs +1 -1
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/{MenuPopup.module.scss.cjs → MenuPopup.module.css.cjs} +1 -1
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.cjs +91 -0
- package/dist/esm/src/Filter/FilterMultiSelect/FilterMultiSelect.mjs +12 -5
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.mjs +1 -1
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css.mjs +7 -0
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.mjs +1 -1
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css.mjs +4 -0
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.mjs +85 -0
- package/dist/styles.css +47 -39
- package/dist/types/Filter/FilterMultiSelect/FilterMultiSelect.d.ts +7 -1
- package/dist/types/Filter/FilterMultiSelect/_docs/MockData.d.ts +1 -0
- package/dist/types/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.d.ts +22 -0
- package/dist/types/Filter/FilterMultiSelect/subcomponents/MenuPopup/index.d.ts +1 -0
- package/package.json +1 -1
- package/src/Filter/FilterBar/subcomponents/FilterBarMultiSelect/FilterBarMultiSelect.spec.tsx +1 -0
- package/src/Filter/FilterMultiSelect/FilterMultiSelect.tsx +10 -4
- package/src/Filter/FilterMultiSelect/_docs/FilterMultiSelect.mdx +9 -1
- package/src/Filter/FilterMultiSelect/_docs/FilterMultiSelect.stories.tsx +79 -2
- package/src/Filter/FilterMultiSelect/_docs/MockData.ts +39 -0
- package/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.spec.tsx +2 -18
- package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css +22 -0
- package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.tsx +1 -1
- package/src/Filter/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.module.scss +1 -0
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css +22 -0
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.tsx +1 -1
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.tsx +115 -0
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/index.ts +1 -0
- package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss.cjs +0 -9
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss.mjs +0 -7
- package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.scss.mjs +0 -4
- package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss +0 -25
- package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.scss +0 -24
|
@@ -10,6 +10,7 @@ var LoadMoreButton = require('./subcomponents/LoadMoreButton/LoadMoreButton.cjs'
|
|
|
10
10
|
var MenuFooter = require('./subcomponents/MenuLayout/MenuFooter/MenuFooter.cjs');
|
|
11
11
|
var MenuLoadingSkeleton = require('./subcomponents/MenuLayout/MenuLoadingSkeleton/MenuLoadingSkeleton.cjs');
|
|
12
12
|
var MenuPopup = require('./subcomponents/MenuPopup/MenuPopup.cjs');
|
|
13
|
+
var ResponsiveMenuPopup = require('./subcomponents/MenuPopup/ResponsiveMenuPopup.cjs');
|
|
13
14
|
var MultiSelectOption = require('./subcomponents/MultiSelectOption/MultiSelectOption.cjs');
|
|
14
15
|
var NoResults = require('./subcomponents/NoResults/NoResults.cjs');
|
|
15
16
|
var SearchInput = require('./subcomponents/SearchInput/SearchInput.cjs');
|
|
@@ -27,6 +28,7 @@ var React__default = /*#__PURE__*/_interopDefault(React);
|
|
|
27
28
|
var FilterMultiSelect = function (_a) {
|
|
28
29
|
var trigger = _a.trigger,
|
|
29
30
|
children = _a.children,
|
|
31
|
+
customMenuPopup = _a.customMenuPopup,
|
|
30
32
|
isOpen = _a.isOpen,
|
|
31
33
|
defaultOpen = _a.defaultOpen,
|
|
32
34
|
onOpenChange = _a.onOpenChange,
|
|
@@ -41,17 +43,19 @@ var FilterMultiSelect = function (_a) {
|
|
|
41
43
|
selectionMode = _b === void 0 ? 'multiple' : _b,
|
|
42
44
|
onSearchInputChange = _a.onSearchInputChange,
|
|
43
45
|
triggerRef = _a.triggerRef,
|
|
44
|
-
className = _a.className
|
|
46
|
+
className = _a.className,
|
|
47
|
+
restProps = tslib.__rest(_a, ["trigger", "children", "customMenuPopup", "isOpen", "defaultOpen", "onOpenChange", "isLoading", "loadingSkeleton", "label", "items", "selectedKeys", "defaultSelectedKeys", "onSelectionChange", "selectionMode", "onSearchInputChange", "triggerRef", "className"]);
|
|
48
|
+
var MenuComponent = customMenuPopup !== null && customMenuPopup !== void 0 ? customMenuPopup : MenuPopup.MenuPopup;
|
|
45
49
|
var menuTriggerProps = {
|
|
46
50
|
isOpen: isOpen,
|
|
47
51
|
defaultOpen: defaultOpen,
|
|
48
52
|
onOpenChange: onOpenChange,
|
|
49
53
|
triggerRef: triggerRef
|
|
50
54
|
};
|
|
51
|
-
var menuPopupProps = {
|
|
55
|
+
var menuPopupProps = tslib.__assign({
|
|
52
56
|
isLoading: isLoading,
|
|
53
57
|
loadingSkeleton: loadingSkeleton
|
|
54
|
-
};
|
|
58
|
+
}, restProps);
|
|
55
59
|
var disabledKeys = new Set(items === null || items === void 0 ? void 0 : items.filter(function (item) {
|
|
56
60
|
return item.isDisabled === true;
|
|
57
61
|
}).map(function (disabledItem) {
|
|
@@ -69,7 +73,9 @@ var FilterMultiSelect = function (_a) {
|
|
|
69
73
|
};
|
|
70
74
|
return React__default.default.createElement(MenuTriggerProvider.MenuTriggerProvider, tslib.__assign({}, menuTriggerProps), React__default.default.createElement("div", {
|
|
71
75
|
className: className
|
|
72
|
-
}, React__default.default.createElement(MenuTriggerProvider.MenuTriggerConsumer, null, trigger), React__default.default.createElement(
|
|
76
|
+
}, React__default.default.createElement(MenuTriggerProvider.MenuTriggerConsumer, null, trigger), React__default.default.createElement(MenuComponent, tslib.__assign({
|
|
77
|
+
"aria-label": label
|
|
78
|
+
}, menuPopupProps), React__default.default.createElement(SelectionProvider.SelectionProvider, tslib.__assign({}, selectionProps), React__default.default.createElement(SelectionProvider.SelectionConsumer, null, children)))));
|
|
73
79
|
};
|
|
74
80
|
FilterMultiSelect.displayName = 'FilterMultiSelect';
|
|
75
81
|
FilterMultiSelect.TriggerButton = FilterTriggerButton.FilterTriggerButton;
|
|
@@ -85,4 +91,5 @@ FilterMultiSelect.MenuFooter = MenuFooter.MenuFooter; // For layout
|
|
|
85
91
|
FilterMultiSelect.MenuLoadingSkeleton = MenuLoadingSkeleton.MenuLoadingSkeleton; // Menu Loading Skeleton example
|
|
86
92
|
FilterMultiSelect.LoadMoreButton = LoadMoreButton.LoadMoreButton;
|
|
87
93
|
FilterMultiSelect.NoResults = NoResults.NoResults;
|
|
94
|
+
FilterMultiSelect.ResponsiveMenuPopup = ResponsiveMenuPopup.ResponsiveMenuPopup;
|
|
88
95
|
exports.FilterMultiSelect = FilterMultiSelect;
|
|
@@ -4,7 +4,7 @@ var tslib = require('tslib');
|
|
|
4
4
|
var React = require('react');
|
|
5
5
|
var classnames = require('classnames');
|
|
6
6
|
var SelectionProvider = require('../../context/SelectionProvider/SelectionProvider.cjs');
|
|
7
|
-
var ListBox_module = require('./ListBox.module.
|
|
7
|
+
var ListBox_module = require('./ListBox.module.css.cjs');
|
|
8
8
|
function _interopDefault(e) {
|
|
9
9
|
return e && e.__esModule ? e : {
|
|
10
10
|
default: e
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var styles = {
|
|
4
|
+
"listBox": "ListBox-module_listBox__HBScm",
|
|
5
|
+
"overflown": "ListBox-module_overflown__PdKED",
|
|
6
|
+
"hidden": "ListBox-module_hidden__mO-oL",
|
|
7
|
+
"noResultsWrapper": "ListBox-module_noResultsWrapper__RnMj0"
|
|
8
|
+
};
|
|
9
|
+
module.exports = styles;
|
|
@@ -6,7 +6,7 @@ var focus = require('@react-aria/focus');
|
|
|
6
6
|
var overlays = require('@react-aria/overlays');
|
|
7
7
|
var MenuTriggerProvider = require('../../context/MenuTriggerProvider/MenuTriggerProvider.cjs');
|
|
8
8
|
require('../../context/SelectionProvider/SelectionProvider.cjs');
|
|
9
|
-
var MenuPopup_module = require('./MenuPopup.module.
|
|
9
|
+
var MenuPopup_module = require('./MenuPopup.module.css.cjs');
|
|
10
10
|
function _interopDefault(e) {
|
|
11
11
|
return e && e.__esModule ? e : {
|
|
12
12
|
default: e
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var tslib = require('tslib');
|
|
4
|
+
var React = require('react');
|
|
5
|
+
var reactDom = require('@floating-ui/react-dom');
|
|
6
|
+
var classnames = require('classnames');
|
|
7
|
+
var reactFocusOn = require('react-focus-on');
|
|
8
|
+
var MenuTriggerProvider = require('../../context/MenuTriggerProvider/MenuTriggerProvider.cjs');
|
|
9
|
+
require('../../context/SelectionProvider/SelectionProvider.cjs');
|
|
10
|
+
var MenuPopup_module = require('./MenuPopup.module.css.cjs');
|
|
11
|
+
function _interopDefault(e) {
|
|
12
|
+
return e && e.__esModule ? e : {
|
|
13
|
+
default: e
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
var React__default = /*#__PURE__*/_interopDefault(React);
|
|
17
|
+
var classnames__default = /*#__PURE__*/_interopDefault(classnames);
|
|
18
|
+
|
|
19
|
+
/** This is a popup primitive that can be used with the FilterMultiSelect when there are overflow issues with the original implementation. This uses the floating-ui */
|
|
20
|
+
var ResponsiveMenuPopup = function (_a) {
|
|
21
|
+
var children = _a.children,
|
|
22
|
+
_b = _a.floatingConfig,
|
|
23
|
+
floatingConfig = _b === void 0 ? {
|
|
24
|
+
placement: 'bottom-start',
|
|
25
|
+
strategy: 'absolute',
|
|
26
|
+
whileElementsMounted: reactDom.autoUpdate,
|
|
27
|
+
shouldFlip: true,
|
|
28
|
+
shouldResize: true
|
|
29
|
+
} : _b,
|
|
30
|
+
classNameOverride = _a.classNameOverride,
|
|
31
|
+
isLoading = _a.isLoading,
|
|
32
|
+
loadingSkeleton = _a.loadingSkeleton,
|
|
33
|
+
restProps = tslib.__rest(_a, ["children", "floatingConfig", "classNameOverride", "isLoading", "loadingSkeleton"]);
|
|
34
|
+
var _c = React.useState(null),
|
|
35
|
+
floatingElement = _c[0],
|
|
36
|
+
setFloatingElement = _c[1];
|
|
37
|
+
var _d = MenuTriggerProvider.useMenuTriggerContext(),
|
|
38
|
+
menuTriggerState = _d.menuTriggerState,
|
|
39
|
+
buttonRef = _d.buttonRef;
|
|
40
|
+
var referenceElement = buttonRef.current;
|
|
41
|
+
var _e = reactDom.useFloating(tslib.__assign({
|
|
42
|
+
elements: {
|
|
43
|
+
reference: referenceElement,
|
|
44
|
+
floating: floatingElement
|
|
45
|
+
},
|
|
46
|
+
middleware: [reactDom.offset(6), floatingConfig.shouldFlip && reactDom.autoPlacement({
|
|
47
|
+
allowedPlacements: ['bottom-start', 'top-start']
|
|
48
|
+
}), floatingConfig.shouldResize && reactDom.size({
|
|
49
|
+
apply: function (_a) {
|
|
50
|
+
var availableHeight = _a.availableHeight,
|
|
51
|
+
elements = _a.elements;
|
|
52
|
+
Object.assign(elements.floating.style, {
|
|
53
|
+
maxHeight: Math.max(250, Math.min(availableHeight - 12, 500)) + 'px'
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
})]
|
|
57
|
+
}, floatingConfig)),
|
|
58
|
+
floatingStyles = _e.floatingStyles,
|
|
59
|
+
update = _e.update;
|
|
60
|
+
var handleReturnFocus = function () {
|
|
61
|
+
requestAnimationFrame(function () {
|
|
62
|
+
var _a;
|
|
63
|
+
(_a = buttonRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
React.useEffect(function () {
|
|
67
|
+
var _a;
|
|
68
|
+
if (floatingElement && referenceElement) {
|
|
69
|
+
(_a = floatingElement.showPopover) === null || _a === void 0 ? void 0 : _a.call(floatingElement);
|
|
70
|
+
update();
|
|
71
|
+
}
|
|
72
|
+
}, [floatingElement, referenceElement, update]);
|
|
73
|
+
return menuTriggerState.isOpen ? React__default.default.createElement(reactFocusOn.FocusOn, {
|
|
74
|
+
enabled: menuTriggerState.isOpen,
|
|
75
|
+
scrollLock: true,
|
|
76
|
+
returnFocus: false,
|
|
77
|
+
onClickOutside: menuTriggerState.close,
|
|
78
|
+
onEscapeKey: menuTriggerState.close,
|
|
79
|
+
onDeactivation: handleReturnFocus
|
|
80
|
+
}, React__default.default.createElement("div", tslib.__assign({
|
|
81
|
+
ref: setFloatingElement,
|
|
82
|
+
style: floatingStyles,
|
|
83
|
+
className: classnames__default.default(MenuPopup_module.menuPopup, classNameOverride),
|
|
84
|
+
role: "dialog",
|
|
85
|
+
"aria-modal": "true",
|
|
86
|
+
// @ts-expect-error: popover is valid in supported browsers
|
|
87
|
+
popover: "manual"
|
|
88
|
+
}, restProps), isLoading && loadingSkeleton ? loadingSkeleton : children)) : React__default.default.createElement(React__default.default.Fragment, null);
|
|
89
|
+
};
|
|
90
|
+
ResponsiveMenuPopup.displayName = 'FilterMultiSelect.ResponsiveMenuPopup';
|
|
91
|
+
exports.ResponsiveMenuPopup = ResponsiveMenuPopup;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __assign } from 'tslib';
|
|
1
|
+
import { __rest, __assign } from 'tslib';
|
|
2
2
|
import React from 'react';
|
|
3
3
|
import { MenuTriggerProvider, MenuTriggerConsumer } from './context/MenuTriggerProvider/MenuTriggerProvider.mjs';
|
|
4
4
|
import { SelectionProvider, SelectionConsumer } from './context/SelectionProvider/SelectionProvider.mjs';
|
|
@@ -8,6 +8,7 @@ import { LoadMoreButton } from './subcomponents/LoadMoreButton/LoadMoreButton.mj
|
|
|
8
8
|
import { MenuFooter } from './subcomponents/MenuLayout/MenuFooter/MenuFooter.mjs';
|
|
9
9
|
import { MenuLoadingSkeleton } from './subcomponents/MenuLayout/MenuLoadingSkeleton/MenuLoadingSkeleton.mjs';
|
|
10
10
|
import { MenuPopup } from './subcomponents/MenuPopup/MenuPopup.mjs';
|
|
11
|
+
import { ResponsiveMenuPopup } from './subcomponents/MenuPopup/ResponsiveMenuPopup.mjs';
|
|
11
12
|
import { MultiSelectOption } from './subcomponents/MultiSelectOption/MultiSelectOption.mjs';
|
|
12
13
|
import { NoResults } from './subcomponents/NoResults/NoResults.mjs';
|
|
13
14
|
import { SearchInput } from './subcomponents/SearchInput/SearchInput.mjs';
|
|
@@ -20,6 +21,7 @@ const FilterMultiSelect = /*#__PURE__*/function () {
|
|
|
20
21
|
const FilterMultiSelect = function (_a) {
|
|
21
22
|
var trigger = _a.trigger,
|
|
22
23
|
children = _a.children,
|
|
24
|
+
customMenuPopup = _a.customMenuPopup,
|
|
23
25
|
isOpen = _a.isOpen,
|
|
24
26
|
defaultOpen = _a.defaultOpen,
|
|
25
27
|
onOpenChange = _a.onOpenChange,
|
|
@@ -34,17 +36,19 @@ const FilterMultiSelect = /*#__PURE__*/function () {
|
|
|
34
36
|
selectionMode = _b === void 0 ? 'multiple' : _b,
|
|
35
37
|
onSearchInputChange = _a.onSearchInputChange,
|
|
36
38
|
triggerRef = _a.triggerRef,
|
|
37
|
-
className = _a.className
|
|
39
|
+
className = _a.className,
|
|
40
|
+
restProps = __rest(_a, ["trigger", "children", "customMenuPopup", "isOpen", "defaultOpen", "onOpenChange", "isLoading", "loadingSkeleton", "label", "items", "selectedKeys", "defaultSelectedKeys", "onSelectionChange", "selectionMode", "onSearchInputChange", "triggerRef", "className"]);
|
|
41
|
+
var MenuComponent = customMenuPopup !== null && customMenuPopup !== void 0 ? customMenuPopup : MenuPopup;
|
|
38
42
|
var menuTriggerProps = {
|
|
39
43
|
isOpen: isOpen,
|
|
40
44
|
defaultOpen: defaultOpen,
|
|
41
45
|
onOpenChange: onOpenChange,
|
|
42
46
|
triggerRef: triggerRef
|
|
43
47
|
};
|
|
44
|
-
var menuPopupProps = {
|
|
48
|
+
var menuPopupProps = __assign({
|
|
45
49
|
isLoading: isLoading,
|
|
46
50
|
loadingSkeleton: loadingSkeleton
|
|
47
|
-
};
|
|
51
|
+
}, restProps);
|
|
48
52
|
var disabledKeys = new Set(items === null || items === void 0 ? void 0 : items.filter(function (item) {
|
|
49
53
|
return item.isDisabled === true;
|
|
50
54
|
}).map(function (disabledItem) {
|
|
@@ -62,7 +66,9 @@ const FilterMultiSelect = /*#__PURE__*/function () {
|
|
|
62
66
|
};
|
|
63
67
|
return /*#__PURE__*/React.createElement(MenuTriggerProvider, __assign({}, menuTriggerProps), /*#__PURE__*/React.createElement("div", {
|
|
64
68
|
className: className
|
|
65
|
-
}, /*#__PURE__*/React.createElement(MenuTriggerConsumer, null, trigger), /*#__PURE__*/React.createElement(
|
|
69
|
+
}, /*#__PURE__*/React.createElement(MenuTriggerConsumer, null, trigger), /*#__PURE__*/React.createElement(MenuComponent, __assign({
|
|
70
|
+
"aria-label": label
|
|
71
|
+
}, menuPopupProps), /*#__PURE__*/React.createElement(SelectionProvider, __assign({}, selectionProps), /*#__PURE__*/React.createElement(SelectionConsumer, null, children)))));
|
|
66
72
|
};
|
|
67
73
|
FilterMultiSelect.displayName = 'FilterMultiSelect';
|
|
68
74
|
FilterMultiSelect.TriggerButton = FilterTriggerButton;
|
|
@@ -79,6 +85,7 @@ const FilterMultiSelect = /*#__PURE__*/function () {
|
|
|
79
85
|
FilterMultiSelect.MenuLoadingSkeleton = MenuLoadingSkeleton;
|
|
80
86
|
FilterMultiSelect.LoadMoreButton = LoadMoreButton;
|
|
81
87
|
FilterMultiSelect.NoResults = NoResults;
|
|
88
|
+
FilterMultiSelect.ResponsiveMenuPopup = ResponsiveMenuPopup;
|
|
82
89
|
return FilterMultiSelect;
|
|
83
90
|
}();
|
|
84
91
|
export { FilterMultiSelect };
|
|
@@ -2,7 +2,7 @@ import { __assign, __spreadArray } from 'tslib';
|
|
|
2
2
|
import React, { useState, useEffect } from 'react';
|
|
3
3
|
import classnames from 'classnames';
|
|
4
4
|
import { useSelectionContext } from '../../context/SelectionProvider/SelectionProvider.mjs';
|
|
5
|
-
import styles from './ListBox.module.
|
|
5
|
+
import styles from './ListBox.module.css.mjs';
|
|
6
6
|
var getItemsFromKeys = function (items, keys) {
|
|
7
7
|
var itemKeys = Array.from(keys);
|
|
8
8
|
return itemKeys.reduce(function (acc, itemKey) {
|
|
@@ -4,7 +4,7 @@ import { FocusScope } from '@react-aria/focus';
|
|
|
4
4
|
import { useOverlay, DismissButton } from '@react-aria/overlays';
|
|
5
5
|
import { useMenuTriggerContext } from '../../context/MenuTriggerProvider/MenuTriggerProvider.mjs';
|
|
6
6
|
import '../../context/SelectionProvider/SelectionProvider.mjs';
|
|
7
|
-
import styles from './MenuPopup.module.
|
|
7
|
+
import styles from './MenuPopup.module.css.mjs';
|
|
8
8
|
const MenuPopup = /*#__PURE__*/function () {
|
|
9
9
|
const MenuPopup = function (_a) {
|
|
10
10
|
var isLoading = _a.isLoading,
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { __rest, __assign } from 'tslib';
|
|
2
|
+
import React, { useState, useEffect } from 'react';
|
|
3
|
+
import { useFloating, offset, autoPlacement, size, autoUpdate } from '@floating-ui/react-dom';
|
|
4
|
+
import classnames from 'classnames';
|
|
5
|
+
import { FocusOn } from 'react-focus-on';
|
|
6
|
+
import { useMenuTriggerContext } from '../../context/MenuTriggerProvider/MenuTriggerProvider.mjs';
|
|
7
|
+
import '../../context/SelectionProvider/SelectionProvider.mjs';
|
|
8
|
+
import styles from './MenuPopup.module.css.mjs';
|
|
9
|
+
|
|
10
|
+
/** This is a popup primitive that can be used with the FilterMultiSelect when there are overflow issues with the original implementation. This uses the floating-ui */
|
|
11
|
+
const ResponsiveMenuPopup = /*#__PURE__*/function () {
|
|
12
|
+
const ResponsiveMenuPopup = function (_a) {
|
|
13
|
+
var children = _a.children,
|
|
14
|
+
_b = _a.floatingConfig,
|
|
15
|
+
floatingConfig = _b === void 0 ? {
|
|
16
|
+
placement: 'bottom-start',
|
|
17
|
+
strategy: 'absolute',
|
|
18
|
+
whileElementsMounted: autoUpdate,
|
|
19
|
+
shouldFlip: true,
|
|
20
|
+
shouldResize: true
|
|
21
|
+
} : _b,
|
|
22
|
+
classNameOverride = _a.classNameOverride,
|
|
23
|
+
isLoading = _a.isLoading,
|
|
24
|
+
loadingSkeleton = _a.loadingSkeleton,
|
|
25
|
+
restProps = __rest(_a, ["children", "floatingConfig", "classNameOverride", "isLoading", "loadingSkeleton"]);
|
|
26
|
+
var _c = useState(null),
|
|
27
|
+
floatingElement = _c[0],
|
|
28
|
+
setFloatingElement = _c[1];
|
|
29
|
+
var _d = useMenuTriggerContext(),
|
|
30
|
+
menuTriggerState = _d.menuTriggerState,
|
|
31
|
+
buttonRef = _d.buttonRef;
|
|
32
|
+
var referenceElement = buttonRef.current;
|
|
33
|
+
var _e = useFloating(__assign({
|
|
34
|
+
elements: {
|
|
35
|
+
reference: referenceElement,
|
|
36
|
+
floating: floatingElement
|
|
37
|
+
},
|
|
38
|
+
middleware: [offset(6), floatingConfig.shouldFlip && autoPlacement({
|
|
39
|
+
allowedPlacements: ['bottom-start', 'top-start']
|
|
40
|
+
}), floatingConfig.shouldResize && size({
|
|
41
|
+
apply: function (_a) {
|
|
42
|
+
var availableHeight = _a.availableHeight,
|
|
43
|
+
elements = _a.elements;
|
|
44
|
+
Object.assign(elements.floating.style, {
|
|
45
|
+
maxHeight: Math.max(250, Math.min(availableHeight - 12, 500)) + 'px'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
})]
|
|
49
|
+
}, floatingConfig)),
|
|
50
|
+
floatingStyles = _e.floatingStyles,
|
|
51
|
+
update = _e.update;
|
|
52
|
+
var handleReturnFocus = function () {
|
|
53
|
+
requestAnimationFrame(function () {
|
|
54
|
+
var _a;
|
|
55
|
+
(_a = buttonRef.current) === null || _a === void 0 ? void 0 : _a.focus();
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
useEffect(function () {
|
|
59
|
+
var _a;
|
|
60
|
+
if (floatingElement && referenceElement) {
|
|
61
|
+
(_a = floatingElement.showPopover) === null || _a === void 0 ? void 0 : _a.call(floatingElement);
|
|
62
|
+
update();
|
|
63
|
+
}
|
|
64
|
+
}, [floatingElement, referenceElement, update]);
|
|
65
|
+
return menuTriggerState.isOpen ? (/*#__PURE__*/React.createElement(FocusOn, {
|
|
66
|
+
enabled: menuTriggerState.isOpen,
|
|
67
|
+
scrollLock: true,
|
|
68
|
+
returnFocus: false,
|
|
69
|
+
onClickOutside: menuTriggerState.close,
|
|
70
|
+
onEscapeKey: menuTriggerState.close,
|
|
71
|
+
onDeactivation: handleReturnFocus
|
|
72
|
+
}, /*#__PURE__*/React.createElement("div", __assign({
|
|
73
|
+
ref: setFloatingElement,
|
|
74
|
+
style: floatingStyles,
|
|
75
|
+
className: classnames(styles.menuPopup, classNameOverride),
|
|
76
|
+
role: "dialog",
|
|
77
|
+
"aria-modal": "true",
|
|
78
|
+
// @ts-expect-error: popover is valid in supported browsers
|
|
79
|
+
popover: "manual"
|
|
80
|
+
}, restProps), isLoading && loadingSkeleton ? loadingSkeleton : children))) : (/*#__PURE__*/React.createElement(React.Fragment, null));
|
|
81
|
+
};
|
|
82
|
+
ResponsiveMenuPopup.displayName = 'FilterMultiSelect.ResponsiveMenuPopup';
|
|
83
|
+
return ResponsiveMenuPopup;
|
|
84
|
+
}();
|
|
85
|
+
export { ResponsiveMenuPopup };
|
package/dist/styles.css
CHANGED
|
@@ -9,6 +9,14 @@
|
|
|
9
9
|
}
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
+
@layer kz-components {
|
|
13
|
+
.List-module_list__bbFPn {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
gap: var(--spacing-16);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
12
20
|
@layer kz-components {
|
|
13
21
|
.ListItem-module_listItem__xGr6A {
|
|
14
22
|
font-family: var(--typography-paragraph-body-font-family);
|
|
@@ -19,24 +27,6 @@
|
|
|
19
27
|
}
|
|
20
28
|
}
|
|
21
29
|
|
|
22
|
-
@layer kz-components {
|
|
23
|
-
.ListSection-module_listSectionHeader__bptHg {
|
|
24
|
-
font-family: var(--typography-heading-5-font-family);
|
|
25
|
-
font-weight: var(--typography-heading-5-font-weight);
|
|
26
|
-
font-size: var(--typography-heading-5-font-size);
|
|
27
|
-
line-height: var(--typography-heading-5-line-height);
|
|
28
|
-
letter-spacing: var(--typography-heading-5-letter-spacing);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
@layer kz-components {
|
|
33
|
-
.List-module_list__bbFPn {
|
|
34
|
-
display: flex;
|
|
35
|
-
flex-direction: column;
|
|
36
|
-
gap: var(--spacing-16);
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
30
|
@layer kz-components {
|
|
41
31
|
.Trigger-module_button__giSqA {
|
|
42
32
|
display: flex;
|
|
@@ -56,6 +46,16 @@
|
|
|
56
46
|
}
|
|
57
47
|
}
|
|
58
48
|
|
|
49
|
+
@layer kz-components {
|
|
50
|
+
.ListSection-module_listSectionHeader__bptHg {
|
|
51
|
+
font-family: var(--typography-heading-5-font-family);
|
|
52
|
+
font-weight: var(--typography-heading-5-font-weight);
|
|
53
|
+
font-size: var(--typography-heading-5-font-size);
|
|
54
|
+
line-height: var(--typography-heading-5-line-height);
|
|
55
|
+
letter-spacing: var(--typography-heading-5-letter-spacing);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
59
|
@layer kz-components {
|
|
60
60
|
/*
|
|
61
61
|
* This is taken from the Material Symbols CDN
|
|
@@ -3800,26 +3800,29 @@
|
|
|
3800
3800
|
}
|
|
3801
3801
|
}
|
|
3802
3802
|
|
|
3803
|
-
/** THIS IS AN AUTOGENERATED FILE **/
|
|
3804
3803
|
@layer kz-components {
|
|
3805
|
-
.ListBox-
|
|
3804
|
+
.ListBox-module_listBox__HBScm {
|
|
3806
3805
|
list-style: none;
|
|
3807
|
-
padding: var(--spacing-
|
|
3808
|
-
margin: 0 var(--spacing-
|
|
3806
|
+
padding: var(--spacing-12);
|
|
3807
|
+
margin: 0 var(--spacing-12) 0 0;
|
|
3809
3808
|
display: grid;
|
|
3810
3809
|
max-height: 22rem;
|
|
3811
3810
|
overflow-y: auto;
|
|
3812
3811
|
}
|
|
3813
|
-
|
|
3814
|
-
|
|
3812
|
+
|
|
3813
|
+
.ListBox-module_overflown__PdKED {
|
|
3814
|
+
padding-right: var(--spacing-12);
|
|
3815
3815
|
}
|
|
3816
|
-
|
|
3816
|
+
|
|
3817
|
+
.ListBox-module_hidden__mO-oL {
|
|
3817
3818
|
display: none;
|
|
3818
3819
|
}
|
|
3819
|
-
|
|
3820
|
+
|
|
3821
|
+
.ListBox-module_noResultsWrapper__RnMj0 {
|
|
3820
3822
|
list-style: none;
|
|
3821
3823
|
}
|
|
3822
3824
|
}
|
|
3825
|
+
|
|
3823
3826
|
/** THIS IS AN AUTOGENERATED FILE **/
|
|
3824
3827
|
/** THIS IS AN AUTOGENERATED FILE **/
|
|
3825
3828
|
/** THIS IS AN AUTOGENERATED FILE **/
|
|
@@ -3830,6 +3833,7 @@
|
|
|
3830
3833
|
padding: 0;
|
|
3831
3834
|
}
|
|
3832
3835
|
.ListBoxSection-module_listBoxSectionHeader__N-2Fi {
|
|
3836
|
+
position: relative;
|
|
3833
3837
|
font-family: var(--typography-heading-6-font-family, "Inter", "Noto Sans", Helvetica, Arial, sans-serif);
|
|
3834
3838
|
font-size: var(--typography-heading-6-font-size, 0.875rem);
|
|
3835
3839
|
font-weight: var(--typography-heading-6-font-weight, 600);
|
|
@@ -3864,25 +3868,29 @@
|
|
|
3864
3868
|
margin-right: var(--spacing-sm, 0.75rem);
|
|
3865
3869
|
}
|
|
3866
3870
|
}
|
|
3867
|
-
/** THIS IS AN AUTOGENERATED FILE **/
|
|
3868
|
-
/** THIS IS AN AUTOGENERATED FILE **/
|
|
3869
|
-
/** THIS IS AN AUTOGENERATED FILE **/
|
|
3870
|
-
/** THIS IS AN AUTOGENERATED FILE **/
|
|
3871
3871
|
@layer kz-components {
|
|
3872
|
-
.MenuPopup-
|
|
3873
|
-
|
|
3872
|
+
.MenuPopup-module_menuPopup__QgGEa {
|
|
3873
|
+
/* from $ca-z-index-dropdown */
|
|
3874
3874
|
z-index: 1000;
|
|
3875
3875
|
box-sizing: border-box;
|
|
3876
|
-
background: var(--color-white
|
|
3877
|
-
color: var(--color-purple-800
|
|
3878
|
-
border-radius: var(--border-solid-border-radius
|
|
3879
|
-
box-shadow: var(--shadow-large-box-shadow
|
|
3880
|
-
padding: var(--spacing-
|
|
3881
|
-
margin-top: var(--spacing-
|
|
3876
|
+
background: var(--color-white);
|
|
3877
|
+
color: var(--color-purple-800);
|
|
3878
|
+
border-radius: var(--border-solid-border-radius);
|
|
3879
|
+
box-shadow: var(--shadow-large-box-shadow);
|
|
3880
|
+
padding: var(--spacing-12) 0;
|
|
3881
|
+
margin-top: var(--spacing-6);
|
|
3882
3882
|
text-align: start;
|
|
3883
|
-
width: 294px;
|
|
3883
|
+
width: var(--menu-container-width, 294px);
|
|
3884
|
+
max-height: var(--menu-container-height, 500px);
|
|
3885
|
+
}
|
|
3886
|
+
|
|
3887
|
+
.MenuPopup-module_menuPopup__QgGEa[popover]:popover-open {
|
|
3888
|
+
z-index: unset;
|
|
3889
|
+
margin: 0;
|
|
3890
|
+
inset: unset;
|
|
3884
3891
|
}
|
|
3885
3892
|
}
|
|
3893
|
+
|
|
3886
3894
|
/** THIS IS AN AUTOGENERATED FILE **/
|
|
3887
3895
|
/** THIS IS AN AUTOGENERATED FILE **/
|
|
3888
3896
|
/** THIS IS AN AUTOGENERATED FILE **/
|
|
@@ -15,11 +15,13 @@ type SelectionProps = {
|
|
|
15
15
|
export type FilterMultiSelectProps = {
|
|
16
16
|
trigger: (value?: MenuTriggerProviderContextType) => React.ReactNode;
|
|
17
17
|
children: (value?: SelectionProviderContextType) => React.ReactNode;
|
|
18
|
+
/** Replaces the MenuPopup. Should only be used for changing how the floating element is positioned, ie: with the `<ResponsiveMenuPopup />` primitive. */
|
|
19
|
+
customMenuPopup?: React.ComponentType<MenuPopupProps>;
|
|
18
20
|
triggerRef?: React.RefObject<HTMLButtonElement>;
|
|
19
21
|
className?: string;
|
|
20
22
|
} & Omit<MenuPopupProps, 'children'> & Omit<MenuTriggerProviderProps, 'children'> & SelectionProps;
|
|
21
23
|
export declare const FilterMultiSelect: {
|
|
22
|
-
({ trigger, children, isOpen, defaultOpen, onOpenChange, isLoading, loadingSkeleton, label, items, selectedKeys, defaultSelectedKeys, onSelectionChange, selectionMode, onSearchInputChange, triggerRef, className, }: FilterMultiSelectProps): JSX.Element;
|
|
24
|
+
({ trigger, children, customMenuPopup, isOpen, defaultOpen, onOpenChange, isLoading, loadingSkeleton, label, items, selectedKeys, defaultSelectedKeys, onSelectionChange, selectionMode, onSearchInputChange, triggerRef, className, ...restProps }: FilterMultiSelectProps): JSX.Element;
|
|
23
25
|
displayName: string;
|
|
24
26
|
TriggerButton: {
|
|
25
27
|
({ selectedOptionLabels, label, classNameOverride, labelCharacterLimitBeforeTruncate, }: import("./subcomponents/Trigger").FilterTriggerButtonProps): JSX.Element;
|
|
@@ -70,5 +72,9 @@ export declare const FilterMultiSelect: {
|
|
|
70
72
|
({ children, ...restProps }: import("./subcomponents/NoResults").NoResultsProps): JSX.Element;
|
|
71
73
|
displayName: string;
|
|
72
74
|
};
|
|
75
|
+
ResponsiveMenuPopup: {
|
|
76
|
+
({ children, floatingConfig, classNameOverride, isLoading, loadingSkeleton, ...restProps }: import("./subcomponents/MenuPopup").ResponsiveMenuPopupProps): JSX.Element;
|
|
77
|
+
displayName: string;
|
|
78
|
+
};
|
|
73
79
|
};
|
|
74
80
|
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type HTMLAttributes } from 'react';
|
|
2
|
+
import { type UseFloatingOptions } from '@floating-ui/react-dom';
|
|
3
|
+
import { type OverrideClassName } from "../../../../types/OverrideClassName";
|
|
4
|
+
import { type MenuPopupProps } from './MenuPopup';
|
|
5
|
+
export type FloatingConfig = Pick<UseFloatingOptions, 'placement' | 'strategy' | 'whileElementsMounted'> & {
|
|
6
|
+
/** Whether the component should automatically resize based on the available window height.
|
|
7
|
+
* @default true
|
|
8
|
+
*/
|
|
9
|
+
shouldResize?: boolean;
|
|
10
|
+
/** Whether the component should automatically flip to the top of the input based on the available window height.
|
|
11
|
+
* @default true
|
|
12
|
+
*/
|
|
13
|
+
shouldFlip?: boolean;
|
|
14
|
+
};
|
|
15
|
+
export type ResponsiveMenuPopupProps = MenuPopupProps & {
|
|
16
|
+
floatingConfig?: FloatingConfig;
|
|
17
|
+
} & OverrideClassName<HTMLAttributes<HTMLDivElement>>;
|
|
18
|
+
/** This is a popup primitive that can be used with the FilterMultiSelect when there are overflow issues with the original implementation. This uses the floating-ui */
|
|
19
|
+
export declare const ResponsiveMenuPopup: {
|
|
20
|
+
({ children, floatingConfig, classNameOverride, isLoading, loadingSkeleton, ...restProps }: ResponsiveMenuPopupProps): JSX.Element;
|
|
21
|
+
displayName: string;
|
|
22
|
+
};
|
package/package.json
CHANGED
package/src/Filter/FilterBar/subcomponents/FilterBarMultiSelect/FilterBarMultiSelect.spec.tsx
CHANGED
|
@@ -180,6 +180,7 @@ describe('<FilterBarMultiSelect />', () => {
|
|
|
180
180
|
})
|
|
181
181
|
|
|
182
182
|
await user.click(getByRole('option', { name: 'Fruit Jelly' }))
|
|
183
|
+
await user.keyboard('{Escape}')
|
|
183
184
|
await waitFor(() => {
|
|
184
185
|
expect(getByRole('button', { name: 'Toppings : Pearls, Fruit Jelly' })).toBeInTheDocument()
|
|
185
186
|
})
|
|
@@ -13,7 +13,7 @@ import { ListBox } from './subcomponents/ListBox'
|
|
|
13
13
|
import { ListBoxSection } from './subcomponents/ListBoxSection'
|
|
14
14
|
import { LoadMoreButton } from './subcomponents/LoadMoreButton'
|
|
15
15
|
import { MenuFooter, MenuLoadingSkeleton } from './subcomponents/MenuLayout'
|
|
16
|
-
import { MenuPopup, type MenuPopupProps } from './subcomponents/MenuPopup'
|
|
16
|
+
import { MenuPopup, ResponsiveMenuPopup, type MenuPopupProps } from './subcomponents/MenuPopup'
|
|
17
17
|
import { MultiSelectOption } from './subcomponents/MultiSelectOption'
|
|
18
18
|
import { NoResults } from './subcomponents/NoResults'
|
|
19
19
|
import { SearchInput } from './subcomponents/SearchInput'
|
|
@@ -35,6 +35,8 @@ type SelectionProps = {
|
|
|
35
35
|
export type FilterMultiSelectProps = {
|
|
36
36
|
trigger: (value?: MenuTriggerProviderContextType) => React.ReactNode
|
|
37
37
|
children: (value?: SelectionProviderContextType) => React.ReactNode // the content of the menu
|
|
38
|
+
/** Replaces the MenuPopup. Should only be used for changing how the floating element is positioned, ie: with the `<ResponsiveMenuPopup />` primitive. */
|
|
39
|
+
customMenuPopup?: React.ComponentType<MenuPopupProps>
|
|
38
40
|
triggerRef?: React.RefObject<HTMLButtonElement>
|
|
39
41
|
className?: string
|
|
40
42
|
} & Omit<MenuPopupProps, 'children'> &
|
|
@@ -44,6 +46,7 @@ export type FilterMultiSelectProps = {
|
|
|
44
46
|
export const FilterMultiSelect = ({
|
|
45
47
|
trigger,
|
|
46
48
|
children,
|
|
49
|
+
customMenuPopup,
|
|
47
50
|
isOpen,
|
|
48
51
|
defaultOpen,
|
|
49
52
|
onOpenChange,
|
|
@@ -58,9 +61,11 @@ export const FilterMultiSelect = ({
|
|
|
58
61
|
onSearchInputChange,
|
|
59
62
|
triggerRef,
|
|
60
63
|
className,
|
|
64
|
+
...restProps
|
|
61
65
|
}: FilterMultiSelectProps): JSX.Element => {
|
|
66
|
+
const MenuComponent = customMenuPopup ?? MenuPopup
|
|
62
67
|
const menuTriggerProps = { isOpen, defaultOpen, onOpenChange, triggerRef }
|
|
63
|
-
const menuPopupProps = { isLoading, loadingSkeleton }
|
|
68
|
+
const menuPopupProps = { isLoading, loadingSkeleton, ...restProps }
|
|
64
69
|
const disabledKeys: Selection = new Set(
|
|
65
70
|
items?.filter((item) => item.isDisabled === true).map((disabledItem) => disabledItem.value),
|
|
66
71
|
)
|
|
@@ -79,11 +84,11 @@ export const FilterMultiSelect = ({
|
|
|
79
84
|
<MenuTriggerProvider {...menuTriggerProps}>
|
|
80
85
|
<div className={className}>
|
|
81
86
|
<MenuTriggerConsumer>{trigger}</MenuTriggerConsumer>
|
|
82
|
-
<
|
|
87
|
+
<MenuComponent aria-label={label} {...menuPopupProps}>
|
|
83
88
|
<SelectionProvider {...selectionProps}>
|
|
84
89
|
<SelectionConsumer>{children}</SelectionConsumer>
|
|
85
90
|
</SelectionProvider>
|
|
86
|
-
</
|
|
91
|
+
</MenuComponent>
|
|
87
92
|
</div>
|
|
88
93
|
</MenuTriggerProvider>
|
|
89
94
|
)
|
|
@@ -104,3 +109,4 @@ FilterMultiSelect.MenuFooter = MenuFooter // For layout
|
|
|
104
109
|
FilterMultiSelect.MenuLoadingSkeleton = MenuLoadingSkeleton // Menu Loading Skeleton example
|
|
105
110
|
FilterMultiSelect.LoadMoreButton = LoadMoreButton
|
|
106
111
|
FilterMultiSelect.NoResults = NoResults
|
|
112
|
+
FilterMultiSelect.ResponsiveMenuPopup = ResponsiveMenuPopup
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Controls, Meta, Canvas } from '@storybook/blocks'
|
|
1
|
+
import { Controls, Meta, Canvas, DocsStory } from '@storybook/blocks'
|
|
2
2
|
import { ResourceLinks, KAIOInstallation, NoClipCanvas } from '~storybook/components'
|
|
3
3
|
import * as FilterMultiSelectStories from './FilterMultiSelect.stories'
|
|
4
4
|
|
|
@@ -30,6 +30,14 @@ The FilterMultiSelect is a component relies heavily on consumer implemntation. I
|
|
|
30
30
|
|
|
31
31
|
<Canvas of={FilterMultiSelectStories.WithSectionHeaders} />
|
|
32
32
|
|
|
33
|
+
### With customMenuPopup component
|
|
34
|
+
|
|
35
|
+
You can replace the `MenuPopup` component within the FilterMultiSelect to allow flexibility in how the popup's placement is determined. While the default behavior should satisfy most scenarios, this can be used when there is limited vertical space available in the viewport.
|
|
36
|
+
|
|
37
|
+
<Canvas of={FilterMultiSelectStories.AboveIfAvailable} />
|
|
38
|
+
|
|
39
|
+
For convenience, a primitive called `ResponsiveMenuPopup` that can be accessed via dot notation that will automatically adjust the placement and size of the popup based on the available window height. This implementation uses `floating-ui` and the [Popover API](https://developer.mozilla.org/en-US/docs/Web/API/Popover_API) instead of `react-aria` hooks. It also locks scroll when the popup is active.
|
|
40
|
+
|
|
33
41
|
### Async
|
|
34
42
|
|
|
35
43
|
The following is an example of how you may create an async FilterMultiSelect using `@tanstack/react-query`.
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import React, { useState } from 'react'
|
|
2
2
|
import type { Selection } from '@react-types/shared'
|
|
3
3
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
4
|
+
import { expect, userEvent, waitFor, within } from '@storybook/test'
|
|
4
5
|
import isChromatic from 'chromatic'
|
|
5
6
|
import { InlineNotification } from '~components/Notification'
|
|
6
7
|
import { TextField } from '~components/TextField'
|
|
7
8
|
import { FilterMultiSelect, getSelectedOptionLabels } from '..'
|
|
8
|
-
import { mockItems } from './MockData'
|
|
9
|
+
import { mockItems, mockManyItems } from './MockData'
|
|
9
10
|
|
|
10
11
|
const IS_CHROMATIC = isChromatic()
|
|
11
12
|
|
|
@@ -14,7 +15,7 @@ const meta = {
|
|
|
14
15
|
component: FilterMultiSelect,
|
|
15
16
|
parameters: {
|
|
16
17
|
docs: {
|
|
17
|
-
source: { type: '
|
|
18
|
+
source: { type: 'auto' },
|
|
18
19
|
},
|
|
19
20
|
},
|
|
20
21
|
args: {
|
|
@@ -178,6 +179,7 @@ export const WithSectionHeaders: Story = {
|
|
|
178
179
|
...FilterMultiSelectTemplate,
|
|
179
180
|
args: {
|
|
180
181
|
isOpen: IS_CHROMATIC || undefined,
|
|
182
|
+
items: mockManyItems,
|
|
181
183
|
children: (): JSX.Element => (
|
|
182
184
|
<>
|
|
183
185
|
<FilterMultiSelect.SearchInput />
|
|
@@ -308,3 +310,78 @@ export const WithSectionNotification: Story = {
|
|
|
308
310
|
chromatic: { disable: false },
|
|
309
311
|
},
|
|
310
312
|
}
|
|
313
|
+
|
|
314
|
+
const sourceCode = `
|
|
315
|
+
<FilterMultiSelect
|
|
316
|
+
{...filterMultiSelectProps}
|
|
317
|
+
customMenuPopup={(props): JSX.Element => (
|
|
318
|
+
// This will replace the default MenuPopup with a custom one. The rest of the component should still be implemented as the FilterMultiSelect pattern.
|
|
319
|
+
<FilterMultiSelect.ResponsiveMenuPopup {...props} />
|
|
320
|
+
)}
|
|
321
|
+
>
|
|
322
|
+
{/* FilterMultiSelect children */}
|
|
323
|
+
</FilterMultiSelect>
|
|
324
|
+
`
|
|
325
|
+
|
|
326
|
+
export const AboveIfAvailable: Story = {
|
|
327
|
+
...WithSectionNotification,
|
|
328
|
+
name: 'With customMenuPopup and vertical placement',
|
|
329
|
+
parameters: {
|
|
330
|
+
viewport: {
|
|
331
|
+
viewports: {
|
|
332
|
+
LimitedViewportAutoPlace: {
|
|
333
|
+
name: 'Limited vertical space',
|
|
334
|
+
styles: {
|
|
335
|
+
width: '1024px',
|
|
336
|
+
height: '650px',
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
defaultViewport: 'LimitedViewportAutoPlace',
|
|
341
|
+
},
|
|
342
|
+
docs: { source: { code: sourceCode } },
|
|
343
|
+
},
|
|
344
|
+
args: {
|
|
345
|
+
customMenuPopup: (props): JSX.Element => <FilterMultiSelect.ResponsiveMenuPopup {...props} />,
|
|
346
|
+
},
|
|
347
|
+
decorators: [
|
|
348
|
+
(Story) => (
|
|
349
|
+
<div>
|
|
350
|
+
<div style={{ height: '80vh', maxHeight: '500px' }}>Content above</div>
|
|
351
|
+
<Story />
|
|
352
|
+
</div>
|
|
353
|
+
),
|
|
354
|
+
],
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export const ShouldResize: Story = {
|
|
358
|
+
...AboveIfAvailable,
|
|
359
|
+
name: 'With customMenuPopup, vertical placement and resized popup',
|
|
360
|
+
parameters: {
|
|
361
|
+
chromatic: {
|
|
362
|
+
disable: false,
|
|
363
|
+
},
|
|
364
|
+
viewport: {
|
|
365
|
+
viewports: {
|
|
366
|
+
LimitedViewportAutoPlace: {
|
|
367
|
+
name: 'Limited vertical space',
|
|
368
|
+
styles: {
|
|
369
|
+
width: '1024px',
|
|
370
|
+
height: '450px',
|
|
371
|
+
},
|
|
372
|
+
},
|
|
373
|
+
},
|
|
374
|
+
defaultViewport: 'LimitedViewportAutoPlace',
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
play: async ({ canvasElement, step }) => {
|
|
378
|
+
const canvas = within(canvasElement.parentElement!)
|
|
379
|
+
const triggerButton = await canvas.findByRole('button', {
|
|
380
|
+
name: /Engineer/i,
|
|
381
|
+
})
|
|
382
|
+
await step('Trigger opens the FilterMultiSelect dialog', async () => {
|
|
383
|
+
await userEvent.click(triggerButton)
|
|
384
|
+
await waitFor(() => expect(canvas.getByRole('dialog')).toBeVisible())
|
|
385
|
+
})
|
|
386
|
+
},
|
|
387
|
+
}
|
|
@@ -58,3 +58,42 @@ export const locationDemographicValues = [
|
|
|
58
58
|
label: 'London',
|
|
59
59
|
},
|
|
60
60
|
]
|
|
61
|
+
|
|
62
|
+
export const mockManyItems: ItemType[] = [
|
|
63
|
+
{ label: 'Front-End', value: 'id-fe', count: '1245' },
|
|
64
|
+
{ label: 'Back-End', value: 'id-be', count: '4', isDisabled: true },
|
|
65
|
+
{ label: 'SRE', value: 'id-sre', count: '4', isDisabled: true },
|
|
66
|
+
{ label: 'Dev-ops', value: 'id-devops' },
|
|
67
|
+
{ label: 'Others', value: 'id-others' },
|
|
68
|
+
{
|
|
69
|
+
label: 'Engineer-type-1 has a really really long label',
|
|
70
|
+
value: 'id-type-1',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
label: 'Engineer-type-2 also has a really really long label',
|
|
74
|
+
value: 'id-type-2',
|
|
75
|
+
count: '156',
|
|
76
|
+
},
|
|
77
|
+
{ label: 'Engineer-type-3', value: 'id-type-3' },
|
|
78
|
+
{
|
|
79
|
+
label: 'Engineer-type-4',
|
|
80
|
+
value: 'id-type-4',
|
|
81
|
+
count: '4',
|
|
82
|
+
isDisabled: true,
|
|
83
|
+
},
|
|
84
|
+
{ label: 'Engineer-type-5', value: 'id-type-5' },
|
|
85
|
+
{ label: 'UI Designer', value: 'id-ui', count: '42' },
|
|
86
|
+
{ label: 'UX Researcher', value: 'id-ux', count: '15' },
|
|
87
|
+
{ label: 'Product Manager', value: 'id-pm', count: '28' },
|
|
88
|
+
{ label: 'Project Manager', value: 'id-project', count: '19', isDisabled: true },
|
|
89
|
+
{ label: 'Data Scientist', value: 'id-ds', count: '11' },
|
|
90
|
+
{ label: 'Machine Learning Engineer', value: 'id-ml', count: '7' },
|
|
91
|
+
{ label: 'QA Tester', value: 'id-qa', count: '22' },
|
|
92
|
+
{
|
|
93
|
+
label: 'Technical Writer with documentation expertise',
|
|
94
|
+
value: 'id-tech-writer',
|
|
95
|
+
count: '5',
|
|
96
|
+
},
|
|
97
|
+
{ label: 'DevSecOps Engineer', value: 'id-devsecops', count: '3', isDisabled: true },
|
|
98
|
+
{ label: 'Cloud Architect', value: 'id-cloud', count: '8' },
|
|
99
|
+
]
|
package/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.spec.tsx
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import React from 'react'
|
|
2
2
|
import { render, screen, waitFor } from '@testing-library/react'
|
|
3
3
|
import userEvent from '@testing-library/user-event'
|
|
4
|
-
import { vi } from 'vitest'
|
|
5
4
|
import { FilterTriggerButton } from '~components/Filter/FilterMultiSelect/subcomponents/Trigger'
|
|
6
5
|
import { MenuPopup } from '../../subcomponents/MenuPopup'
|
|
7
6
|
import { MenuTriggerProvider, type MenuTriggerProviderProps } from './MenuTriggerProvider'
|
|
@@ -53,15 +52,11 @@ describe('<MenuTriggerProvider /> - Visual content', () => {
|
|
|
53
52
|
rerender(<MenuTriggerProviderWrapper isOpen={false} />)
|
|
54
53
|
expect(screen.queryByText('menu-content-mock')).not.toBeInTheDocument()
|
|
55
54
|
})
|
|
56
|
-
|
|
57
|
-
it('fires the onOpenChange callback when the trigger is interacted', async () => {
|
|
55
|
+
it('fires the onOpenChange callback on user interaction to close the menu', async () => {
|
|
58
56
|
const onOpenChange = vi.fn()
|
|
59
57
|
render(<MenuTriggerProviderWrapper isOpen onOpenChange={onOpenChange} />)
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
name: 'trigger-display-label-mock',
|
|
63
|
-
})
|
|
64
|
-
await user.click(trigger)
|
|
59
|
+
await user.keyboard('{Escape}')
|
|
65
60
|
|
|
66
61
|
await waitFor(() => {
|
|
67
62
|
expect(onOpenChange).toBeCalledTimes(1)
|
|
@@ -86,17 +81,6 @@ describe('<MenuTriggerProvider /> - Mouse interaction', () => {
|
|
|
86
81
|
})
|
|
87
82
|
|
|
88
83
|
describe('Given the menu is opened', () => {
|
|
89
|
-
it('is closed when user clicks on the trigger', async () => {
|
|
90
|
-
render(<MenuTriggerProviderWrapper defaultOpen />)
|
|
91
|
-
const trigger = screen.getByRole('button', {
|
|
92
|
-
name: 'trigger-display-label-mock',
|
|
93
|
-
})
|
|
94
|
-
await user.click(trigger)
|
|
95
|
-
await waitFor(() => {
|
|
96
|
-
expect(screen.queryByText('menu-content-mock')).not.toBeInTheDocument()
|
|
97
|
-
})
|
|
98
|
-
})
|
|
99
|
-
|
|
100
84
|
it('is closed when user clicks outside of the menu', async () => {
|
|
101
85
|
render(<MenuTriggerProviderWrapper defaultOpen />)
|
|
102
86
|
await user.click(document.body)
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@layer kz-components {
|
|
2
|
+
.listBox {
|
|
3
|
+
list-style: none;
|
|
4
|
+
padding: var(--spacing-12);
|
|
5
|
+
margin: 0 var(--spacing-12) 0 0;
|
|
6
|
+
display: grid;
|
|
7
|
+
max-height: 22rem;
|
|
8
|
+
overflow-y: auto;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
.overflown {
|
|
12
|
+
padding-right: var(--spacing-12);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.hidden {
|
|
16
|
+
display: none;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.noResultsWrapper {
|
|
20
|
+
list-style: none;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -3,7 +3,7 @@ import { type Collection, type Key } from '@react-types/shared'
|
|
|
3
3
|
import classnames from 'classnames'
|
|
4
4
|
import { useSelectionContext } from '../../context/SelectionProvider'
|
|
5
5
|
import { type MultiSelectItem } from '../../types'
|
|
6
|
-
import styles from './ListBox.module.
|
|
6
|
+
import styles from './ListBox.module.css'
|
|
7
7
|
|
|
8
8
|
export type ListBoxItems = {
|
|
9
9
|
selectedItems: MultiSelectItem[]
|
package/src/Filter/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.module.scss
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
.listBoxSectionHeader {
|
|
13
|
+
position: relative; // this is needed to ensure the VisuallyHidden element doesn't impact the scroll height of the list
|
|
13
14
|
font-family: $typography-heading-6-font-family;
|
|
14
15
|
font-size: $typography-heading-6-font-size;
|
|
15
16
|
font-weight: $typography-heading-6-font-weight;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
@layer kz-components {
|
|
2
|
+
.menuPopup {
|
|
3
|
+
/* from $ca-z-index-dropdown */
|
|
4
|
+
z-index: 1000;
|
|
5
|
+
box-sizing: border-box;
|
|
6
|
+
background: var(--color-white);
|
|
7
|
+
color: var(--color-purple-800);
|
|
8
|
+
border-radius: var(--border-solid-border-radius);
|
|
9
|
+
box-shadow: var(--shadow-large-box-shadow);
|
|
10
|
+
padding: var(--spacing-12) 0;
|
|
11
|
+
margin-top: var(--spacing-6);
|
|
12
|
+
text-align: start;
|
|
13
|
+
width: var(--menu-container-width, 294px);
|
|
14
|
+
max-height: var(--menu-container-height, 500px);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.menuPopup[popover]:popover-open {
|
|
18
|
+
z-index: unset;
|
|
19
|
+
margin: 0;
|
|
20
|
+
inset: unset;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -2,7 +2,7 @@ import React from 'react'
|
|
|
2
2
|
import { FocusScope } from '@react-aria/focus'
|
|
3
3
|
import { DismissButton, useOverlay } from '@react-aria/overlays'
|
|
4
4
|
import { useMenuTriggerContext } from '../../context'
|
|
5
|
-
import styles from './MenuPopup.module.
|
|
5
|
+
import styles from './MenuPopup.module.css'
|
|
6
6
|
|
|
7
7
|
export type MenuPopupProps = {
|
|
8
8
|
isLoading?: boolean
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import React, { useEffect, useState, type HTMLAttributes } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
autoPlacement,
|
|
4
|
+
autoUpdate,
|
|
5
|
+
offset,
|
|
6
|
+
size,
|
|
7
|
+
useFloating,
|
|
8
|
+
type UseFloatingOptions,
|
|
9
|
+
} from '@floating-ui/react-dom'
|
|
10
|
+
import classnames from 'classnames'
|
|
11
|
+
import { FocusOn } from 'react-focus-on'
|
|
12
|
+
import { type OverrideClassName } from '~components/types/OverrideClassName'
|
|
13
|
+
import { useMenuTriggerContext } from '../../context'
|
|
14
|
+
import { type MenuPopupProps } from './MenuPopup'
|
|
15
|
+
import styles from './MenuPopup.module.css'
|
|
16
|
+
|
|
17
|
+
export type FloatingConfig = Pick<
|
|
18
|
+
UseFloatingOptions,
|
|
19
|
+
'placement' | 'strategy' | 'whileElementsMounted'
|
|
20
|
+
> & {
|
|
21
|
+
/** Whether the component should automatically resize based on the available window height.
|
|
22
|
+
* @default true
|
|
23
|
+
*/
|
|
24
|
+
shouldResize?: boolean
|
|
25
|
+
/** Whether the component should automatically flip to the top of the input based on the available window height.
|
|
26
|
+
* @default true
|
|
27
|
+
*/
|
|
28
|
+
shouldFlip?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type ResponsiveMenuPopupProps = MenuPopupProps & {
|
|
32
|
+
floatingConfig?: FloatingConfig
|
|
33
|
+
} & OverrideClassName<HTMLAttributes<HTMLDivElement>>
|
|
34
|
+
|
|
35
|
+
/** This is a popup primitive that can be used with the FilterMultiSelect when there are overflow issues with the original implementation. This uses the floating-ui */
|
|
36
|
+
export const ResponsiveMenuPopup = ({
|
|
37
|
+
children,
|
|
38
|
+
floatingConfig = {
|
|
39
|
+
placement: 'bottom-start',
|
|
40
|
+
strategy: 'absolute',
|
|
41
|
+
whileElementsMounted: autoUpdate,
|
|
42
|
+
shouldFlip: true,
|
|
43
|
+
shouldResize: true,
|
|
44
|
+
},
|
|
45
|
+
classNameOverride,
|
|
46
|
+
isLoading,
|
|
47
|
+
loadingSkeleton,
|
|
48
|
+
...restProps
|
|
49
|
+
}: ResponsiveMenuPopupProps): JSX.Element => {
|
|
50
|
+
const [floatingElement, setFloatingElement] = useState<HTMLDivElement | null>(null)
|
|
51
|
+
const { menuTriggerState, buttonRef } = useMenuTriggerContext()
|
|
52
|
+
const referenceElement = buttonRef.current
|
|
53
|
+
|
|
54
|
+
const { floatingStyles, update } = useFloating({
|
|
55
|
+
elements: {
|
|
56
|
+
reference: referenceElement,
|
|
57
|
+
floating: floatingElement,
|
|
58
|
+
},
|
|
59
|
+
middleware: [
|
|
60
|
+
offset(6),
|
|
61
|
+
floatingConfig.shouldFlip &&
|
|
62
|
+
autoPlacement({ allowedPlacements: ['bottom-start', 'top-start'] }),
|
|
63
|
+
floatingConfig.shouldResize &&
|
|
64
|
+
size({
|
|
65
|
+
apply({ availableHeight, elements }) {
|
|
66
|
+
Object.assign(elements.floating.style, {
|
|
67
|
+
maxHeight: Math.max(250, Math.min(availableHeight - 12, 500)) + 'px',
|
|
68
|
+
})
|
|
69
|
+
},
|
|
70
|
+
}),
|
|
71
|
+
],
|
|
72
|
+
...floatingConfig,
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
const handleReturnFocus = (): void => {
|
|
76
|
+
requestAnimationFrame(() => {
|
|
77
|
+
buttonRef.current?.focus()
|
|
78
|
+
})
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
useEffect(() => {
|
|
82
|
+
if (floatingElement && referenceElement) {
|
|
83
|
+
floatingElement.showPopover?.()
|
|
84
|
+
update()
|
|
85
|
+
}
|
|
86
|
+
}, [floatingElement, referenceElement, update])
|
|
87
|
+
|
|
88
|
+
return menuTriggerState.isOpen ? (
|
|
89
|
+
<FocusOn
|
|
90
|
+
enabled={menuTriggerState.isOpen}
|
|
91
|
+
scrollLock={true}
|
|
92
|
+
returnFocus={false}
|
|
93
|
+
onClickOutside={menuTriggerState.close}
|
|
94
|
+
onEscapeKey={menuTriggerState.close}
|
|
95
|
+
onDeactivation={handleReturnFocus}
|
|
96
|
+
>
|
|
97
|
+
<div
|
|
98
|
+
ref={setFloatingElement}
|
|
99
|
+
style={floatingStyles}
|
|
100
|
+
className={classnames(styles.menuPopup, classNameOverride)}
|
|
101
|
+
role="dialog"
|
|
102
|
+
aria-modal="true"
|
|
103
|
+
// @ts-expect-error: popover is valid in supported browsers
|
|
104
|
+
popover="manual"
|
|
105
|
+
{...restProps}
|
|
106
|
+
>
|
|
107
|
+
{isLoading && loadingSkeleton ? loadingSkeleton : children}
|
|
108
|
+
</div>
|
|
109
|
+
</FocusOn>
|
|
110
|
+
) : (
|
|
111
|
+
<></>
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
ResponsiveMenuPopup.displayName = 'FilterMultiSelect.ResponsiveMenuPopup'
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
var styles = {
|
|
4
|
-
"listBox": "ListBox-module_listBox__q95MO",
|
|
5
|
-
"overflown": "ListBox-module_overflown__wChQA",
|
|
6
|
-
"hidden": "ListBox-module_hidden__eYdXv",
|
|
7
|
-
"noResultsWrapper": "ListBox-module_noResultsWrapper__WcLRm"
|
|
8
|
-
};
|
|
9
|
-
module.exports = styles;
|
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
@import '~@kaizen/design-tokens/sass/spacing';
|
|
2
|
-
|
|
3
|
-
@layer kz-components {
|
|
4
|
-
.listBox {
|
|
5
|
-
list-style: none;
|
|
6
|
-
padding: $spacing-sm;
|
|
7
|
-
margin: 0 $spacing-sm 0 0;
|
|
8
|
-
display: grid;
|
|
9
|
-
max-height: 22rem;
|
|
10
|
-
overflow-y: auto;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
.overflown {
|
|
14
|
-
padding-right: $spacing-sm;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.hidden {
|
|
18
|
-
display: none;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
// this is a div but remove styles briefly flickering to a bullet list as the sections are removed
|
|
22
|
-
.noResultsWrapper {
|
|
23
|
-
list-style: none;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
@import '~@kaizen/design-tokens/sass/spacing';
|
|
2
|
-
@import '~@kaizen/design-tokens/sass/shadow';
|
|
3
|
-
@import '~@kaizen/design-tokens/sass/border';
|
|
4
|
-
@import '~@kaizen/design-tokens/sass/color';
|
|
5
|
-
|
|
6
|
-
@layer kz-components {
|
|
7
|
-
// figma hard coded: https://www.figma.com/file/eZKEE5kXbEMY3lx84oz8iN/%E2%9D%A4%EF%B8%8F-UI-Kit%3A-Heart?node-id=22814%3A96966
|
|
8
|
-
$menu-container-width: 294px;
|
|
9
|
-
$menu-container-max-height: 312px;
|
|
10
|
-
|
|
11
|
-
.menuPopup {
|
|
12
|
-
position: absolute;
|
|
13
|
-
z-index: 1000; // from $ca-z-index-dropdown
|
|
14
|
-
box-sizing: border-box;
|
|
15
|
-
background: $color-white;
|
|
16
|
-
color: $color-purple-800;
|
|
17
|
-
border-radius: $border-solid-border-radius;
|
|
18
|
-
box-shadow: $shadow-large-box-shadow;
|
|
19
|
-
padding: $spacing-sm 0;
|
|
20
|
-
margin-top: $spacing-xs;
|
|
21
|
-
text-align: start;
|
|
22
|
-
width: $menu-container-width;
|
|
23
|
-
}
|
|
24
|
-
}
|