@kaizen/components 0.0.0-canary-test-select-aria-props-update-20250616233358 → 0.0.0-canary-test-fms-popover-api-update-20250624021814

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 (39) hide show
  1. package/dist/cjs/src/Filter/FilterMultiSelect/FilterMultiSelect.cjs +7 -3
  2. package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.cjs +1 -1
  3. package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css.cjs +9 -0
  4. package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.cjs +57 -38
  5. package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/{MenuPopup.module.scss.cjs → MenuPopup.module.css.cjs} +1 -1
  6. package/dist/cjs/src/LikertScaleLegacy/LikertScaleLegacy.cjs +5 -3
  7. package/dist/esm/src/Filter/FilterMultiSelect/FilterMultiSelect.mjs +7 -3
  8. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.mjs +1 -1
  9. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css.mjs +7 -0
  10. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.mjs +58 -41
  11. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css.mjs +4 -0
  12. package/dist/esm/src/LikertScaleLegacy/LikertScaleLegacy.mjs +5 -3
  13. package/dist/styles.css +28 -21
  14. package/dist/types/Filter/FilterMultiSelect/FilterMultiSelect.d.ts +1 -1
  15. package/dist/types/Filter/FilterMultiSelect/_docs/MockData.d.ts +1 -0
  16. package/dist/types/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.d.ts +7 -4
  17. package/dist/types/LikertScaleLegacy/LikertScaleLegacy.d.ts +5 -1
  18. package/package.json +1 -1
  19. package/src/Filter/FilterBar/subcomponents/FilterBarMultiSelect/FilterBarMultiSelect.spec.tsx +1 -0
  20. package/src/Filter/FilterMultiSelect/FilterMultiSelect.tsx +3 -2
  21. package/src/Filter/FilterMultiSelect/_docs/FilterMultiSelect.mdx +8 -0
  22. package/src/Filter/FilterMultiSelect/_docs/FilterMultiSelect.stories.tsx +89 -1
  23. package/src/Filter/FilterMultiSelect/_docs/MockData.ts +39 -0
  24. package/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.spec.tsx +2 -18
  25. package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css +22 -0
  26. package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.tsx +1 -1
  27. package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css +22 -0
  28. package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.tsx +62 -42
  29. package/src/LikertScaleLegacy/LikertScaleLegacy.spec.tsx +1 -0
  30. package/src/LikertScaleLegacy/LikertScaleLegacy.tsx +7 -1
  31. package/src/LikertScaleLegacy/_docs/LikertScaleLegacy.mdx +8 -0
  32. package/src/LikertScaleLegacy/_docs/LikertScaleLegacy.stories.tsx +30 -1
  33. package/src/__next__/Menu/_docs/Menu--migration-guide.mdx +91 -0
  34. package/src/__next__/Select/_docs/Select.mdx +1 -1
  35. package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss.cjs +0 -9
  36. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss.mjs +0 -7
  37. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.scss.mjs +0 -4
  38. package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss +0 -25
  39. package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.scss +0 -24
