@kaizen/components 1.78.1 → 1.78.3

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 (58) hide show
  1. package/dist/cjs/src/Filter/FilterMultiSelect/FilterMultiSelect.cjs +11 -4
  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 +1 -1
  5. package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/{MenuPopup.module.scss.cjs → MenuPopup.module.css.cjs} +1 -1
  6. package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.cjs +91 -0
  7. package/dist/cjs/src/LikertScaleLegacy/LikertScaleLegacy.cjs +5 -3
  8. package/dist/cjs/src/Menu/subcomponents/StatelessMenu/StatelessMenu.cjs +0 -1
  9. package/dist/cjs/src/__next__/Select/Select.cjs +23 -15
  10. package/dist/esm/src/Filter/FilterMultiSelect/FilterMultiSelect.mjs +12 -5
  11. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.mjs +1 -1
  12. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css.mjs +7 -0
  13. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.mjs +1 -1
  14. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css.mjs +4 -0
  15. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.mjs +85 -0
  16. package/dist/esm/src/LikertScaleLegacy/LikertScaleLegacy.mjs +5 -3
  17. package/dist/esm/src/Menu/subcomponents/StatelessMenu/StatelessMenu.mjs +0 -1
  18. package/dist/esm/src/__next__/Select/Select.mjs +23 -15
  19. package/dist/styles.css +8717 -8712
  20. package/dist/types/Filter/FilterMultiSelect/FilterMultiSelect.d.ts +7 -1
  21. package/dist/types/Filter/FilterMultiSelect/_docs/MockData.d.ts +1 -0
  22. package/dist/types/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.d.ts +22 -0
  23. package/dist/types/Filter/FilterMultiSelect/subcomponents/MenuPopup/index.d.ts +1 -0
  24. package/dist/types/LikertScaleLegacy/LikertScaleLegacy.d.ts +5 -1
  25. package/dist/types/Menu/subcomponents/StatelessMenu/StatelessMenu.d.ts +0 -1
  26. package/dist/types/__next__/Select/Select.d.ts +1 -1
  27. package/package.json +1 -1
  28. package/src/Filter/FilterBar/subcomponents/FilterBarMultiSelect/FilterBarMultiSelect.spec.tsx +1 -0
  29. package/src/Filter/FilterMultiSelect/FilterMultiSelect.tsx +10 -4
  30. package/src/Filter/FilterMultiSelect/_docs/FilterMultiSelect.mdx +9 -1
  31. package/src/Filter/FilterMultiSelect/_docs/FilterMultiSelect.stories.tsx +79 -2
  32. package/src/Filter/FilterMultiSelect/_docs/MockData.ts +39 -0
  33. package/src/Filter/FilterMultiSelect/context/MenuTriggerProvider/MenuTriggerProvider.spec.tsx +2 -18
  34. package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.css +20 -0
  35. package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.tsx +1 -1
  36. package/src/Filter/FilterMultiSelect/subcomponents/ListBoxSection/ListBoxSection.module.scss +1 -0
  37. package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.css +20 -0
  38. package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.tsx +1 -1
  39. package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/ResponsiveMenuPopup.tsx +115 -0
  40. package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/index.ts +1 -0
  41. package/src/LikertScaleLegacy/LikertScaleLegacy.spec.tsx +1 -0
  42. package/src/LikertScaleLegacy/LikertScaleLegacy.tsx +7 -1
  43. package/src/LikertScaleLegacy/_docs/LikertScaleLegacy.mdx +8 -0
  44. package/src/LikertScaleLegacy/_docs/LikertScaleLegacy.stories.tsx +30 -1
  45. package/src/Menu/subcomponents/StatelessMenu/StatelessMenu.tsx +0 -2
  46. package/src/__next__/Select/Select.tsx +5 -0
  47. package/src/__next__/Select/_docs/Select.mdx +8 -0
  48. package/src/__next__/Select/_docs/Select.stories.tsx +93 -0
  49. package/src/__next__/Tooltip/_docs/ApiSpecification.mdx +2 -2
  50. package/src/__next__/Tooltip/_docs/Tooltip.docs.stories.tsx +15 -30
  51. package/src/__next__/Tooltip/_docs/Tooltip.mdx +1 -1
  52. package/src/__next__/Tooltip/_docs/Tooltip.spec.stories.tsx +21 -58
  53. package/src/__next__/Tooltip/_docs/Tooltip.stories.tsx +2 -2
  54. package/dist/cjs/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss.cjs +0 -9
  55. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss.mjs +0 -7
  56. package/dist/esm/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.scss.mjs +0 -4
  57. package/src/Filter/FilterMultiSelect/subcomponents/ListBox/ListBox.module.scss +0 -23
  58. package/src/Filter/FilterMultiSelect/subcomponents/MenuPopup/MenuPopup.module.scss +0 -22
