@westpac/ui 0.57.4 → 0.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (110) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/dist/component-type.json +1 -1
  3. package/dist/components/compacta/compacta.component.d.ts +1 -1
  4. package/dist/components/compacta/compacta.component.js +21 -1
  5. package/dist/components/compacta/compacta.types.d.ts +12 -0
  6. package/dist/components/index.d.ts +1 -0
  7. package/dist/components/index.js +1 -0
  8. package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.component.d.ts +2 -0
  9. package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.component.js +20 -0
  10. package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.styles.d.ts +37 -0
  11. package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.styles.js +8 -0
  12. package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.types.d.ts +5 -0
  13. package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.types.js +1 -0
  14. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-list-box-section/multi-select-list-box-section.component.d.ts +2 -0
  15. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-list-box-section/multi-select-list-box-section.component.js +31 -0
  16. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-list-box-section/multi-select-list-box-section.styles.d.ts +25 -0
  17. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-list-box-section/multi-select-list-box-section.styles.js +6 -0
  18. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-list-box-section/multi-select-list-box-section.types.d.ts +4 -0
  19. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-list-box-section/multi-select-list-box-section.types.js +1 -0
  20. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.d.ts +2 -0
  21. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.js +62 -0
  22. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.styles.d.ts +82 -0
  23. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.styles.js +32 -0
  24. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.types.d.ts +8 -0
  25. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.types.js +1 -0
  26. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-select-all-option/multi-select-select-all-option.component.d.ts +1 -0
  27. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-select-all-option/multi-select-select-all-option.component.js +93 -0
  28. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-select-all-option/multi-select-select-all-option.styles.d.ts +64 -0
  29. package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-select-all-option/multi-select-select-all-option.styles.js +26 -0
  30. package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.component.d.ts +2 -0
  31. package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.component.js +35 -0
  32. package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.styles.d.ts +43 -0
  33. package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.styles.js +9 -0
  34. package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.types.d.ts +5 -0
  35. package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.types.js +1 -0
  36. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.component.d.ts +2 -0
  37. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.component.js +118 -0
  38. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.d.ts +263 -0
  39. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.js +99 -0
  40. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.types.d.ts +15 -0
  41. package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.types.js +1 -0
  42. package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.component.d.ts +2 -0
  43. package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.component.js +52 -0
  44. package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.styles.d.ts +31 -0
  45. package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.styles.js +7 -0
  46. package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.types.d.ts +6 -0
  47. package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.types.js +1 -0
  48. package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.d.ts +2 -0
  49. package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.js +74 -0
  50. package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.styles.d.ts +31 -0
  51. package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.styles.js +7 -0
  52. package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.types.d.ts +6 -0
  53. package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.types.js +1 -0
  54. package/dist/components/multi-select/index.d.ts +2 -0
  55. package/dist/components/multi-select/index.js +1 -0
  56. package/dist/components/multi-select/multi-select.component.d.ts +7 -0
  57. package/dist/components/multi-select/multi-select.component.js +95 -0
  58. package/dist/components/multi-select/multi-select.styles.d.ts +25 -0
  59. package/dist/components/multi-select/multi-select.styles.js +6 -0
  60. package/dist/components/multi-select/multi-select.types.d.ts +61 -0
  61. package/dist/components/multi-select/multi-select.types.js +1 -0
  62. package/dist/components/multi-select/utils/filter-nodes.d.ts +2 -0
  63. package/dist/components/multi-select/utils/filter-nodes.js +25 -0
  64. package/dist/components/tooltip/components/tooltip-content/tooltip-content.component.d.ts +1 -1
  65. package/dist/components/tooltip/components/tooltip-content/tooltip-content.component.js +4 -2
  66. package/dist/components/tooltip/components/tooltip-content/tooltip-content.styles.d.ts +16 -1
  67. package/dist/components/tooltip/components/tooltip-content/tooltip-content.styles.js +7 -1
  68. package/dist/components/tooltip/components/tooltip-content/tooltip-content.types.d.ts +1 -0
  69. package/dist/components/tooltip/tooltip.component.d.ts +1 -1
  70. package/dist/components/tooltip/tooltip.component.js +4 -3
  71. package/dist/components/tooltip/tooltip.types.d.ts +3 -0
  72. package/dist/css/westpac-ui.css +366 -0
  73. package/dist/css/westpac-ui.min.css +366 -0
  74. package/package.json +4 -1
  75. package/src/components/compacta/compacta.component.tsx +21 -0
  76. package/src/components/compacta/compacta.types.ts +10 -0
  77. package/src/components/index.ts +1 -0
  78. package/src/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.component.tsx +26 -0
  79. package/src/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.styles.ts +9 -0
  80. package/src/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.types.ts +6 -0
  81. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-list-box-section/multi-select-list-box-section.component.tsx +42 -0
  82. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-list-box-section/multi-select-list-box-section.styles.ts +7 -0
  83. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-list-box-section/multi-select-list-box-section.types.ts +5 -0
  84. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.tsx +66 -0
  85. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.styles.ts +33 -0
  86. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.types.ts +7 -0
  87. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-select-all-option/multi-select-select-all-option.component.tsx +105 -0
  88. package/src/components/multi-select/components/multi-select-list-box/components/multi-select-select-all-option/multi-select-select-all-option.styles.ts +22 -0
  89. package/src/components/multi-select/components/multi-select-list-box/multi-select-list-box.component.tsx +42 -0
  90. package/src/components/multi-select/components/multi-select-list-box/multi-select-list-box.styles.ts +10 -0
  91. package/src/components/multi-select/components/multi-select-list-box/multi-select-list-box.types.ts +6 -0
  92. package/src/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.component.tsx +136 -0
  93. package/src/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.ts +74 -0
  94. package/src/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.types.ts +19 -0
  95. package/src/components/multi-select/components/multi-select-popover/multi-select-popover.component.tsx +64 -0
  96. package/src/components/multi-select/components/multi-select-popover/multi-select-popover.styles.ts +8 -0
  97. package/src/components/multi-select/components/multi-select-popover/multi-select-popover.types.ts +8 -0
  98. package/src/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.tsx +81 -0
  99. package/src/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.styles.ts +8 -0
  100. package/src/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.types.ts +7 -0
  101. package/src/components/multi-select/index.ts +2 -0
  102. package/src/components/multi-select/multi-select.component.tsx +116 -0
  103. package/src/components/multi-select/multi-select.styles.ts +7 -0
  104. package/src/components/multi-select/multi-select.types.ts +60 -0
  105. package/src/components/multi-select/utils/filter-nodes.ts +29 -0
  106. package/src/components/tooltip/components/tooltip-content/tooltip-content.component.tsx +2 -2
  107. package/src/components/tooltip/components/tooltip-content/tooltip-content.styles.ts +6 -0
  108. package/src/components/tooltip/components/tooltip-content/tooltip-content.types.ts +1 -0
  109. package/src/components/tooltip/tooltip.component.tsx +7 -3
  110. package/src/components/tooltip/tooltip.types.ts +4 -0