@@ -41,7 +41,8 @@ var FilterMultiSelect = function (_a) {
41
41
  selectionMode = _b === void 0 ? 'multiple' : _b,
42
42
  onSearchInputChange = _a.onSearchInputChange,
43
43
  triggerRef = _a.triggerRef,
44
- className = _a.className;
44
+ className = _a.className,
45
+ floatingOptions = _a.floatingOptions;
45
46
  var menuTriggerProps = {
46
47
  isOpen: isOpen,
47
48
  defaultOpen: defaultOpen,
@@ -50,7 +51,8 @@ var FilterMultiSelect = function (_a) {
50
51
  };
51
52
  var menuPopupProps = {
52
53
  isLoading: isLoading,
53
- loadingSkeleton: loadingSkeleton
54
+ loadingSkeleton: loadingSkeleton,
55
+ floatingOptions: floatingOptions
54
56
  };
55
57
  var disabledKeys = new Set(items === null || items === void 0 ? void 0 : items.filter(function (item) {
56
58
  return item.isDisabled === true;
@@ -69,7 +71,9 @@ var FilterMultiSelect = function (_a) {
69
71
  };
70
72
  return React__default.default.createElement(MenuTriggerProvider.MenuTriggerProvider, tslib.__assign({}, menuTriggerProps), React__default.default.createElement("div", {
71
73
  className: className
72
- }, React__default.default.createElement(MenuTriggerProvider.MenuTriggerConsumer, null, trigger), React__default.default.createElement(MenuPopup.MenuPopup, tslib.__assign({}, menuPopupProps), React__default.default.createElement(SelectionProvider.SelectionProvider, tslib.__assign({}, selectionProps), React__default.default.createElement(SelectionProvider.SelectionConsumer, null, children)))));
74
+ }, React__default.default.createElement(MenuTriggerProvider.MenuTriggerConsumer, null, trigger), React__default.default.createElement(MenuPopup.MenuPopup, tslib.__assign({
75
+ "aria-label": label
76
+ }, menuPopupProps), React__default.default.createElement(SelectionProvider.SelectionProvider, tslib.__assign({}, selectionProps), React__default.default.createElement(SelectionProvider.SelectionConsumer, null, children)))));
73
77
  };
74
78
  FilterMultiSelect.displayName = 'FilterMultiSelect';
75
79
  FilterMultiSelect.TriggerButton = FilterTriggerButton.FilterTriggerButton;
@@ -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.scss.cjs');
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;
@@ -2,55 +2,74 @@
2
2
 
3
3
  var tslib = require('tslib');
4
4
  var React = require('react');
5
- var focus = require('@react-aria/focus');
6
- var overlays = require('@react-aria/overlays');
5
+ var reactDom = require('@floating-ui/react-dom');
6
+ var classnames = require('classnames');
7
+ var reactFocusOn = require('react-focus-on');
7
8
  var MenuTriggerProvider = require('../../context/MenuTriggerProvider/MenuTriggerProvider.cjs');
8
9
  require('../../context/SelectionProvider/SelectionProvider.cjs');
9
- var MenuPopup_module = require('./MenuPopup.module.scss.cjs');
10
+ var MenuPopup_module = require('./MenuPopup.module.css.cjs');
10
11
  function _interopDefault(e) {
11
12
  return e && e.__esModule ? e : {
12
13
  default: e
13
14
  };
14
15
  }
15
16
  var React__default = /*#__PURE__*/_interopDefault(React);
17
+ var classnames__default = /*#__PURE__*/_interopDefault(classnames);
16
18
  var MenuPopup = function (_a) {
17
- var isLoading = _a.isLoading,
19
+ var children = _a.children,
20
+ floatingOptions = _a.floatingOptions,
21
+ classNameOverride = _a.classNameOverride,
22
+ isLoading = _a.isLoading,
18
23
  loadingSkeleton = _a.loadingSkeleton,
19
- children = _a.children;
20
- var menuTriggerState = MenuTriggerProvider.useMenuTriggerContext().menuTriggerState;
21
- var onClose = function () {
22
- return menuTriggerState.close();
24
+ restProps = tslib.__rest(_a, ["children", "floatingOptions", "classNameOverride", "isLoading", "loadingSkeleton"]);
25
+ var _b = React.useState(null),
26
+ floatingElement = _b[0],
27
+ setFloatingElement = _b[1];
28
+ var _c = MenuTriggerProvider.useMenuTriggerContext(),
29
+ menuTriggerState = _c.menuTriggerState,
30
+ buttonRef = _c.buttonRef;
31
+ var referenceElement = buttonRef.current;
32
+ var _d = reactDom.useFloating(tslib.__assign({
33
+ placement: 'bottom-start',
34
+ elements: {
35
+ reference: referenceElement,
36
+ floating: floatingElement
37
+ },
38
+ strategy: 'absolute',
39
+ middleware: [reactDom.offset(6)],
40
+ whileElementsMounted: reactDom.autoUpdate
41
+ }, floatingOptions)),
42
+ floatingStyles = _d.floatingStyles,
43
+ update = _d.update;
44
+ var handleReturnFocus = function () {
45
+ requestAnimationFrame(function () {
46
+ var _a;
47
+ (_a = buttonRef.current) === null || _a === void 0 ? void 0 : _a.focus();
48
+ });
23
49
  };
24
- // Handle events that should cause the menu to close,
25
- // e.g. blur, clicking outside, or pressing the escape key.
26
- var overlayRef = React__default.default.createRef();
27
- var overlayProps = overlays.useOverlay({
28
- onClose: onClose,
29
- isOpen: menuTriggerState.isOpen,
30
- isDismissable: true
31
- }, overlayRef).overlayProps;
32
- // Wrap in <FocusScope> so that focus is restored back to the trigger when the menu is closed
33
- // and auto focus on the first focusable item after loading. (disable eslint no-autofocus error for it)
34
- // In addition, add hidden <DismissButton> components at the start and end of the list
35
- // to allow screen reader users to dismiss the popup easily.
36
- return menuTriggerState.isOpen ? React__default.default.createElement("div", tslib.__assign({}, overlayProps, {
37
- ref: overlayRef,
38
- className: MenuPopup_module.menuPopup
39
- }), isLoading && loadingSkeleton ? React__default.default.createElement(React__default.default.Fragment, null, React__default.default.createElement(overlays.DismissButton, {
40
- onDismiss: onClose
41
- }), loadingSkeleton, React__default.default.createElement(overlays.DismissButton, {
42
- onDismiss: onClose
43
- })) :
44
- // eslint-disable-next-line jsx-a11y/no-autofocus
45
- React__default.default.createElement(focus.FocusScope, {
46
- contain: true,
47
- autoFocus: true,
48
- restoreFocus: true
49
- }, React__default.default.createElement(overlays.DismissButton, {
50
- onDismiss: onClose
51
- }), children, React__default.default.createElement(overlays.DismissButton, {
52
- onDismiss: onClose
53
- }))) : React__default.default.createElement(React__default.default.Fragment, null);
50
+ React.useEffect(function () {
51
+ var _a;
52
+ if (floatingElement && referenceElement) {
53
+ (_a = floatingElement.showPopover) === null || _a === void 0 ? void 0 : _a.call(floatingElement);
54
+ update();
55
+ }
56
+ }, [floatingElement, referenceElement, update]);
57
+ return menuTriggerState.isOpen ? React__default.default.createElement(reactFocusOn.FocusOn, {
58
+ enabled: menuTriggerState.isOpen,
59
+ scrollLock: false,
60
+ returnFocus: false,
61
+ onClickOutside: menuTriggerState.close,
62
+ onEscapeKey: menuTriggerState.close,
63
+ onDeactivation: handleReturnFocus
64
+ }, React__default.default.createElement("div", tslib.__assign({
65
+ ref: setFloatingElement,
66
+ style: floatingStyles,
67
+ className: classnames__default.default(MenuPopup_module.menuPopup, classNameOverride),
68
+ role: "dialog",
69
+ "aria-modal": "true",
70
+ // @ts-expect-error: popover is valid in supported browsers
71
+ popover: "manual"
72
+ }, restProps), isLoading && loadingSkeleton ? loadingSkeleton : children)) : React__default.default.createElement(React__default.default.Fragment, null);
54
73
  };
55
74
  MenuPopup.displayName = 'FilterMultiSelect.MenuPopup';
56
75
  exports.MenuPopup = MenuPopup;
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
3
  var styles = {
4
- "menuPopup": "MenuPopup-module_menuPopup__UVgnP"
4
+ "menuPopup": "MenuPopup-module_menuPopup__QgGEa"
5
5
  };
6
6
  module.exports = styles;
@@ -36,7 +36,8 @@ var LikertScaleLegacy = function (_a) {
36
36
  onSelect = _a.onSelect,
37
37
  validationMessage = _a.validationMessage,
38
38
  status = _a.status,
39
- labelId = _a.labelId;
39
+ labelId = _a.labelId,
40
+ isRequired = _a.isRequired;
40
41
  var _e = React.useState(null),
41
42
  hoveredItem = _e[0],
42
43
  setHoveredItem = _e[1];
@@ -83,11 +84,12 @@ var LikertScaleLegacy = function (_a) {
83
84
  var isRated = selectedItem && selectedItem.value > 0;
84
85
  return React__default.default.createElement("div", {
85
86
  className: classnames__default.default(LikertScaleLegacy_module.container, isRated && LikertScaleLegacy_module.rated, reversed && [LikertScaleLegacy_module.reversed], hoveredItem !== null && LikertScaleLegacy_module.hovered),
86
- "aria-labelledby": labelId,
87
+ "aria-labelledby": isRequired ? "".concat(labelId) : labelId,
87
88
  role: "radiogroup",
88
89
  tabIndex: -1,
89
90
  "aria-describedby": validationMessageId,
90
- "data-testid": dataTestId
91
+ "data-testid": dataTestId,
92
+ "aria-required": isRequired
91
93
  }, React__default.default.createElement("div", {
92
94
  className: LikertScaleLegacy_module.legend,
93
95
  "data-testid": dataTestId && "".concat(dataTestId, "-legend")
@@ -34,7 +34,8 @@ const FilterMultiSelect = /*#__PURE__*/function () {
34
34
  selectionMode = _b === void 0 ? 'multiple' : _b,
35
35
  onSearchInputChange = _a.onSearchInputChange,
36
36
  triggerRef = _a.triggerRef,
37
- className = _a.className;
37
+ className = _a.className,
38
+ floatingOptions = _a.floatingOptions;
38
39
  var menuTriggerProps = {
39
40
  isOpen: isOpen,
40
41
  defaultOpen: defaultOpen,
@@ -43,7 +44,8 @@ const FilterMultiSelect = /*#__PURE__*/function () {
43
44
  };
44
45
  var menuPopupProps = {
45
46
  isLoading: isLoading,
46
- loadingSkeleton: loadingSkeleton
47
+ loadingSkeleton: loadingSkeleton,
48
+ floatingOptions: floatingOptions
47
49
  };
48
50
  var disabledKeys = new Set(items === null || items === void 0 ? void 0 : items.filter(function (item) {
49
51
  return item.isDisabled === true;
@@ -62,7 +64,9 @@ const FilterMultiSelect = /*#__PURE__*/function () {
62
64
  };
63
65
  return /*#__PURE__*/React.createElement(MenuTriggerProvider, __assign({}, menuTriggerProps), /*#__PURE__*/React.createElement("div", {
64
66
  className: className
65
- }, /*#__PURE__*/React.createElement(MenuTriggerConsumer, null, trigger), /*#__PURE__*/React.createElement(MenuPopup, __assign({}, menuPopupProps), /*#__PURE__*/React.createElement(SelectionProvider, __assign({}, selectionProps), /*#__PURE__*/React.createElement(SelectionConsumer, null, children)))));
67
+ }, /*#__PURE__*/React.createElement(MenuTriggerConsumer, null, trigger), /*#__PURE__*/React.createElement(MenuPopup, __assign({
68
+ "aria-label": label
69
+ }, menuPopupProps), /*#__PURE__*/React.createElement(SelectionProvider, __assign({}, selectionProps), /*#__PURE__*/React.createElement(SelectionConsumer, null, children)))));
66
70
  };
67
71
  FilterMultiSelect.displayName = 'FilterMultiSelect';
68
72
  FilterMultiSelect.TriggerButton = FilterTriggerButton;
@@ -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.scss.mjs';
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) {
@@ -0,0 +1,7 @@
1
+ var styles = {
2
+ "listBox": "ListBox-module_listBox__HBScm",
3
+ "overflown": "ListBox-module_overflown__PdKED",
4
+ "hidden": "ListBox-module_hidden__mO-oL",
5
+ "noResultsWrapper": "ListBox-module_noResultsWrapper__RnMj0"
6
+ };
7
+ export { styles as default };
@@ -1,50 +1,67 @@
1
- import { __assign } from 'tslib';
2
- import React from 'react';
3
- import { FocusScope } from '@react-aria/focus';
4
- import { useOverlay, DismissButton } from '@react-aria/overlays';
1
+ import { __rest, __assign } from 'tslib';
2
+ import React, { useState, useEffect } from 'react';
3
+ import { useFloating, autoUpdate, offset } from '@floating-ui/react-dom';
4
+ import classnames from 'classnames';
5
+ import { FocusOn } from 'react-focus-on';
5
6
  import { useMenuTriggerContext } from '../../context/MenuTriggerProvider/MenuTriggerProvider.mjs';
6
7
  import '../../context/SelectionProvider/SelectionProvider.mjs';
7
- import styles from './MenuPopup.module.scss.mjs';
8
+ import styles from './MenuPopup.module.css.mjs';
8
9
  const MenuPopup = /*#__PURE__*/function () {
9
10
  const MenuPopup = function (_a) {
10
- var isLoading = _a.isLoading,
11
+ var children = _a.children,
12
+ floatingOptions = _a.floatingOptions,
13
+ classNameOverride = _a.classNameOverride,
14
+ isLoading = _a.isLoading,
11
15
  loadingSkeleton = _a.loadingSkeleton,
12
- children = _a.children;
13
- var menuTriggerState = useMenuTriggerContext().menuTriggerState;
14
- var onClose = function () {
15
- return menuTriggerState.close();
16
+ restProps = __rest(_a, ["children", "floatingOptions", "classNameOverride", "isLoading", "loadingSkeleton"]);
17
+ var _b = useState(null),
18
+ floatingElement = _b[0],
19
+ setFloatingElement = _b[1];
20
+ var _c = useMenuTriggerContext(),
21
+ menuTriggerState = _c.menuTriggerState,
22
+ buttonRef = _c.buttonRef;
23
+ var referenceElement = buttonRef.current;
24
+ var _d = useFloating(__assign({
25
+ placement: 'bottom-start',
26
+ elements: {
27
+ reference: referenceElement,
28
+ floating: floatingElement
29
+ },
30
+ strategy: 'absolute',
31
+ middleware: [offset(6)],
32
+ whileElementsMounted: autoUpdate
33
+ }, floatingOptions)),
34
+ floatingStyles = _d.floatingStyles,
35
+ update = _d.update;
36
+ var handleReturnFocus = function () {
37
+ requestAnimationFrame(function () {
38
+ var _a;
39
+ (_a = buttonRef.current) === null || _a === void 0 ? void 0 : _a.focus();
40
+ });
16
41
  };
17
- // Handle events that should cause the menu to close,
18
- // e.g. blur, clicking outside, or pressing the escape key.
19
- var overlayRef = /*#__PURE__*/React.createRef();
20
- var overlayProps = useOverlay({
21
- onClose: onClose,
22
- isOpen: menuTriggerState.isOpen,
23
- isDismissable: true
24
- }, overlayRef).overlayProps;
25
- // Wrap in <FocusScope> so that focus is restored back to the trigger when the menu is closed
26
- // and auto focus on the first focusable item after loading. (disable eslint no-autofocus error for it)
27
- // In addition, add hidden <DismissButton> components at the start and end of the list
28
- // to allow screen reader users to dismiss the popup easily.
29
- return menuTriggerState.isOpen ? (/*#__PURE__*/React.createElement("div", __assign({}, overlayProps, {
30
- ref: overlayRef,
31
- className: styles.menuPopup
32
- }), isLoading && loadingSkeleton ? (/*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(DismissButton, {
33
- onDismiss: onClose
34
- }), loadingSkeleton, /*#__PURE__*/React.createElement(DismissButton, {
35
- onDismiss: onClose
36
- }))) : (
37
- /*#__PURE__*/
38
- // eslint-disable-next-line jsx-a11y/no-autofocus
39
- React.createElement(FocusScope, {
40
- contain: true,
41
- autoFocus: true,
42
- restoreFocus: true
43
- }, /*#__PURE__*/React.createElement(DismissButton, {
44
- onDismiss: onClose
45
- }), children, /*#__PURE__*/React.createElement(DismissButton, {
46
- onDismiss: onClose
47
- }))))) : (/*#__PURE__*/React.createElement(React.Fragment, null));
42
+ useEffect(function () {
43
+ var _a;
44
+ if (floatingElement && referenceElement) {
45
+ (_a = floatingElement.showPopover) === null || _a === void 0 ? void 0 : _a.call(floatingElement);
46
+ update();
47
+ }
48
+ }, [floatingElement, referenceElement, update]);
49
+ return menuTriggerState.isOpen ? (/*#__PURE__*/React.createElement(FocusOn, {
50
+ enabled: menuTriggerState.isOpen,
51
+ scrollLock: false,
52
+ returnFocus: false,
53
+ onClickOutside: menuTriggerState.close,
54
+ onEscapeKey: menuTriggerState.close,
55
+ onDeactivation: handleReturnFocus
56
+ }, /*#__PURE__*/React.createElement("div", __assign({
57
+ ref: setFloatingElement,
58
+ style: floatingStyles,
59
+ className: classnames(styles.menuPopup, classNameOverride),
60
+ role: "dialog",
61
+ "aria-modal": "true",
62
+ // @ts-expect-error: popover is valid in supported browsers
63
+ popover: "manual"
64
+ }, restProps), isLoading && loadingSkeleton ? loadingSkeleton : children))) : (/*#__PURE__*/React.createElement(React.Fragment, null));
48
65
  };
49
66
  MenuPopup.displayName = 'FilterMultiSelect.MenuPopup';
50
67
  return MenuPopup;
@@ -0,0 +1,4 @@
1
+ var styles = {
2
+ "menuPopup": "MenuPopup-module_menuPopup__QgGEa"
3
+ };
4
+ export { styles as default };
@@ -27,7 +27,8 @@ var LikertScaleLegacy = function (_a) {
27
27
  onSelect = _a.onSelect,
28
28
  validationMessage = _a.validationMessage,
29
29
  status = _a.status,
30
- labelId = _a.labelId;
30
+ labelId = _a.labelId,
31
+ isRequired = _a.isRequired;
31
32
  var _e = useState(null),
32
33
  hoveredItem = _e[0],
33
34
  setHoveredItem = _e[1];
@@ -74,11 +75,12 @@ var LikertScaleLegacy = function (_a) {
74
75
  var isRated = selectedItem && selectedItem.value > 0;
75
76
  return /*#__PURE__*/React.createElement("div", {
76
77
  className: classnames(styles.container, isRated && styles.rated, reversed && [styles.reversed], hoveredItem !== null && styles.hovered),
77
- "aria-labelledby": labelId,
78
+ "aria-labelledby": isRequired ? "".concat(labelId) : labelId,
78
79
  role: "radiogroup",
79
80
  tabIndex: -1,
80
81
  "aria-describedby": validationMessageId,
81
- "data-testid": dataTestId
82
+ "data-testid": dataTestId,
83
+ "aria-required": isRequired
82
84
  }, /*#__PURE__*/React.createElement("div", {
83
85
  className: styles.legend,
84
86
  "data-testid": dataTestId && "".concat(dataTestId, "-legend")
package/dist/styles.css CHANGED
@@ -3743,26 +3743,29 @@
3743
3743
  }
3744
3744
  }
3745
3745
 
3746
- /** THIS IS AN AUTOGENERATED FILE **/
3747
3746
  @layer kz-components {
3748
- .ListBox-module_listBox__q95MO {
3747
+ .ListBox-module_listBox__HBScm {
3749
3748
  list-style: none;
3750
- padding: var(--spacing-sm, 0.75rem);
3751
- margin: 0 var(--spacing-sm, 0.75rem) 0 0;
3749
+ padding: var(--spacing-12);
3750
+ margin: 0 var(--spacing-12) 0 0;
3752
3751
  display: grid;
3753
3752
  max-height: 22rem;
3754
3753
  overflow-y: auto;
3755
3754
  }
3756
- .ListBox-module_overflown__wChQA {
3757
- padding-right: var(--spacing-sm, 0.75rem);
3755
+
3756
+ .ListBox-module_overflown__PdKED {
3757
+ padding-right: var(--spacing-12);
3758
3758
  }
3759
- .ListBox-module_hidden__eYdXv {
3759
+
3760
+ .ListBox-module_hidden__mO-oL {
3760
3761
  display: none;
3761
3762
  }
3762
- .ListBox-module_noResultsWrapper__WcLRm {
3763
+
3764
+ .ListBox-module_noResultsWrapper__RnMj0 {
3763
3765
  list-style: none;
3764
3766
  }
3765
3767
  }
3768
+
3766
3769
  /** THIS IS AN AUTOGENERATED FILE **/
3767
3770
  /** THIS IS AN AUTOGENERATED FILE **/
3768
3771
  /** THIS IS AN AUTOGENERATED FILE **/
@@ -3807,25 +3810,29 @@
3807
3810
  margin-right: var(--spacing-sm, 0.75rem);
3808
3811
  }
3809
3812
  }
3810
- /** THIS IS AN AUTOGENERATED FILE **/
3811
- /** THIS IS AN AUTOGENERATED FILE **/
3812
- /** THIS IS AN AUTOGENERATED FILE **/
3813
- /** THIS IS AN AUTOGENERATED FILE **/
3814
3813
  @layer kz-components {
3815
- .MenuPopup-module_menuPopup__UVgnP {
3816
- position: absolute;
3814
+ .MenuPopup-module_menuPopup__QgGEa {
3815
+ /* from $ca-z-index-dropdown */
3817
3816
  z-index: 1000;
3818
3817
  box-sizing: border-box;
3819
- background: var(--color-white, #ffffff);
3820
- color: var(--color-purple-800, #2f2438);
3821
- border-radius: var(--border-solid-border-radius, 7px);
3822
- box-shadow: var(--shadow-large-box-shadow, 0 3px 9px 0 rgba(0, 0, 0, 0.1), 0 8px 40px 0 rgba(0, 0, 0, 0.08));
3823
- padding: var(--spacing-sm, 0.75rem) 0;
3824
- margin-top: var(--spacing-xs, 0.375rem);
3818
+ background: var(--color-white);
3819
+ color: var(--color-purple-800);
3820
+ border-radius: var(--border-solid-border-radius);
3821
+ box-shadow: var(--shadow-large-box-shadow);
3822
+ padding: var(--spacing-6) 0;
3823
+ margin-top: var(--spacing-6);
3825
3824
  text-align: start;
3826
- width: 294px;
3825
+ width: var(--menu-container-width, 294px);
3826
+ max-height: var(--menu-container-height, 500px);
3827
+ }
3828
+
3829
+ .MenuPopup-module_menuPopup__QgGEa[popover]:popover-open {
3830
+ z-index: unset;
3831
+ margin: 0;
3832
+ inset: unset;
3827
3833
  }
3828
3834
  }
3835
+
3829
3836
  /** THIS IS AN AUTOGENERATED FILE **/
3830
3837
  /** THIS IS AN AUTOGENERATED FILE **/
3831
3838
  /** THIS IS AN AUTOGENERATED FILE **/
@@ -19,7 +19,7 @@ export type FilterMultiSelectProps = {
19
19
  className?: string;
20
20
  } & Omit<MenuPopupProps, 'children'> & Omit<MenuTriggerProviderProps, 'children'> & SelectionProps;
21
21
  export declare const FilterMultiSelect: {
22
- ({ trigger, children, isOpen, defaultOpen, onOpenChange, isLoading, loadingSkeleton, label, items, selectedKeys, defaultSelectedKeys, onSelectionChange, selectionMode, onSearchInputChange, triggerRef, className, }: FilterMultiSelectProps): JSX.Element;
22
+ ({ trigger, children, isOpen, defaultOpen, onOpenChange, isLoading, loadingSkeleton, label, items, selectedKeys, defaultSelectedKeys, onSelectionChange, selectionMode, onSearchInputChange, triggerRef, className, floatingOptions, }: FilterMultiSelectProps): JSX.Element;
23
23
  displayName: string;
24
24
  TriggerButton: {
25
25
  ({ selectedOptionLabels, label, classNameOverride, labelCharacterLimitBeforeTruncate, }: import("./subcomponents/Trigger").FilterTriggerButtonProps): JSX.Element;
@@ -8,3 +8,4 @@ export declare const locationDemographicValues: {
8
8
  demographicValueId: string;
9
9
  label: string;
10
10
  }[];
11
+ export declare const mockManyItems: ItemType[];
@@ -1,10 +1,13 @@
1
- import React from 'react';
1
+ import React, { type HTMLAttributes } from 'react';
2
+ import { type UseFloatingOptions } from '@floating-ui/react-dom';
3
+ import { type OverrideClassName } from "../../../../types/OverrideClassName";
2
4
  export type MenuPopupProps = {
5
+ children: React.ReactNode;
6
+ floatingOptions?: Partial<UseFloatingOptions>;
3
7
  isLoading?: boolean;
4
8
  loadingSkeleton?: React.ReactNode;
5
- children: React.ReactNode;
6
- };
9
+ } & OverrideClassName<HTMLAttributes<HTMLDivElement>>;
7
10
  export declare const MenuPopup: {
8
- ({ isLoading, loadingSkeleton, children, }: MenuPopupProps): JSX.Element;
11
+ ({ children, floatingOptions, classNameOverride, isLoading, loadingSkeleton, ...restProps }: MenuPopupProps): JSX.Element;
9
12
  displayName: string;
10
13
  };
@@ -12,10 +12,14 @@ export type LikertScaleProps = {
12
12
  'colorSchema'?: ColorSchema | 'classical';
13
13
  'validationMessage'?: string;
14
14
  'status'?: 'default' | 'error';
15
+ /**
16
+ * Sets aria-required value on radiogroup for assistive technologies. Validation must still be handled.
17
+ */
18
+ 'isRequired'?: boolean;
15
19
  'onSelect': (value: ScaleItem | null) => void;
16
20
  };
17
21
  /**
18
22
  * {@link https://cultureamp.atlassian.net/wiki/spaces/DesignSystem/pages/3082060201/Likert+Scale Guidance} |
19
23
  * {@link https://cultureamp.design/?path=/docs/components-likertscalelegacy--docs Storybook}
20
24
  */
21
- export declare const LikertScaleLegacy: ({ scale, selectedItem, reversed, colorSchema, "data-testid": dataTestId, onSelect, validationMessage, status, labelId, }: LikertScaleProps) => JSX.Element;
25
+ export declare const LikertScaleLegacy: ({ scale, selectedItem, reversed, colorSchema, "data-testid": dataTestId, onSelect, validationMessage, status, labelId, isRequired, }: LikertScaleProps) => JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "0.0.0-canary-test-select-aria-props-update-20250616233358",
3
+ "version": "0.0.0-canary-test-fms-popover-api-update-20250624021814",
4
4
  "description": "Kaizen component library",
5
5
  "author": "Geoffrey Chong <geoff.chong@cultureamp.com>",
6
6
  "homepage": "https://cultureamp.design",
@@ -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
  })
@@ -58,9 +58,10 @@ export const FilterMultiSelect = ({
58
58
  onSearchInputChange,
59
59
  triggerRef,
60
60
  className,
61
+ floatingOptions,
61
62
  }: FilterMultiSelectProps): JSX.Element => {
62
63
  const menuTriggerProps = { isOpen, defaultOpen, onOpenChange, triggerRef }
63
- const menuPopupProps = { isLoading, loadingSkeleton }
64
+ const menuPopupProps = { isLoading, loadingSkeleton, floatingOptions }
64
65
  const disabledKeys: Selection = new Set(
65
66
  items?.filter((item) => item.isDisabled === true).map((disabledItem) => disabledItem.value),
66
67
  )
@@ -79,7 +80,7 @@ export const FilterMultiSelect = ({
79
80
  <MenuTriggerProvider {...menuTriggerProps}>
80
81
  <div className={className}>
81
82
  <MenuTriggerConsumer>{trigger}</MenuTriggerConsumer>
82
- <MenuPopup {...menuPopupProps}>
83
+ <MenuPopup aria-label={label} {...menuPopupProps}>
83
84
  <SelectionProvider {...selectionProps}>
84
85
  <SelectionConsumer>{children}</SelectionConsumer>
85
86
  </SelectionProvider>
@@ -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 floatingOptions
34
+
35
+ You can also use the `floatingOptions` to leverage any of `Floating UI's` [configurable options](https://floating-ui.com/docs/usefloating). While in most cases the default behavior should satisfy most scenarios, the `middleware` for `autoplace` and `size` can be useful when there is limited screen space available.
36
+
37
+ The following example showcases how to use the [autoPlacement](https://floating-ui.com/docs/autoPlacement) and [size](https://floating-ui.com/docs/size) and spread the options in with the default values.
38
+
39
+ <Canvas of={FilterMultiSelectStories.WithFloatingOptions} />
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
+ import { autoPlacement, offset, size } from '@floating-ui/react-dom'
2
3
  import type { Selection } from '@react-types/shared'
3
4
  import type { Meta, StoryObj } from '@storybook/react'
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
 
@@ -308,3 +309,90 @@ export const WithSectionNotification: Story = {
308
309
  chromatic: { disable: false },
309
310
  },
310
311
  }
312
+
313
+ export const WithManyOptions: Story = {
314
+ ...FilterMultiSelectTemplate,
315
+ name: 'With many options',
316
+ args: {
317
+ items: mockManyItems,
318
+ },
319
+ }
320
+
321
+ const floatingOptionsSourceCode = `
322
+ import { autoPlacement, size, offset } from '@floating-ui/react-dom'
323
+
324
+ // ...source code
325
+
326
+ <FilterMultiSelect
327
+ {...args}
328
+ floatingOptions={{
329
+ ...{
330
+ middleware: [
331
+ size({
332
+ apply({ availableHeight, elements }) {
333
+ Object.assign(elements.floating.style, {
334
+ maxHeight: Math.max(250, Math.min(availableHeight - 12, 500)) + "px",
335
+ })
336
+ },
337
+ }),
338
+ autoPlacement({
339
+ allowedPlacements: ['bottom-start', 'top-start'],
340
+ }),
341
+ offset(6),
342
+ ],
343
+ },
344
+ }}
345
+ />
346
+ `
347
+
348
+ export const WithFloatingOptions: Story = {
349
+ ...FilterMultiSelectTemplate,
350
+ name: 'With floatingOptions',
351
+ args: {
352
+ floatingOptions: {
353
+ middleware: [
354
+ size({
355
+ apply({ availableHeight, elements }) {
356
+ Object.assign(elements.floating.style, {
357
+ maxHeight: Math.max(250, Math.min(availableHeight - 12, 500)) + 'px',
358
+ })
359
+ },
360
+ }),
361
+ autoPlacement({
362
+ allowedPlacements: ['bottom-start', 'top-start'],
363
+ }),
364
+ offset(6),
365
+ ],
366
+ },
367
+ },
368
+ parameters: {
369
+ docs: { source: { code: floatingOptionsSourceCode } },
370
+ },
371
+ }
372
+
373
+ export const AboveIfAvailable: Story = {
374
+ ...FilterMultiSelectTemplate,
375
+ ...WithFloatingOptions,
376
+ name: 'With limited viewport and autoplacement above',
377
+ parameters: {
378
+ viewport: {
379
+ viewports: {
380
+ LimitedViewportAutoPlace: {
381
+ name: 'Limited vertical space',
382
+ styles: {
383
+ width: '1024px',
384
+ height: '500px',
385
+ },
386
+ },
387
+ },
388
+ defaultViewport: 'LimitedViewportAutoPlace',
389
+ },
390
+ },
391
+ decorators: [
392
+ (Story) => (
393
+ <div className="mt-[350px]">
394
+ <Story />
395
+ </div>
396
+ ),
397
+ ],
398
+ }
@@ -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
+ ]
@@ -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
- const trigger = screen.getByRole('button', {
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.scss'
6
+ import styles from './ListBox.module.css'
7
7
 
8
8
  export type ListBoxItems = {
9
9
  selectedItems: MultiSelectItem[]
@@ -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-6) 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
+ }
@@ -1,58 +1,78 @@
1
- import React from 'react'
2
- import { FocusScope } from '@react-aria/focus'
3
- import { DismissButton, useOverlay } from '@react-aria/overlays'
1
+ import React, { useEffect, useState, type HTMLAttributes } from 'react'
2
+ import { autoUpdate, offset, useFloating, type UseFloatingOptions } from '@floating-ui/react-dom'
3
+ import classnames from 'classnames'
4
+ import { FocusOn } from 'react-focus-on'
5
+ import { type OverrideClassName } from '~components/types/OverrideClassName'
4
6
  import { useMenuTriggerContext } from '../../context'
5
- import styles from './MenuPopup.module.scss'
7
+ import styles from './MenuPopup.module.css'
6
8
 
7
9
  export type MenuPopupProps = {
10
+ children: React.ReactNode
11
+ floatingOptions?: Partial<UseFloatingOptions>
8
12
  isLoading?: boolean
9
13
  loadingSkeleton?: React.ReactNode
10
- children: React.ReactNode
11
- }
14
+ } & OverrideClassName<HTMLAttributes<HTMLDivElement>>
12
15
 
13
16
  export const MenuPopup = ({
17
+ children,
18
+ floatingOptions,
19
+ classNameOverride,
14
20
  isLoading,
15
21
  loadingSkeleton,
16
- children,
22
+ ...restProps
17
23
  }: MenuPopupProps): JSX.Element => {
18
- const { menuTriggerState } = useMenuTriggerContext()
19
-
20
- const onClose = (): void => menuTriggerState.close()
21
-
22
- // Handle events that should cause the menu to close,
23
- // e.g. blur, clicking outside, or pressing the escape key.
24
- const overlayRef = React.createRef<HTMLDivElement>()
25
- const { overlayProps } = useOverlay(
26
- {
27
- onClose,
28
- isOpen: menuTriggerState.isOpen,
29
- isDismissable: true,
24
+ const [floatingElement, setFloatingElement] = useState<HTMLDivElement | null>(null)
25
+ const { menuTriggerState, buttonRef } = useMenuTriggerContext()
26
+
27
+ const referenceElement = buttonRef.current
28
+
29
+ const { floatingStyles, update } = useFloating({
30
+ placement: 'bottom-start',
31
+ elements: {
32
+ reference: referenceElement,
33
+ floating: floatingElement,
30
34
  },
31
- overlayRef,
32
- )
35
+ strategy: 'absolute',
36
+ middleware: [offset(6)],
37
+ whileElementsMounted: autoUpdate,
38
+ ...floatingOptions,
39
+ })
40
+
41
+ const handleReturnFocus = (): void => {
42
+ requestAnimationFrame(() => {
43
+ buttonRef.current?.focus()
44
+ })
45
+ }
46
+
47
+ useEffect(() => {
48
+ if (floatingElement && referenceElement) {
49
+ floatingElement.showPopover?.()
50
+ update()
51
+ }
52
+ }, [floatingElement, referenceElement, update])
33
53
 
34
- // Wrap in <FocusScope> so that focus is restored back to the trigger when the menu is closed
35
- // and auto focus on the first focusable item after loading. (disable eslint no-autofocus error for it)
36
- // In addition, add hidden <DismissButton> components at the start and end of the list
37
- // to allow screen reader users to dismiss the popup easily.
38
54
  return menuTriggerState.isOpen ? (
39
- <div {...overlayProps} ref={overlayRef} className={styles.menuPopup}>
40
- {isLoading && loadingSkeleton ? (
41
- <>
42
- <DismissButton onDismiss={onClose} />
43
- {loadingSkeleton}
44
- <DismissButton onDismiss={onClose} />
45
- </>
46
- ) : (
47
- // eslint-disable-next-line jsx-a11y/no-autofocus
48
- <FocusScope contain autoFocus restoreFocus>
49
- <DismissButton onDismiss={onClose} />
50
-
51
- {children}
52
- <DismissButton onDismiss={onClose} />
53
- </FocusScope>
54
- )}
55
- </div>
55
+ <FocusOn
56
+ enabled={menuTriggerState.isOpen}
57
+ scrollLock={false}
58
+ returnFocus={false}
59
+ onClickOutside={menuTriggerState.close}
60
+ onEscapeKey={menuTriggerState.close}
61
+ onDeactivation={handleReturnFocus}
62
+ >
63
+ <div
64
+ ref={setFloatingElement}
65
+ style={floatingStyles}
66
+ className={classnames(styles.menuPopup, classNameOverride)}
67
+ role="dialog"
68
+ aria-modal="true"
69
+ // @ts-expect-error: popover is valid in supported browsers
70
+ popover="manual"
71
+ {...restProps}
72
+ >
73
+ {isLoading && loadingSkeleton ? loadingSkeleton : children}
74
+ </div>
75
+ </FocusOn>
56
76
  ) : (
57
77
  <></>
58
78
  )
@@ -36,6 +36,7 @@ const LikertScaleLegacyWrapper = (props: Partial<LikertScaleProps>): JSX.Element
36
36
  labelId="test__likert-scale"
37
37
  selectedItem={null}
38
38
  onSelect={(): void => undefined}
39
+ isRequired
39
40
  {...props}
40
41
  />
41
42
  )
@@ -25,6 +25,10 @@ export type LikertScaleProps = {
25
25
  'colorSchema'?: ColorSchema | 'classical'
26
26
  'validationMessage'?: string
27
27
  'status'?: 'default' | 'error'
28
+ /**
29
+ * Sets aria-required value on radiogroup for assistive technologies. Validation must still be handled.
30
+ */
31
+ 'isRequired'?: boolean
28
32
  'onSelect': (value: ScaleItem | null) => void
29
33
  }
30
34
 
@@ -46,6 +50,7 @@ export const LikertScaleLegacy = ({
46
50
  validationMessage,
47
51
  status,
48
52
  labelId,
53
+ isRequired,
49
54
  }: LikertScaleProps): JSX.Element => {
50
55
  const [hoveredItem, setHoveredItem] = useState<ScaleItem | null>(null)
51
56
  const itemRefs: ItemRefs = scale.map((s) => ({
@@ -104,11 +109,12 @@ export const LikertScaleLegacy = ({
104
109
  reversed && [styles.reversed],
105
110
  hoveredItem !== null && styles.hovered,
106
111
  )}
107
- aria-labelledby={labelId}
112
+ aria-labelledby={isRequired ? `${labelId}` : labelId}
108
113
  role="radiogroup"
109
114
  tabIndex={-1}
110
115
  aria-describedby={validationMessageId}
111
116
  data-testid={dataTestId}
117
+ aria-required={isRequired}
112
118
  >
113
119
  <div className={styles.legend} data-testid={dataTestId && `${dataTestId}-legend`}>
114
120
  <Text variant="small" color={reversed ? 'white' : 'dark'}>
@@ -21,3 +21,11 @@ Likert scale radio buttons let people select one option in a Likert scale rangin
21
21
 
22
22
  <Canvas of={LikertScaleLegacyStories.Playground} />
23
23
  <Controls of={LikertScaleLegacyStories.Playground} />
24
+
25
+ ## API
26
+
27
+ ### isRequired
28
+
29
+ Sets aria-required value on radiogroup for assistive technologies. An accessible label must be provided and validation must still be handled within implementations.
30
+
31
+ <Canvas of={LikertScaleLegacyStories.IsRequired} />
@@ -1,5 +1,7 @@
1
1
  import React, { useState } from 'react'
2
2
  import { type Meta, type StoryObj } from '@storybook/react'
3
+ import { expect, within } from '@storybook/test'
4
+ import { VisuallyHidden } from '~components/VisuallyHidden'
3
5
  import { LikertScaleLegacy } from '../index'
4
6
  import { type Scale, type ScaleItem } from '../types'
5
7
 
@@ -57,7 +59,7 @@ export const Playground: Story = {
57
59
  code: `
58
60
  const SatisfactionExample = () => {
59
61
  const [selectedItem, setSelectedItem] = useState<ScaleItem | null>(null)
60
-
62
+
61
63
  return (
62
64
  <LikertScaleLegacy
63
65
  scale={[
@@ -82,3 +84,30 @@ export const Playground: Story = {
82
84
  },
83
85
  },
84
86
  }
87
+
88
+ export const IsRequired: Story = {
89
+ render: (args) => {
90
+ const [selectedItem, setSelectedItem] = useState<ScaleItem | null>(null)
91
+ const labelId = React.useId()
92
+ return (
93
+ <div>
94
+ <VisuallyHidden id={labelId}>Likert scale label</VisuallyHidden>
95
+ <LikertScaleLegacy
96
+ {...args}
97
+ labelId={labelId}
98
+ selectedItem={selectedItem}
99
+ onSelect={setSelectedItem}
100
+ />
101
+ </div>
102
+ )
103
+ },
104
+ args: {
105
+ isRequired: true,
106
+ },
107
+ play: async ({ canvasElement }) => {
108
+ const canvas = within(canvasElement.parentElement!)
109
+ const likertScale = canvas.getByRole('radiogroup', { name: 'Likert scale label' })
110
+
111
+ expect(likertScale).toHaveAttribute('aria-required', 'true')
112
+ },
113
+ }
@@ -0,0 +1,91 @@
1
+ import { Meta } from '@storybook/blocks'
2
+
3
+ <Meta title="Components/Menu/Migration guide" />
4
+
5
+ # Menu migration guide
6
+
7
+ ## Audience
8
+
9
+ This guide is relevant for Kaizen All-In-One (KAIO) v1 consumers.
10
+
11
+ ## Purpose
12
+
13
+ This guide provides instructions for migrating menu usage from the `deprecated` (`@kaizen/components`) `Menu` component to the `next` (`@kaizen/components/next`) `Menu` component.
14
+
15
+ This migration is a prerequisite for [migrating to KAIO v2](/docs/releases-upcoming-major-releases--docs).
16
+
17
+ ## Key API changes
18
+
19
+ `next/Menu` separates its functionality into the following components:
20
+
21
+ - `MenuTrigger` wraps the `MenuPopover` component and its trigger element.
22
+ - `MenuPopover` contains a `Menu` component, and controls the popover placement and open and close interactions.
23
+ - `Menu` contains one or more `MenuItem` and `MenuSection` components.
24
+ - `MenuSection` enables menu items to be grouped into sections.
25
+ - `MenuHeader` provides a section's header content.
26
+ - `MenuItem` provides a menu item's content, and handles item selection.
27
+
28
+ Other notable changes:
29
+
30
+ - `Menu.align` prop becomes MenuPopover.placement, and values are mapped as follows:
31
+ - `left` becomes `start`
32
+ - `right` becomes `end`
33
+ - `Menu.autoHide` prop is retired
34
+ - `Menu.button` prop becomes `MenuTrigger.children`
35
+ - The trigger element must be a `next/Button`
36
+ - `Menu.dropdownWidth` prop is retired
37
+ - `Menu.portalSelector` prop is retired
38
+ - Where needed, [PortalProvider](https://react-spectrum.adobe.com/react-aria/PortalProvider.html) can be used to control portalling behaviour
39
+ - `MenuItem.destructive` prop is retired
40
+ - This change aligns with a broader move towards more judicious use of colour
41
+ - `MenuItem.disabled` prop becomes `MenuItem.isDisabled`
42
+ - `MenuItem.label` prop becomes `MenuItem.children`
43
+ - `MenuItem.onClick` prop becomes `MenuItem.onAction`
44
+ - React Aria's `Menu` does not expose native click events, e.g. `MenuItem.onAction` cannot call `e.preventDefault()`
45
+ - See React Aria [Menu documentation](https://react-spectrum.adobe.com/react-aria/Menu.html) for more details on working with `Menu` and `MenuItem` events
46
+ - `MenuList.heading` prop becomes `MenuHeader` in a `MenuSection`
47
+
48
+ ## Migration example
49
+
50
+ ### Before
51
+
52
+ ```tsx
53
+ <Menu button={<Button>Trigger</Button>}>
54
+ <MenuList>
55
+ <MenuList heading={<MenuHeading>Section One</MenuHeading>}>
56
+ <MenuItem onClick={() => alert('1')} label="Item 1" />
57
+ <MenuItem onClick={() => alert('2')} label="Item 2" />
58
+ </MenuList>
59
+ <MenuList heading={<MenuHeading>Section Two</MenuHeading>}>
60
+ <MenuItem onClick={() => alert('3')} label="Item 3" />
61
+ <MenuItem onClick={() => alert('4')} label="Item 4" />
62
+ </MenuList>
63
+ </MenuList>
64
+ </Menu>
65
+ ```
66
+
67
+ ### After
68
+
69
+ ```tsx
70
+ <MenuTrigger>
71
+ <Button>Trigger</Button>
72
+ <MenuPopover>
73
+ <Menu>
74
+ <MenuSection>
75
+ <MenuHeader>Section One</MenuHeader>
76
+ <MenuItem onAction={() => alert('1')}>Item 1</MenuItem>
77
+ <MenuItem onAction={() => alert('2')}>Item 2</MenuItem>
78
+ </MenuSection>
79
+ <MenuSection>
80
+ <MenuHeader>Section Two</MenuHeader>
81
+ <MenuItem onAction={() => alert('3')}>Item 3</MenuItem>
82
+ <MenuItem onAction={() => alert('4')}>Item 4</MenuItem>
83
+ </MenuSection>
84
+ </Menu>
85
+ </MenuPopover>
86
+ </MenuTrigger>
87
+ ```
88
+
89
+ ## More information
90
+
91
+ More information about `next/Menu` can be found at [API Specification](/docs/components-menu-menu-next-api-specification--docs) and [Usage Guidelines](/docs/components-menu-menu-next-usage-guidelines--docs).
@@ -98,7 +98,7 @@ When using the `isRequired` property you can also specify the `validationBehavio
98
98
 
99
99
  <Canvas of={SelectStories.SelectNativeValidationBehavior} />
100
100
 
101
- While both use `aria-required` to announce whether the field has to have value to assistive technologies, the `native` will option will prevent form submissions if the `selectedKey` is `undefined`.
101
+ While both use `aria-required` to announce whether the field has to have a value to assistive technologies, the `native` will option will prevent form submissions if the `selectedKey` is `undefined`.
102
102
 
103
103
  ### Full width
104
104
 
@@ -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,7 +0,0 @@
1
- var styles = {
2
- "listBox": "ListBox-module_listBox__q95MO",
3
- "overflown": "ListBox-module_overflown__wChQA",
4
- "hidden": "ListBox-module_hidden__eYdXv",
5
- "noResultsWrapper": "ListBox-module_noResultsWrapper__WcLRm"
6
- };
7
- export { styles as default };
@@ -1,4 +0,0 @@
1
- var styles = {
2
- "menuPopup": "MenuPopup-module_menuPopup__UVgnP"
3
- };
4
- export { styles as default };
@@ -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
- }