) => {
+ action('optionsChange')(e)
+ setSelectedItems(e.selectedItems)
+ }
+
return (
<>
+
> = (args) => {
gridTemplateColumns: 'repeat(4, auto)',
}}
>
- {selectedItems
- .filter((option) => !(option.label === selectAllOption.label))
- .map((x) => (
- onDelete(x.label)}>
- {x.label}
-
- ))}
+ {selectedItems.map((x) => (
+ onDelete(x.label)}>
+ {x.label}
+
+ ))}
- !(o.label === selectAllOption.label))
- .length
- }/ ${options.length} selected`}
- multiple
- optionLabel={optionLabel}
- />
>
)
}
diff --git a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.test.tsx b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.test.tsx
index 826acf1cf4..65ecf494b7 100644
--- a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.test.tsx
+++ b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.test.tsx
@@ -181,6 +181,43 @@ describe('Autocomplete', () => {
expect(checked.length).toBe(2)
})
+ it('Can select all options', async () => {
+ const onChange = jest.fn()
+ render(
+ ,
+ )
+
+ const labeledNodes = await screen.findAllByLabelText(labelText)
+ const optionsList = labeledNodes[1]
+
+ const buttonNode = await screen.findByLabelText('toggle options', {
+ selector: 'button',
+ })
+
+ fireEvent.click(buttonNode)
+
+ const options = await within(optionsList).findAllByRole('option')
+ fireEvent.click(options[0])
+
+ await waitFor(() => {
+ expect(onChange).toHaveBeenCalledWith({ selectedItems: items })
+ })
+
+ fireEvent.click(options[0])
+
+ await waitFor(() => {
+ expect(onChange).toHaveBeenCalledWith({ selectedItems: [] })
+ })
+ })
+
it('Can open the options on button click', async () => {
render()
diff --git a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tsx b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tsx
index ca943d5668..32a74c7079 100644
--- a/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tsx
+++ b/packages/eds-core-react/src/components/Autocomplete/Autocomplete.tsx
@@ -58,6 +58,8 @@ const Container = styled.div`
position: relative;
`
+const AllSymbol = Symbol('Select all')
+
const StyledList = styled(List)(
({ theme }) => css`
background-color: ${theme.background};
@@ -96,7 +98,10 @@ const StyledButton = styled(Button)(
`,
)
-type IndexFinderType = ({
+// Typescript can struggle with parsing generic arrow functions in a .tsx file (see https://github.com/microsoft/TypeScript/issues/15713)
+// Workaround is to add a trailing , after T, which tricks the compiler, but also have to ignore prettier rule.
+// prettier-ignore
+type IndexFinderType = ({
calc,
index,
optionDisabled,
@@ -244,6 +249,8 @@ export type AutocompleteProps = {
onInputChange?: (text: string) => void
/** Enable multiselect */
multiple?: boolean
+ /** Add select-all option. Throws an error if true while multiple = false */
+ allowSelectAll?: boolean
/** Custom option label. NOTE: This is required when option is an object */
optionLabel?: (option: T) => string
/** Custom option template */
@@ -292,6 +299,7 @@ function AutocompleteInner(
onInputChange,
selectedOptions,
multiple,
+ allowSelectAll,
initialSelectedOptions = [],
disablePortal,
optionDisabled = () => false,
@@ -312,9 +320,29 @@ function AutocompleteInner(
const isControlled = Boolean(selectedOptions)
const [inputOptions, setInputOptions] = useState(options)
- const [availableItems, setAvailableItems] = useState(inputOptions)
+ const [_availableItems, setAvailableItems] = useState(inputOptions)
const [typedInputValue, setTypedInputValue] = useState('')
+ const showSelectAll = useMemo(() => {
+ if (!multiple && allowSelectAll) {
+ throw new Error(`allowSelectAll can only be used with multiple`)
+ }
+ return allowSelectAll && !typedInputValue
+ }, [allowSelectAll, multiple, typedInputValue])
+
+ const availableItems = useMemo(() => {
+ if (showSelectAll) return [AllSymbol as T, ..._availableItems]
+ return _availableItems
+ }, [_availableItems, showSelectAll])
+
+ const toggleAllSelected = () => {
+ if (selectedItems.length === inputOptions.length) {
+ setSelectedItems([])
+ } else {
+ setSelectedItems(inputOptions)
+ }
+ }
+
useEffect(() => {
const availableHash = JSON.stringify(inputOptions)
const optionsHash = JSON.stringify(options)
@@ -353,7 +381,9 @@ function AutocompleteInner(
...multipleSelectionProps,
onSelectedItemsChange: (changes) => {
if (onOptionsChange) {
- const { selectedItems } = changes
+ const selectedItems = changes.selectedItems.filter(
+ (item) => item !== AllSymbol,
+ )
onOptionsChange({ selectedItems })
}
},
@@ -376,6 +406,14 @@ function AutocompleteInner(
setSelectedItems,
} = useMultipleSelection(multipleSelectionProps)
+ const allSelectedState = useMemo(() => {
+ if (!inputOptions || !selectedItems) return 'NONE'
+ if (inputOptions.length === selectedItems.length) return 'ALL'
+ if (inputOptions.length != selectedItems.length && selectedItems.length > 0)
+ return 'SOME'
+ return 'NONE'
+ }, [inputOptions, selectedItems])
+
const getLabel = useCallback(
(item: T) => {
//note: non strict check for null or undefined to allow 0
@@ -448,7 +486,9 @@ function AutocompleteInner(
type !== useCombobox.stateChangeTypes.MenuMouseLeave &&
highlightedIndex >= 0
) {
- rowVirtualizer.scrollToIndex(highlightedIndex)
+ rowVirtualizer.scrollToIndex(highlightedIndex, {
+ align: allowSelectAll ? 'center' : 'auto',
+ })
}
},
onIsOpenChange: ({ selectedItem }) => {
@@ -465,7 +505,9 @@ function AutocompleteInner(
case useCombobox.stateChangeTypes.ItemClick:
//note: non strict check for null or undefined to allow 0
if (selectedItem != null && !optionDisabled(selectedItem)) {
- if (multiple) {
+ if (selectedItem === AllSymbol) {
+ toggleAllSelected()
+ } else if (multiple) {
selectedItems.includes(selectedItem)
? removeSelectedItem(selectedItem)
: addSelectedItem(selectedItem)
@@ -606,6 +648,9 @@ function AutocompleteInner(
}
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
+ if (clearSearchOnChange) {
+ setTypedInputValue('')
+ }
return {
...changes,
isOpen: true, // keep menu open after selection.
@@ -756,10 +801,44 @@ function AutocompleteInner(
const label = getLabel(item)
const isDisabled = optionDisabled(item)
const isSelected = selectedItemsLabels.includes(label)
+ if (item === AllSymbol) {
+ return (
+
+ )
+ }
return (
function AutocompleteOptionInner(
@@ -73,6 +74,7 @@ function AutocompleteOptionInner(
optionComponent,
multiple,
isSelected,
+ indeterminate,
isDisabled,
multiline,
highlighted,
@@ -97,6 +99,7 @@ function AutocompleteOptionInner(
{
return null