@@ -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 {};
@@ -8,3 +8,4 @@ export declare const locationDemographicValues: {
8
8
  demographicValueId: string;
9
9
  label: string;
10
10
  }[];
11
+ export declare const mockManyItems: ItemType[];
@@ -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
+ };
@@ -1 +1,2 @@
1
1
  export * from './MenuPopup';
2
+ export * from './ResponsiveMenuPopup';
@@ -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;
@@ -41,7 +41,6 @@ export type StatelessMenuProps = {
41
41
  'renderButton': (args: {
42
42
  'onClick': (e: any) => void;
43
43
  'onMouseDown': (e: any) => void;
44
- 'aria-haspopup': boolean;
45
44
  'aria-expanded': boolean;
46
45
  }) => React.ReactElement;
47
46
  'onClick'?: (event: SyntheticEvent) => void;
@@ -51,7 +51,7 @@ export type SelectProps<Option extends SelectOption = SelectOption> = {
51
51
  * {@link https://cultureamp.design/?path=/docs/components-select--docs Storybook}
52
52
  */
53
53
  export declare const Select: {
54
- <Option extends SelectOption = SelectOption>({ label, items, id: propsId, trigger, children, status, validationMessage, isReversed, isFullWidth, disabledValues, classNameOverride, selectedKey, description, placeholder, isDisabled, portalContainerId, ...restProps }: SelectProps<Option>): JSX.Element;
54
+ <Option extends SelectOption = SelectOption>({ label, items, id: propsId, trigger, children, status, validationMessage, isReversed, isRequired, isFullWidth, disabledValues, classNameOverride, selectedKey, description, placeholder, isDisabled, portalContainerId, onSelectionChange, ...restProps }: SelectProps<Option>): JSX.Element;
55
55
  displayName: string;
56
56
  Section: {
57
57
  <Option extends SelectOption = SelectOption>({ section, }: import("./subcomponents").ListBoxSectionProps<Option>): JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kaizen/components",
3
- "version": "1.78.1",
3
+ "version": "1.78.3",
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
  })
@@ -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
- <MenuPopup {...menuPopupProps}>
87
+ <MenuComponent aria-label={label} {...menuPopupProps}>
83
88
  <SelectionProvider {...selectionProps}>
84
89
  <SelectionConsumer>{children}</SelectionConsumer>
85
90
  </SelectionProvider>
86
- </MenuPopup>
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: 'code' },
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
+ ]
@@ -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,20 @@
1
+ .listBox {
2
+ list-style: none;
3
+ padding: var(--spacing-12);
4
+ margin: 0 var(--spacing-12) 0 0;
5
+ display: grid;
6
+ max-height: 22rem;
7
+ overflow-y: auto;
8
+ }
9
+
10
+ .overflown {
11
+ padding-right: var(--spacing-12);
12
+ }
13
+
14
+ .hidden {
15
+ display: none;
16
+ }
17
+
18
+ .noResultsWrapper {
19
+ list-style: none;
20
+ }
@@ -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[]
@@ -9,6 +9,7 @@
9
9
  }
10
10
 
11
11
  .listBoxSectionHeader {
12
+ position: relative; // this is needed to ensure the VisuallyHidden element doesn't impact the scroll height of the list
12
13
  font-family: $typography-heading-6-font-family;
13
14
  font-size: $typography-heading-6-font-size;
14
15
  font-weight: $typography-heading-6-font-weight;
@@ -0,0 +1,20 @@
1
+ .menuPopup {
2
+ /* from $ca-z-index-dropdown */
3
+ z-index: 1000;
4
+ box-sizing: border-box;
5
+ background: var(--color-white);
6
+ color: var(--color-purple-800);
7
+ border-radius: var(--border-solid-border-radius);
8
+ box-shadow: var(--shadow-large-box-shadow);
9
+ padding: var(--spacing-12) 0;
10
+ margin-top: var(--spacing-6);
11
+ text-align: start;
12
+ width: var(--menu-container-width, 294px);
13
+ max-height: var(--menu-container-height, 500px);
14
+ }
15
+
16
+ .menuPopup[popover]:popover-open {
17
+ z-index: unset;
18
+ margin: 0;
19
+ inset: unset;
20
+ }
@@ -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.scss'
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 +1,2 @@
1
1
  export * from './MenuPopup'
2
+ export * from './ResponsiveMenuPopup'
@@ -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} />