@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.
- package/CHANGELOG.md +12 -0
- package/dist/component-type.json +1 -1
- package/dist/components/compacta/compacta.component.d.ts +1 -1
- package/dist/components/compacta/compacta.component.js +21 -1
- package/dist/components/compacta/compacta.types.d.ts +12 -0
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.component.d.ts +2 -0
- package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.component.js +20 -0
- package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.styles.d.ts +37 -0
- package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.styles.js +8 -0
- package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.types.d.ts +5 -0
- package/dist/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.types.js +1 -0
- 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
- 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
- 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
- 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
- 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
- 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
- package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.d.ts +2 -0
- package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.js +62 -0
- package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.styles.d.ts +82 -0
- package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.styles.js +32 -0
- package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.types.d.ts +8 -0
- package/dist/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.types.js +1 -0
- 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
- 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
- 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
- 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
- package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.component.d.ts +2 -0
- package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.component.js +35 -0
- package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.styles.d.ts +43 -0
- package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.styles.js +9 -0
- package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.types.d.ts +5 -0
- package/dist/components/multi-select/components/multi-select-list-box/multi-select-list-box.types.js +1 -0
- package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.component.d.ts +2 -0
- package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.component.js +118 -0
- package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.d.ts +263 -0
- package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.js +99 -0
- package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.types.d.ts +15 -0
- package/dist/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.types.js +1 -0
- package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.component.d.ts +2 -0
- package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.component.js +52 -0
- package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.styles.d.ts +31 -0
- package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.styles.js +7 -0
- package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.types.d.ts +6 -0
- package/dist/components/multi-select/components/multi-select-popover/multi-select-popover.types.js +1 -0
- package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.d.ts +2 -0
- package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.js +74 -0
- package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.styles.d.ts +31 -0
- package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.styles.js +7 -0
- package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.types.d.ts +6 -0
- package/dist/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.types.js +1 -0
- package/dist/components/multi-select/index.d.ts +2 -0
- package/dist/components/multi-select/index.js +1 -0
- package/dist/components/multi-select/multi-select.component.d.ts +7 -0
- package/dist/components/multi-select/multi-select.component.js +95 -0
- package/dist/components/multi-select/multi-select.styles.d.ts +25 -0
- package/dist/components/multi-select/multi-select.styles.js +6 -0
- package/dist/components/multi-select/multi-select.types.d.ts +61 -0
- package/dist/components/multi-select/multi-select.types.js +1 -0
- package/dist/components/multi-select/utils/filter-nodes.d.ts +2 -0
- package/dist/components/multi-select/utils/filter-nodes.js +25 -0
- package/dist/components/tooltip/components/tooltip-content/tooltip-content.component.d.ts +1 -1
- package/dist/components/tooltip/components/tooltip-content/tooltip-content.component.js +4 -2
- package/dist/components/tooltip/components/tooltip-content/tooltip-content.styles.d.ts +16 -1
- package/dist/components/tooltip/components/tooltip-content/tooltip-content.styles.js +7 -1
- package/dist/components/tooltip/components/tooltip-content/tooltip-content.types.d.ts +1 -0
- package/dist/components/tooltip/tooltip.component.d.ts +1 -1
- package/dist/components/tooltip/tooltip.component.js +4 -3
- package/dist/components/tooltip/tooltip.types.d.ts +3 -0
- package/dist/css/westpac-ui.css +366 -0
- package/dist/css/westpac-ui.min.css +366 -0
- package/package.json +4 -1
- package/src/components/compacta/compacta.component.tsx +21 -0
- package/src/components/compacta/compacta.types.ts +10 -0
- package/src/components/index.ts +1 -0
- package/src/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.component.tsx +26 -0
- package/src/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.styles.ts +9 -0
- package/src/components/multi-select/components/multi-select-dropdown/multi-select-dropdown.types.ts +6 -0
- 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
- 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
- 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
- package/src/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.component.tsx +66 -0
- package/src/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.styles.ts +33 -0
- package/src/components/multi-select/components/multi-select-list-box/components/multi-select-option/multi-select-option.types.ts +7 -0
- 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
- 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
- package/src/components/multi-select/components/multi-select-list-box/multi-select-list-box.component.tsx +42 -0
- package/src/components/multi-select/components/multi-select-list-box/multi-select-list-box.styles.ts +10 -0
- package/src/components/multi-select/components/multi-select-list-box/multi-select-list-box.types.ts +6 -0
- package/src/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.component.tsx +136 -0
- package/src/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.styles.ts +74 -0
- package/src/components/multi-select/components/multi-select-list-box-trigger/multi-select-list-box-trigger.types.ts +19 -0
- package/src/components/multi-select/components/multi-select-popover/multi-select-popover.component.tsx +64 -0
- package/src/components/multi-select/components/multi-select-popover/multi-select-popover.styles.ts +8 -0
- package/src/components/multi-select/components/multi-select-popover/multi-select-popover.types.ts +8 -0
- package/src/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.component.tsx +81 -0
- package/src/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.styles.ts +8 -0
- package/src/components/multi-select/components/multi-select-searchbar/multi-select-searchbar.types.ts +7 -0
- package/src/components/multi-select/index.ts +2 -0
- package/src/components/multi-select/multi-select.component.tsx +116 -0
- package/src/components/multi-select/multi-select.styles.ts +7 -0
- package/src/components/multi-select/multi-select.types.ts +60 -0
- package/src/components/multi-select/utils/filter-nodes.ts +29 -0
- package/src/components/tooltip/components/tooltip-content/tooltip-content.component.tsx +2 -2
- package/src/components/tooltip/components/tooltip-content/tooltip-content.styles.ts +6 -0
- package/src/components/tooltip/components/tooltip-content/tooltip-content.types.ts +1 -0
- package/src/components/tooltip/tooltip.component.tsx +7 -3
- package/src/components/tooltip/tooltip.types.ts +4 -0
|
@@ -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,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
|
+
}
|
package/src/components/multi-select/components/multi-select-list-box/multi-select-list-box.styles.ts
ADDED
|
@@ -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,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
|
+
}
|