@@ -0,0 +1,9 @@
1
+ import { tv } from 'tailwind-variants';
2
+
3
+ export const styles = tv({
4
+ slots: {
5
+ popover: 'shadow',
6
+ searchInputWrapper: 'border-b border-b-border p-2',
7
+ clearButton: 'mb-0.5 px-2',
8
+ },
9
+ });
@@ -0,0 +1,6 @@
1
+ import { Dispatch, SetStateAction } from 'react';
2
+ import { AriaListBoxOptions } from 'react-aria';
3
+
4
+ export type MultiSelectDropdownProps<T> = {
5
+ setFilterText: Dispatch<SetStateAction<string>>;
6
+ } & AriaListBoxOptions<T>;
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import React, { useContext, useMemo } from 'react';
4
+ import { useListBoxSection } from 'react-aria';
5
+
6
+ import { MultiSelectContext } from '../../../../multi-select.component.js';
7
+ import { MultiSelectOption } from '../multi-select-option/multi-select-option.component.js';
8
+
9
+ import { styles as listBoxStyles } from './multi-select-list-box-section.styles.js';
10
+
11
+ import type { MultiSelectSectionProps } from './multi-select-list-box-section.types.js';
12
+
13
+ export function MultiSelectListBoxSection<T>({ section }: MultiSelectSectionProps<T>) {
14
+ const { listState } = useContext(MultiSelectContext);
15
+ const { itemProps, headingProps, groupProps } = useListBoxSection({
16
+ heading: section.rendered,
17
+ 'aria-label': section['aria-label'],
18
+ });
19
+
20
+ const styles = listBoxStyles();
21
+
22
+ const childNodes = useMemo(() => {
23
+ return listState?.collection?.getChildren ? [...listState.collection.getChildren(section.key)] : [];
24
+ }, [section.key, listState?.collection]);
25
+
26
+ return (
27
+ <>
28
+ <li {...itemProps}>
29
+ {section.rendered && (
30
+ <span {...headingProps} className={styles.span()} tabIndex={-1}>
31
+ {section.rendered}
32
+ </span>
33
+ )}
34
+ <ul {...groupProps}>
35
+ {childNodes.map(node => (
36
+ <MultiSelectOption key={node.key} item={node} />
37
+ ))}
38
+ </ul>
39
+ </li>
40
+ </>
41
+ );
42
+ }
@@ -0,0 +1,7 @@
1
+ import { tv } from 'tailwind-variants';
2
+
3
+ export const styles = tv({
4
+ slots: {
5
+ span: 'typography-body-10 flex h-6 items-center bg-background px-2.5 py-1 align-middle text-hero',
6
+ },
7
+ });
@@ -0,0 +1,5 @@
1
+ import type { Node } from '@react-types/shared';
2
+
3
+ export type MultiSelectSectionProps<T> = {
4
+ section: Node<T>;
5
+ };
@@ -0,0 +1,66 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useContext, useRef, KeyboardEvent } from 'react';
4
+ import { mergeProps, useFocusRing, useOption } from 'react-aria';
5
+
6
+ import { TickIcon } from '../../../../../icon/index.js';
7
+ import { MultiSelectContext } from '../../../../multi-select.component.js';
8
+
9
+ import { styles as optionStyles } from './multi-select-option.styles.js';
10
+ import { MultiSelectOptionProps } from './multi-select-option.types.js';
11
+
12
+ export function MultiSelectOption<T>({ item }: MultiSelectOptionProps<T>) {
13
+ const { listState, selectAllRef, inputRef } = useContext(MultiSelectContext);
14
+ const selectionMode = listState.selectionManager.selectionMode;
15
+ const ref = useRef<HTMLLIElement>(null);
16
+ const { optionProps, isDisabled, isSelected } = useOption({ key: item.key }, listState, ref);
17
+ const { isFocusVisible, focusProps } = useFocusRing();
18
+
19
+ const styles = optionStyles({
20
+ disabled: isDisabled,
21
+ selectionMode,
22
+ isFocusVisible,
23
+ });
24
+
25
+ // Need to manually handle keyboard accessibility due to component complexity
26
+ const handleButtonKeyDown = useCallback(
27
+ (e: KeyboardEvent<HTMLLIElement>) => {
28
+ if (e.key === 'ArrowUp' && item.index === 0) {
29
+ e.preventDefault();
30
+ if (selectAllRef.current) {
31
+ selectAllRef.current?.focus();
32
+ } else {
33
+ inputRef.current?.focus();
34
+ }
35
+ }
36
+ if (e.key === 'Enter' || e.key === ' ') {
37
+ e.preventDefault();
38
+ listState.selectionManager.toggleSelection(item.key);
39
+ }
40
+ },
41
+ // eslint-disable-next-line react-hooks/exhaustive-deps
42
+ [listState.selectionManager],
43
+ );
44
+
45
+ return (
46
+ <li
47
+ {...mergeProps(optionProps, focusProps)}
48
+ ref={ref}
49
+ className={styles.root()}
50
+ onKeyDown={handleButtonKeyDown}
51
+ aria-checked={selectionMode === 'multiple' ? isSelected : undefined}
52
+ aria-selected={selectionMode === 'single' ? isSelected : undefined}
53
+ tabIndex={0}
54
+ >
55
+ <div className={styles.itemContainer()}>
56
+ <div className={styles.flexZero()}>
57
+ <div className={styles.checkbox()}>
58
+ {isSelected && <TickIcon size="small" aria-hidden="true" color="hero" />}
59
+ </div>
60
+ </div>
61
+ <div className={styles.body()}>{item.rendered}</div>
62
+ </div>
63
+ {item.props?.description && <div className={styles.description()}>{item.props.description}</div>}
64
+ </li>
65
+ );
66
+ }
@@ -0,0 +1,33 @@
1
+ import { tv } from 'tailwind-variants';
2
+
3
+ export const styles = tv({
4
+ slots: {
5
+ root: 'flex cursor-pointer flex-col justify-between bg-white p-2 text-sm text-text transition-[background-color] hover:bg-background',
6
+ checkbox: 'size-4',
7
+ body: 'typography-body-9 -mt-0.5 flex flex-1 items-center',
8
+ flexZero: 'flex-none',
9
+ itemContainer: 'flex gap-1',
10
+ description: 'typography-body-10 relative ml-5 text-muted',
11
+ },
12
+ variants: {
13
+ selectionMode: {
14
+ none: {},
15
+ multiple: {
16
+ checkbox: 'flex items-center justify-center rounded border border-hero bg-white',
17
+ },
18
+ single: {
19
+ checkbox: 'flex items-center justify-center',
20
+ },
21
+ },
22
+ isFocusVisible: {
23
+ true: {
24
+ root: 'bg-background !outline-offset-[-2px] focus-outline',
25
+ },
26
+ },
27
+ disabled: {
28
+ true: {
29
+ root: 'cursor-not-allowed text-muted',
30
+ },
31
+ },
32
+ },
33
+ });
@@ -0,0 +1,7 @@
1
+ import type { Node } from '@react-types/shared';
2
+
3
+ export type MultiSelectOptionProps<T> = {
4
+ // item is a react-stately Node for the given item value type T.
5
+ // props.description is optional and may not exist for all item types, so keep it optional.
6
+ item: Omit<Node<T>, 'props'> & { props?: { description?: string } };
7
+ };
@@ -0,0 +1,105 @@
1
+ 'use client';
2
+
3
+ import React, { useCallback, useContext, useMemo, KeyboardEvent } from 'react';
4
+ import { useOption, useFocusRing, mergeProps } from 'react-aria';
5
+
6
+ import { TickIcon } from '../../../../../icon/index.js';
7
+ import { MultiSelectContext } from '../../../../multi-select.component.js';
8
+
9
+ import { styles as selectAllOptionStyles } from './multi-select-select-all-option.styles.js';
10
+
11
+ export function MultiSelectSelectAllOption() {
12
+ const { listState, selectAllRef, listBoxRef, inputRef } = useContext(MultiSelectContext);
13
+
14
+ const allItemsAreSelected = useMemo(
15
+ () => listState.selectionManager.isSelectAll,
16
+ [listState.selectionManager.isSelectAll],
17
+ );
18
+
19
+ const withOneSelectionOrMore = useMemo(
20
+ () => !![...listState.selectionManager.selectedKeys].length,
21
+ [listState.selectionManager.selectedKeys],
22
+ );
23
+
24
+ // Handle selection change
25
+ const handleSelectionChange = useCallback(() => {
26
+ if (!allItemsAreSelected) {
27
+ // This is because selectAll send a string called 'all' when it is called.
28
+ listState.selectionManager.setSelectedKeys(
29
+ // This makes it so that when filtered select all will add to the currently selected options rather than replacing
30
+ new Set([...listState.selectionManager.selectedKeys, ...listState.selectionManager.collection.getKeys()]),
31
+ );
32
+ return;
33
+ }
34
+ return listState.selectionManager.clearSelection();
35
+ }, [allItemsAreSelected, listState.selectionManager]);
36
+
37
+ // Use React Aria's useOption hook
38
+ const { optionProps } = useOption(
39
+ {
40
+ key: 'select-all',
41
+ },
42
+ listState,
43
+ selectAllRef,
44
+ );
45
+
46
+ const { isFocusVisible, focusProps } = useFocusRing();
47
+
48
+ const styles = selectAllOptionStyles({
49
+ selected: withOneSelectionOrMore,
50
+ isFocusVisible,
51
+ });
52
+
53
+ // Need to manually handle keyboard accessibility due to component complexity
54
+ const handleInputKeyDown = useCallback(
55
+ (e: KeyboardEvent<HTMLDivElement>) => {
56
+ if (e.key === 'ArrowDown') {
57
+ e.preventDefault();
58
+ const firstItem = listBoxRef.current?.querySelector('[data-key]') as HTMLElement;
59
+ firstItem?.focus();
60
+ }
61
+ if (e.key === 'ArrowUp') {
62
+ e.preventDefault();
63
+ inputRef.current?.focus();
64
+ }
65
+ if (e.key === 'Enter' || e.key === ' ') {
66
+ e.preventDefault();
67
+ handleSelectionChange();
68
+ }
69
+ },
70
+ [handleSelectionChange, listBoxRef, inputRef],
71
+ );
72
+
73
+ let ariaChecked: 'true' | 'false' | 'mixed';
74
+ if (allItemsAreSelected) {
75
+ ariaChecked = 'true';
76
+ } else if (withOneSelectionOrMore) {
77
+ ariaChecked = 'mixed';
78
+ } else {
79
+ ariaChecked = 'false';
80
+ }
81
+
82
+ return (
83
+ <div
84
+ className={styles.listItem()}
85
+ key="select-all"
86
+ {...mergeProps(optionProps, focusProps)}
87
+ ref={selectAllRef}
88
+ onClick={handleSelectionChange}
89
+ onKeyDown={e => {
90
+ handleInputKeyDown(e);
91
+ }}
92
+ role="option"
93
+ aria-checked={ariaChecked}
94
+ aria-label="Select all options"
95
+ >
96
+ <div className={styles.button()}>
97
+ <div className={styles.checkbox()} role="presentation">
98
+ {allItemsAreSelected && <TickIcon size="small" aria-hidden="true" color="hero" />}
99
+ {!allItemsAreSelected && withOneSelectionOrMore && <div className={styles.indeterminate()} />}
100
+ </div>
101
+ <span className={styles.label()}>Select all</span>
102
+ </div>
103
+ </div>
104
+ );
105
+ }
@@ -0,0 +1,22 @@
1
+ import { tv } from 'tailwind-variants';
2
+
3
+ export const styles = tv({
4
+ slots: {
5
+ listItem: 'border-b border-b-border hover:bg-background',
6
+ button:
7
+ 'flex w-full cursor-pointer items-center gap-1 p-2 focus-visible:bg-background focus-visible:outline-2 focus-visible:!outline-offset-[-2px] focus-visible:focus-outline',
8
+ checkbox: 'flex size-4 items-center justify-center rounded border border-hero bg-white',
9
+ indeterminate: 'block w-3/5 border-t-2 border-t-hero',
10
+ label: 'typography-body-9',
11
+ },
12
+ variants: {
13
+ selected: {
14
+ true: { listItem: 'bg-background' },
15
+ false: { listItem: '' },
16
+ },
17
+ isFocusVisible: {
18
+ true: { listItem: 'bg-background !outline-offset-[-2px] focus-outline' },
19
+ false: {},
20
+ },
21
+ },
22
+ });
@@ -0,0 +1,42 @@
1
+ 'use client';
2
+
3
+ import React, { useContext } from 'react';
4
+ import { useListBox } from 'react-aria';
5
+
6
+ import { MultiSelectContext } from '../../multi-select.component.js';
7
+
8
+ import { MultiSelectListBoxSection } from './components/multi-select-list-box-section/multi-select-list-box-section.component.js';
9
+ import { MultiSelectOption } from './components/multi-select-option/multi-select-option.component.js';
10
+ import { MultiSelectSelectAllOption } from './components/multi-select-select-all-option/multi-select-select-all-option.component.js';
11
+ import { styles as listBoxStyles } from './multi-select-list-box.styles.js';
12
+
13
+ import type { MultiSelectListBoxProps } from './multi-select-list-box.types.js';
14
+
15
+ export function MultiSelectListBox<T extends object = object>({ listBoxRef, ...props }: MultiSelectListBoxProps<T>) {
16
+ const { listState } = useContext(MultiSelectContext);
17
+ const selectionMode = listState.selectionManager.selectionMode;
18
+ const { listBoxProps } = useListBox({ selectionMode, ...props }, listState, listBoxRef);
19
+
20
+ const stateCollection = [...listState.collection];
21
+
22
+ const styles = listBoxStyles();
23
+
24
+ return (
25
+ <div className={styles.container()}>
26
+ {selectionMode === 'multiple' && stateCollection.length > 0 && <MultiSelectSelectAllOption />}
27
+ <ul {...listBoxProps} ref={listBoxRef} className={styles.ul()}>
28
+ {stateCollection.length > 0 ? (
29
+ stateCollection.map(item =>
30
+ item.type === 'section' ? (
31
+ <MultiSelectListBoxSection key={item.key} section={item} />
32
+ ) : (
33
+ <MultiSelectOption key={item.key} item={item} />
34
+ ),
35
+ )
36
+ ) : (
37
+ <p className={styles.noItemsText()}>No results. Try another search.</p>
38
+ )}
39
+ </ul>
40
+ </div>
41
+ );
42
+ }
@@ -0,0 +1,10 @@
1
+ import { tv } from 'tailwind-variants';
2
+
3
+ export const styles = tv({
4
+ slots: {
5
+ container: 'max-h-[432px] overflow-auto',
6
+ checkbox: 'flex size-4 items-center justify-center rounded border border-border',
7
+ ul: 'w-full outline-none',
8
+ noItemsText: 'typography-body-9 px-2 py-3 text-text',
9
+ },
10
+ });
@@ -0,0 +1,6 @@
1
+ import React from 'react';
2
+ import { type AriaListBoxOptions } from 'react-aria';
3
+
4
+ export type MultiSelectListBoxProps<T> = {
5
+ listBoxRef: React.RefObject<HTMLUListElement>;
6
+ } & Omit<AriaListBoxOptions<T>, 'selectionMode'>;
@@ -0,0 +1,136 @@
1
+ import React, { useCallback, useContext, useEffect, useState, Key, KeyboardEvent } from 'react';
2
+ import { mergeProps, useButton, useFocusRing } from 'react-aria';
3
+
4
+ import { Button } from '../../../button/button.component.js';
5
+ import { DropDownIcon, ClearIcon } from '../../../icon/index.js';
6
+ import { Tooltip } from '../../../tooltip/tooltip.component.js';
7
+ import { MultiSelectContext } from '../../multi-select.component.js';
8
+
9
+ import { styles as triggerStyles } from './multi-select-list-box-trigger.styles.js';
10
+ import { MultiSelectListBoxTriggerProps } from './multi-select-list-box-trigger.types.js';
11
+
12
+ export function MultiSelectListBoxTrigger<T>({
13
+ placeholder,
14
+ showSingleSectionTitle,
15
+ selectedKeys,
16
+ triggerProps,
17
+ id,
18
+ }: MultiSelectListBoxTriggerProps<T>) {
19
+ const { size, overlayState, listState, buttonRef, inputRef } = useContext(MultiSelectContext);
20
+ const selectionMode = listState.selectionManager.selectionMode;
21
+ const { buttonProps } = useButton(triggerProps, buttonRef);
22
+ const { focusProps, isFocusVisible } = useFocusRing();
23
+ const [selectedValues, setSelectedValues] = useState<{ key: string; value: string | undefined }[]>([]);
24
+ const [sectionTitle, setSectionTitle] = useState<string | undefined>(undefined);
25
+
26
+ const finalButtonProps = mergeProps(focusProps, buttonProps);
27
+ const styles = triggerStyles({
28
+ size,
29
+ isFocusVisible,
30
+ });
31
+
32
+ const getSectionTitle = useCallback(
33
+ (key?: Key): string | undefined => {
34
+ const parentKey = key ?? '';
35
+ const item = listState.collection.getItem(parentKey as string);
36
+ if (!item) return undefined;
37
+ const title = (item.props as { title?: string }).title;
38
+ return title;
39
+ },
40
+ [listState.collection],
41
+ );
42
+
43
+ const handleTriggerKeyDown = useCallback(
44
+ (e: KeyboardEvent<HTMLButtonElement>) => {
45
+ if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
46
+ e.preventDefault();
47
+ overlayState.open();
48
+ inputRef.current?.focus();
49
+ }
50
+ },
51
+ // eslint-disable-next-line react-hooks/exhaustive-deps
52
+ [overlayState],
53
+ );
54
+
55
+ // Manage selected items state for display
56
+ useEffect(() => {
57
+ if (!selectedKeys || typeof selectedKeys === 'string' || (selectedKeys instanceof Set && selectedKeys.size === 0)) {
58
+ setSelectedValues([]);
59
+ } else {
60
+ const currentMap = new Map(selectedValues.map(item => [item.key, item.value]));
61
+
62
+ // manages the selected values that should be displayed to work with filtering
63
+ const next: { key: string; value: string | undefined }[] = [];
64
+ for (const key of [...selectedKeys] as string[]) {
65
+ if (currentMap.has(key)) {
66
+ next.push({ key, value: currentMap.get(key) });
67
+ } else {
68
+ next.push({ key, value: listState.collection.getItem(key)?.textValue });
69
+ }
70
+ }
71
+
72
+ // Handles displaying the section title
73
+ if (selectionMode === 'single' && showSingleSectionTitle) {
74
+ const firstKey = ([...selectedKeys] as string[])[0];
75
+ const parentKey = listState.collection.getItem(firstKey)?.parentKey;
76
+ const title = parentKey ? getSectionTitle(parentKey) : undefined;
77
+ setSectionTitle(title);
78
+ }
79
+
80
+ setSelectedValues(next);
81
+ }
82
+ // eslint-disable-next-line react-hooks/exhaustive-deps
83
+ }, [selectedKeys]);
84
+
85
+ const valuesString =
86
+ selectionMode === 'single' && selectedValues.length > 0 && showSingleSectionTitle && sectionTitle
87
+ ? `${sectionTitle}: ${selectedValues[0].value}`
88
+ : selectedValues.map(node => node.value || '').join(', ');
89
+
90
+ return (
91
+ <>
92
+ <Tooltip tooltip={valuesString} position="top">
93
+ <div className={styles.buttonContainer()}>
94
+ <button
95
+ className={styles.control()}
96
+ ref={buttonRef}
97
+ {...finalButtonProps}
98
+ onKeyDown={handleTriggerKeyDown}
99
+ type="button"
100
+ role="combobox"
101
+ aria-autocomplete="list"
102
+ tabIndex={undefined}
103
+ aria-haspopup="dialog"
104
+ id={id}
105
+ >
106
+ {/* Selected items */}
107
+ <div className={styles.selection()}>
108
+ <span className={styles.selectionSpan()}>{selectedValues.length > 0 ? valuesString : placeholder}</span>
109
+ </div>
110
+
111
+ {/* dropdown toggle */}
112
+ <div className={styles.button()}>
113
+ <DropDownIcon color="muted" size="medium" aria-hidden="true" />
114
+ </div>
115
+ </button>
116
+ {selectedValues.length > 0 && (
117
+ <Button
118
+ className={styles.clearButton()}
119
+ look="unstyled"
120
+ onClick={() => {
121
+ listState.selectionManager.clearSelection();
122
+ }}
123
+ >
124
+ <ClearIcon className={styles.clearIcon()} size="small" color="muted" />
125
+ </Button>
126
+ )}
127
+ </div>
128
+ </Tooltip>
129
+ {selectedValues.length > 0 && selectionMode === 'multiple' && (
130
+ <p className={styles.hint()}>
131
+ {selectedValues.length} item{selectedValues.length > 1 && 's'} selected
132
+ </p>
133
+ )}
134
+ </>
135
+ );
136
+ }
@@ -0,0 +1,74 @@
1
+ import { tv } from 'tailwind-variants';
2
+
3
+ // eslint-disable-next-line tailwindcss/enforces-negative-arbitrary-values
4
+ export const styles = tv(
5
+ {
6
+ slots: {
7
+ buttonContainer: 'relative w-full',
8
+ control: 'form-control relative box-border inline-flex w-full flex-row overflow-hidden rounded',
9
+ selection: 'flex flex-1 items-center overflow-hidden whitespace-nowrap pr-4.5 text-left',
10
+ selectionSpan: 'w-full overflow-hidden text-ellipsis',
11
+ hint: 'typography-body-10 text-muted',
12
+ button: 'flex cursor-default items-center justify-center rounded-r border-l border-l-borderDark bg-white',
13
+ clearButton: 'absolute inset-y-0 flex !h-auto items-center justify-center',
14
+ clearIcon: '-mt-0.5',
15
+ },
16
+ variants: {
17
+ size: {
18
+ small: {
19
+ control:
20
+ 'form-control-small min-h-5 group-[.input-group-inset-after]:pr-6 group-[.input-group-inset-before]:pl-6',
21
+ button: '-mb-[0.25rem] -mr-1.5 -mt-0.5 px-0.5',
22
+ clearButton: 'right-6.5',
23
+ },
24
+ medium: {
25
+ control:
26
+ 'form-control-medium min-h-6 group-[.input-group-inset-after]:pr-7 group-[.input-group-inset-before]:pl-7',
27
+ button: '-my-[0.3125rem] -mr-2 px-1.5',
28
+ clearButton: 'right-8.5',
29
+ },
30
+ large: {
31
+ control:
32
+ 'form-control-large min-h-7 group-[.input-group-inset-after]:pr-8 group-[.input-group-inset-before]:pl-8',
33
+ button: '-my-[0.5rem] -mr-2.5 px-1.5',
34
+ clearButton: 'right-8.5',
35
+ },
36
+ xlarge: {
37
+ control:
38
+ 'form-control-xlarge min-h-8 group-[.input-group-inset-after]:pr-9 group-[.input-group-inset-before]:pl-9',
39
+ button: '-mb-[0.625rem] -mr-3 -mt-1.5 px-2',
40
+ clearButton: 'right-9.5',
41
+ },
42
+ },
43
+ invalid: {
44
+ true: {
45
+ control: 'border-danger',
46
+ },
47
+ false: {
48
+ control: 'border-borderDark',
49
+ },
50
+ },
51
+ isFocusVisible: {
52
+ true: {
53
+ control: 'focus-outline',
54
+ },
55
+ },
56
+ width: {
57
+ full: { control: 'w-full' },
58
+ 1: { control: 'box-content w-[1.81ex]' },
59
+ 2: { control: 'box-content w-[3.62ex]' },
60
+ 3: { control: 'box-content w-[5.43ex]' },
61
+ 4: { control: 'box-content w-[7.24ex]' },
62
+ 5: { control: 'box-content w-[9.05ex]' },
63
+ 6: { control: 'box-content w-[10.86ex]' },
64
+ 7: { control: 'box-content w-[12.67ex]' },
65
+ 8: { control: 'box-content w-[14.48ex]' },
66
+ 9: { control: 'box-content w-[16.29ex]' },
67
+ 10: { control: 'box-content w-[18.1ex]' },
68
+ 20: { control: 'box-content w-[36.2ex]' },
69
+ 30: { control: 'box-content w-[54.3ex]' },
70
+ },
71
+ },
72
+ },
73
+ { responsiveVariants: ['xsl', 'sm', 'md', 'lg', 'xl'] },
74
+ );
@@ -0,0 +1,19 @@
1
+ import { AriaButtonProps } from 'react-aria';
2
+ import { ListProps } from 'react-stately';
3
+ import { VariantProps } from 'tailwind-variants';
4
+
5
+ import { MultiSelectProps } from '../../multi-select.types.js';
6
+
7
+ import { styles as triggerStyles } from './multi-select-list-box-trigger.styles.js';
8
+
9
+ type Variants = VariantProps<typeof triggerStyles>;
10
+
11
+ export type MultiSelectSize = Variants['size'];
12
+
13
+ export type MultiSelectListBoxTriggerProps<T> = {
14
+ id?: string;
15
+ placeholder: string;
16
+ selectedKeys?: ListProps<T>['selectedKeys'];
17
+ showSingleSectionTitle?: MultiSelectProps<T>['showSingleSectionTitle'];
18
+ triggerProps: AriaButtonProps<'button'>;
19
+ };
@@ -0,0 +1,64 @@
1
+ 'use client';
2
+
3
+ import React, { useContext } from 'react';
4
+ import { DismissButton, mergeProps, Overlay, usePopover } from 'react-aria';
5
+
6
+ import { MultiSelectContext } from '../../multi-select.component.js';
7
+
8
+ import { styles as popoverStyles } from './multi-select-popover.styles.js';
9
+
10
+ import type { MultiSelectPopoverProps } from './multi-select-popover.types.js';
11
+
12
+ export function MultiSelectPopover({ children, className, ...props }: MultiSelectPopoverProps) {
13
+ const { overlayState, overlayProps, popoverRef, buttonRef, placement, portalContainer } =
14
+ useContext(MultiSelectContext);
15
+
16
+ const { popoverProps } = usePopover(
17
+ {
18
+ ...props,
19
+ placement,
20
+ popoverRef,
21
+ triggerRef: buttonRef,
22
+ isNonModal: true,
23
+ shouldFlip: true,
24
+ shouldCloseOnInteractOutside: () => false, // need to manage accessibility manually due to complexity of component
25
+ offset: 6,
26
+ },
27
+ overlayState,
28
+ );
29
+
30
+ const width = buttonRef.current?.getBoundingClientRect().width;
31
+ const styles = popoverStyles();
32
+
33
+ return (
34
+ <Overlay disableFocusManagement portalContainer={portalContainer}>
35
+ <div
36
+ {...mergeProps(popoverProps, overlayProps)}
37
+ ref={popoverRef}
38
+ className={styles.overlay({ className })}
39
+ style={{ ...popoverProps.style, width: width ? `${width}px` : undefined }}
40
+ onBlur={e => {
41
+ const related = e.relatedTarget as Element | null;
42
+ if (!popoverRef?.current) return;
43
+ // keep open if focus moved to an element inside the popover
44
+ if (related && popoverRef.current.contains(related)) return;
45
+ // keep open if focus moved to the trigger button (so it doesn't open instantly on press)
46
+ if (related && buttonRef?.current && buttonRef.current.contains(related)) return;
47
+ overlayState.close();
48
+ }}
49
+ // Closes the dropdown when using keyboard navigation
50
+ onKeyDown={e => {
51
+ if (e.key === 'Tab' || e.key === 'Escape') {
52
+ overlayState.close();
53
+ }
54
+ }}
55
+ role="dialog"
56
+ aria-modal="true"
57
+ aria-label="Options list with filter"
58
+ >
59
+ {children}
60
+ <DismissButton onDismiss={() => overlayState.close()} />
61
+ </div>
62
+ </Overlay>
63
+ );
64
+ }