♻️ refactor: adjust style and refactor to a new component
This commit is contained in:
parent
5d0dd93271
commit
100e03b7a4
@ -23,17 +23,6 @@ export const selectTheme = tv({
|
|||||||
'disabled:border-none',
|
'disabled:border-none',
|
||||||
],
|
],
|
||||||
input: ['outline-none'],
|
input: ['outline-none'],
|
||||||
inputValue: [
|
|
||||||
'flex',
|
|
||||||
'items-center',
|
|
||||||
'gap-1',
|
|
||||||
'pl-2',
|
|
||||||
'pr-2',
|
|
||||||
'rounded-md',
|
|
||||||
'text-elements-mid-em',
|
|
||||||
'bg-base-bg-emphasized',
|
|
||||||
'hover:bg-base-bg-emphasized/80',
|
|
||||||
],
|
|
||||||
iconContainer: [
|
iconContainer: [
|
||||||
'absolute',
|
'absolute',
|
||||||
'inset-y-0',
|
'inset-y-0',
|
||||||
@ -53,7 +42,7 @@ export const selectTheme = tv({
|
|||||||
'py-1',
|
'py-1',
|
||||||
'flex-col',
|
'flex-col',
|
||||||
'gap-0.5',
|
'gap-0.5',
|
||||||
'w-full',
|
'min-w-full',
|
||||||
'bg-surface-floating',
|
'bg-surface-floating',
|
||||||
'shadow-dropdown',
|
'shadow-dropdown',
|
||||||
'w-auto',
|
'w-auto',
|
||||||
@ -63,46 +52,22 @@ export const selectTheme = tv({
|
|||||||
'border-gray-200',
|
'border-gray-200',
|
||||||
'rounded-xl',
|
'rounded-xl',
|
||||||
],
|
],
|
||||||
item: [
|
|
||||||
'p-2',
|
|
||||||
'gap-3',
|
|
||||||
'flex',
|
|
||||||
'items-start',
|
|
||||||
'justify-between',
|
|
||||||
'rounded-lg',
|
|
||||||
'group',
|
|
||||||
'data-[disabled]:cursor-not-allowed',
|
|
||||||
],
|
|
||||||
itemContent: ['flex', 'flex-1', 'whitespace-nowrap'],
|
|
||||||
itemLabel: [
|
|
||||||
'text-sm',
|
|
||||||
'text-elements-high-em',
|
|
||||||
'tracking-[-0.006em]',
|
|
||||||
'data-[disabled]:text-elements-disabled',
|
|
||||||
],
|
|
||||||
itemDescription: [
|
|
||||||
'text-xs',
|
|
||||||
'text-elements-low-em',
|
|
||||||
'data-[disabled]:text-elements-disabled',
|
|
||||||
],
|
|
||||||
itemIcon: ['h-4.5', 'w-4.5', 'text-elements-high-em'],
|
|
||||||
},
|
},
|
||||||
variants: {
|
variants: {
|
||||||
orientation: {
|
orientation: {
|
||||||
horizontal: {
|
horizontal: {
|
||||||
item: ['items-center'],
|
container: [],
|
||||||
itemContent: ['flex-row', 'items-center', 'gap-2'],
|
|
||||||
},
|
},
|
||||||
vertical: {
|
vertical: {
|
||||||
itemContent: ['flex-col', 'gap-0.5'],
|
container: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
variant: {
|
variant: {
|
||||||
default: {
|
default: {
|
||||||
item: [],
|
container: [],
|
||||||
},
|
},
|
||||||
danger: {
|
danger: {
|
||||||
item: [],
|
container: [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
state: {
|
state: {
|
||||||
@ -147,19 +112,9 @@ export const selectTheme = tv({
|
|||||||
popover: ['hidden'],
|
popover: ['hidden'],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
selected: {
|
|
||||||
true: {
|
|
||||||
item: ['bg-base-bg-emphasized', 'data-[disabled]:bg-transparent'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hasValue: {
|
hasValue: {
|
||||||
true: '',
|
true: '',
|
||||||
},
|
},
|
||||||
multiple: {
|
|
||||||
true: {
|
|
||||||
inputValue: ['pr-1'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
searchable: {
|
searchable: {
|
||||||
true: '',
|
true: '',
|
||||||
false: {
|
false: {
|
||||||
|
@ -10,41 +10,91 @@ import React, {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
import { useMultipleSelection, useCombobox } from 'downshift';
|
import { useMultipleSelection, useCombobox } from 'downshift';
|
||||||
import { SelectTheme, selectTheme } from './Select.theme';
|
import { SelectTheme, selectTheme } from './Select.theme';
|
||||||
import { Button } from '../Button';
|
|
||||||
import {
|
import {
|
||||||
CheckRadioIcon,
|
|
||||||
ChevronDownIcon,
|
ChevronDownIcon,
|
||||||
CrossIcon,
|
CrossIcon,
|
||||||
WarningIcon,
|
WarningIcon,
|
||||||
} from '../CustomIcon';
|
} from 'components/shared/CustomIcon';
|
||||||
import { cloneIcon } from 'utils/cloneIcon';
|
import { cloneIcon } from 'utils/cloneIcon';
|
||||||
import { cn } from 'utils/classnames';
|
import { cn } from 'utils/classnames';
|
||||||
|
import { SelectItem } from './SelectItem';
|
||||||
|
import { SelectValue } from './SelectValue';
|
||||||
|
|
||||||
export type DropdownItem = {
|
export type SelectOption = {
|
||||||
|
/**
|
||||||
|
* The value of the option
|
||||||
|
*/
|
||||||
value: string;
|
value: string;
|
||||||
|
/**
|
||||||
|
* The label of the option
|
||||||
|
*/
|
||||||
label: string;
|
label: string;
|
||||||
|
/**
|
||||||
|
* The description of the option
|
||||||
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/**
|
||||||
|
* Custom left icon for the option
|
||||||
|
*/
|
||||||
leftIcon?: ReactNode;
|
leftIcon?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Custom right icon for the option
|
||||||
|
*/
|
||||||
rightIcon?: ReactNode;
|
rightIcon?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Whether the option is disabled
|
||||||
|
*/
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface MultiSelectProps
|
/**
|
||||||
|
* The orientation of the select
|
||||||
|
*/
|
||||||
|
export type SelectOrientation = 'horizontal' | 'vertical';
|
||||||
|
|
||||||
|
interface SelectProps
|
||||||
extends Omit<ComponentPropsWithoutRef<'input'>, 'size'>,
|
extends Omit<ComponentPropsWithoutRef<'input'>, 'size'>,
|
||||||
SelectTheme {
|
SelectTheme {
|
||||||
items: DropdownItem[];
|
/**
|
||||||
|
* The options of the select
|
||||||
|
*/
|
||||||
|
options: SelectOption[];
|
||||||
|
/**
|
||||||
|
* The label of the select
|
||||||
|
*/
|
||||||
label?: string;
|
label?: string;
|
||||||
|
/**
|
||||||
|
* The description of the select
|
||||||
|
*/
|
||||||
description?: string;
|
description?: string;
|
||||||
|
/**
|
||||||
|
* Wheter the select is multiple or not
|
||||||
|
*/
|
||||||
multiple?: boolean;
|
multiple?: boolean;
|
||||||
|
/**
|
||||||
|
* Wheter the select is searchable or not
|
||||||
|
*/
|
||||||
searchable?: boolean;
|
searchable?: boolean;
|
||||||
|
/**
|
||||||
|
* Wheter the select is clearable or not
|
||||||
|
*/
|
||||||
clearable?: boolean;
|
clearable?: boolean;
|
||||||
|
/**
|
||||||
|
* Custom left icon for the select
|
||||||
|
*/
|
||||||
leftIcon?: ReactNode;
|
leftIcon?: ReactNode;
|
||||||
|
/**
|
||||||
|
* Custom right icon for the select
|
||||||
|
*/
|
||||||
rightIcon?: ReactNode;
|
rightIcon?: ReactNode;
|
||||||
|
/**
|
||||||
|
* The helper text of the select
|
||||||
|
*/
|
||||||
helperText?: string;
|
helperText?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MultiSelect: React.FC<MultiSelectProps> = ({
|
export const Select = ({
|
||||||
items,
|
options,
|
||||||
multiple = false,
|
multiple = false,
|
||||||
searchable = false,
|
searchable = false,
|
||||||
clearable,
|
clearable,
|
||||||
@ -58,11 +108,11 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
rightIcon,
|
rightIcon,
|
||||||
helperText,
|
helperText,
|
||||||
placeholder: placeholderProp = 'Select an option',
|
placeholder: placeholderProp = 'Select an option',
|
||||||
}) => {
|
}: SelectProps) => {
|
||||||
const theme = selectTheme({ size, state, variant, orientation });
|
const theme = selectTheme({ size, state, variant, orientation });
|
||||||
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const [selectedItem, setSelectedItem] = useState<DropdownItem | null>(null);
|
const [selectedItem, setSelectedItem] = useState<SelectOption | null>(null);
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||||
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
|
const [dropdownPosition, setDropdownPosition] = useState<'top' | 'bottom'>(
|
||||||
'bottom',
|
'bottom',
|
||||||
@ -74,11 +124,11 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dropdownOpen && popoverRef.current && inputWrapperRef.current) {
|
if (dropdownOpen && popoverRef.current && inputWrapperRef.current) {
|
||||||
const popover = popoverRef.current;
|
const popover = popoverRef.current;
|
||||||
// @ts-expect-error – we know it's not null
|
// @ts-expect-error – we know it's not null lol
|
||||||
const input = inputWrapperRef.current.getBoundingClientRect();
|
const input = inputWrapperRef.current.getBoundingClientRect();
|
||||||
const spaceBelow = window.innerHeight - input.bottom;
|
const spaceBelow = window.innerHeight - input.bottom;
|
||||||
const spaceAbove = input.top;
|
const spaceAbove = input.top;
|
||||||
// @ts-expect-error – we know it's not null
|
// @ts-expect-error – we know it's not null lol
|
||||||
const popoverHeight = popover.offsetHeight;
|
const popoverHeight = popover.offsetHeight;
|
||||||
|
|
||||||
// Determine if there's enough space below
|
// Determine if there's enough space below
|
||||||
@ -93,7 +143,7 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
}
|
}
|
||||||
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
|
}, [dropdownOpen]); // Re-calculate whenever the dropdown is opened
|
||||||
|
|
||||||
const handleSelectedItemChange = (selectedItem: DropdownItem | null) => {
|
const handleSelectedItemChange = (selectedItem: SelectOption | null) => {
|
||||||
setSelectedItem(selectedItem);
|
setSelectedItem(selectedItem);
|
||||||
setInputValue(selectedItem ? selectedItem.label : '');
|
setInputValue(selectedItem ? selectedItem.label : '');
|
||||||
};
|
};
|
||||||
@ -105,7 +155,7 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
removeSelectedItem,
|
removeSelectedItem,
|
||||||
selectedItems,
|
selectedItems,
|
||||||
reset,
|
reset,
|
||||||
} = useMultipleSelection<DropdownItem>({
|
} = useMultipleSelection<SelectOption>({
|
||||||
onSelectedItemsChange: multiple
|
onSelectedItemsChange: multiple
|
||||||
? undefined
|
? undefined
|
||||||
: ({ selectedItems }) => {
|
: ({ selectedItems }) => {
|
||||||
@ -115,15 +165,15 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
|
|
||||||
const filteredItems = useMemo(() => {
|
const filteredItems = useMemo(() => {
|
||||||
// Show all items if the dropdown is not multiple and not searchable
|
// Show all items if the dropdown is not multiple and not searchable
|
||||||
if (!multiple && !searchable) return items;
|
if (!multiple && !searchable) return options;
|
||||||
// Show only the items that match the input value
|
// Show only the items that match the input value
|
||||||
if (!multiple && searchable) {
|
if (!multiple && searchable) {
|
||||||
return items.filter((item) =>
|
return options.filter((item) =>
|
||||||
item.label.toLowerCase().includes(inputValue.toLowerCase()),
|
item.label.toLowerCase().includes(inputValue.toLowerCase()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Show only the items that match the input value and are not already selected
|
// Show only the items that match the input value and are not already selected
|
||||||
return items.filter(
|
return options.filter(
|
||||||
(item) =>
|
(item) =>
|
||||||
item.label.toLowerCase().includes(inputValue.toLowerCase()) &&
|
item.label.toLowerCase().includes(inputValue.toLowerCase()) &&
|
||||||
!(
|
!(
|
||||||
@ -132,7 +182,7 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
: []
|
: []
|
||||||
).includes(item),
|
).includes(item),
|
||||||
);
|
);
|
||||||
}, [items, inputValue, selectedItem, selectedItems, multiple, searchable]);
|
}, [options, inputValue, selectedItem, selectedItems, multiple, searchable]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isOpen,
|
isOpen,
|
||||||
@ -172,7 +222,7 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isSelected = useCallback(
|
const isSelected = useCallback(
|
||||||
(item: DropdownItem) =>
|
(item: SelectOption) =>
|
||||||
multiple ? selectedItems.includes(item) : selectedItem === item,
|
multiple ? selectedItems.includes(item) : selectedItem === item,
|
||||||
[selectedItems, selectedItem, multiple],
|
[selectedItems, selectedItem, multiple],
|
||||||
);
|
);
|
||||||
@ -233,44 +283,45 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
[cloneIcon, state, theme, helperText],
|
[cloneIcon, state, theme, helperText],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const isMultipleHasValue = multiple && selectedItems.length > 0;
|
||||||
const isMultipleHasValueButNotSearchable =
|
const isMultipleHasValueButNotSearchable =
|
||||||
multiple && !searchable && selectedItems.length > 0;
|
multiple && !searchable && selectedItems.length > 0;
|
||||||
const placeholder = isMultipleHasValueButNotSearchable ? '' : placeholderProp;
|
const placeholder = isMultipleHasValueButNotSearchable ? '' : placeholderProp;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={theme.container()}>
|
<div className={theme.container()}>
|
||||||
|
{/* Label & description */}
|
||||||
{renderLabels}
|
{renderLabels}
|
||||||
|
|
||||||
|
{/* Input */}
|
||||||
<div
|
<div
|
||||||
ref={inputWrapperRef}
|
ref={inputWrapperRef}
|
||||||
className={theme.inputWrapper({
|
className={theme.inputWrapper({
|
||||||
hasValue: multiple && selectedItems.length > 0,
|
hasValue: isMultipleHasValue,
|
||||||
})}
|
})}
|
||||||
onClick={() => !dropdownOpen && openMenu()}
|
onClick={() => !dropdownOpen && openMenu()}
|
||||||
>
|
>
|
||||||
|
{/* Left icon */}
|
||||||
{leftIcon && renderLeftIcon}
|
{leftIcon && renderLeftIcon}
|
||||||
{multiple &&
|
|
||||||
selectedItems.length > 0 &&
|
{/* Multiple input values */}
|
||||||
|
{isMultipleHasValue &&
|
||||||
selectedItems.map((item, index) => (
|
selectedItems.map((item, index) => (
|
||||||
<span
|
<SelectValue
|
||||||
key={`selected-item-${index}`}
|
key={`selected-item-${index}`}
|
||||||
{...getSelectedItemProps({ selectedItem: item, index })}
|
{...getSelectedItemProps({ selectedItem: item, index })}
|
||||||
className={theme.inputValue({ multiple })}
|
option={item}
|
||||||
>
|
size={size}
|
||||||
{item.label}
|
onDelete={removeSelectedItem}
|
||||||
<Button
|
/>
|
||||||
onClick={() => removeSelectedItem(item)}
|
|
||||||
iconOnly
|
|
||||||
variant="unstyled"
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
<CrossIcon size={14} />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
))}
|
))}
|
||||||
|
|
||||||
|
{/* Single input value or searchable area */}
|
||||||
<input
|
<input
|
||||||
{...getInputProps(getDropdownProps())}
|
{...getInputProps(getDropdownProps())}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
readOnly={!searchable} // Control readOnly based on searchable
|
// Control readOnly based on searchable
|
||||||
|
readOnly={!searchable}
|
||||||
className={cn(theme.input({ searchable }), {
|
className={cn(theme.input({ searchable }), {
|
||||||
// Make the input width smaller because we don't need it (not searchable)
|
// Make the input width smaller because we don't need it (not searchable)
|
||||||
'w-6': isMultipleHasValueButNotSearchable,
|
'w-6': isMultipleHasValueButNotSearchable,
|
||||||
@ -278,58 +329,43 @@ export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|||||||
'ml-6': isMultipleHasValueButNotSearchable && clearable,
|
'ml-6': isMultipleHasValueButNotSearchable && clearable,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{/* Right icon */}
|
||||||
{renderRightIcon}
|
{renderRightIcon}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Helper text */}
|
||||||
{renderHelperText}
|
{renderHelperText}
|
||||||
|
|
||||||
|
{/* Popover */}
|
||||||
<ul
|
<ul
|
||||||
{...getMenuProps()}
|
{...getMenuProps()}
|
||||||
id="popover"
|
id="popover"
|
||||||
ref={popoverRef}
|
ref={popoverRef}
|
||||||
className={cn(theme.popover({ isOpen }), {
|
className={cn(theme.popover({ isOpen }), {
|
||||||
'top-1/4': dropdownPosition === 'bottom',
|
// Position the popover based on the dropdown position
|
||||||
'bottom-[95%]': dropdownPosition === 'top',
|
'top-[12.5%]': dropdownPosition === 'bottom' && !label,
|
||||||
|
'top-[35%]': dropdownPosition === 'bottom' && label,
|
||||||
|
'top-[42.5%]': dropdownPosition === 'bottom' && label && description,
|
||||||
|
'bottom-[92.5%]': dropdownPosition === 'top' && !label,
|
||||||
|
'bottom-[75%]': dropdownPosition === 'top' && label,
|
||||||
|
'bottom-[65%]': dropdownPosition === 'top' && label && description,
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{isOpen && filteredItems.length !== 0 ? (
|
{isOpen &&
|
||||||
|
filteredItems.length !== 0 &&
|
||||||
filteredItems.map((item, index) => (
|
filteredItems.map((item, index) => (
|
||||||
<li
|
<SelectItem
|
||||||
{...getItemProps({ item, index })}
|
{...getItemProps({ item, index })}
|
||||||
key={item.value}
|
key={item.value}
|
||||||
className={theme.item({
|
selected={isSelected(item)}
|
||||||
selected: highlightedIndex === index,
|
option={item}
|
||||||
})}
|
hovered={highlightedIndex === index}
|
||||||
data-disabled={item.disabled}
|
orientation={orientation}
|
||||||
>
|
empty={filteredItems.length === 0}
|
||||||
{item.leftIcon &&
|
variant={variant}
|
||||||
cloneIcon(item.leftIcon, { className: theme.itemIcon() })}
|
/>
|
||||||
<div className={theme.itemContent()}>
|
))}
|
||||||
<p className={theme.itemLabel()} data-disabled={item.disabled}>
|
|
||||||
{item.label}
|
|
||||||
</p>
|
|
||||||
{orientation === 'horizontal' && (
|
|
||||||
<span className="h-1 w-1 rounded-full bg-border-interactive-hovered/[0.14]" />
|
|
||||||
)}
|
|
||||||
{item.description && (
|
|
||||||
<p
|
|
||||||
className={theme.itemDescription()}
|
|
||||||
data-disabled={item.disabled}
|
|
||||||
>
|
|
||||||
{item.description}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
{item.rightIcon
|
|
||||||
? cloneIcon(item.rightIcon, { className: theme.itemIcon() })
|
|
||||||
: isSelected(item) && (
|
|
||||||
<CheckRadioIcon
|
|
||||||
className={cn(theme.itemIcon(), 'text-controls-primary')}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<li className={theme.item()}>{'No results found'}</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -1,157 +0,0 @@
|
|||||||
import React, { useState } from 'react';
|
|
||||||
import { useMultipleSelection, useCombobox } from 'downshift';
|
|
||||||
import { SelectTheme, selectTheme } from './Select.theme';
|
|
||||||
import { Button } from '../Button';
|
|
||||||
import { CrossIcon } from '../CustomIcon';
|
|
||||||
|
|
||||||
type Item = {
|
|
||||||
value: string;
|
|
||||||
label: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface MultiSelectProps extends SelectTheme {
|
|
||||||
items: Item[];
|
|
||||||
multiple?: boolean;
|
|
||||||
searchable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const MultiSelect: React.FC<MultiSelectProps> = ({
|
|
||||||
items,
|
|
||||||
multiple = false,
|
|
||||||
searchable: searchableProp,
|
|
||||||
size,
|
|
||||||
state,
|
|
||||||
}) => {
|
|
||||||
const searchable = multiple ? true : searchableProp || false;
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
const [selectedItem, setSelectedItem] = useState<Item | null>(null);
|
|
||||||
const [dropdownOpen, setDropdownOpen] = useState(false); // Track dropdown open state
|
|
||||||
|
|
||||||
const {
|
|
||||||
container,
|
|
||||||
inputWrapper,
|
|
||||||
input: inputClass,
|
|
||||||
inputValue: inputValueClass,
|
|
||||||
popover,
|
|
||||||
item: itemClass,
|
|
||||||
} = selectTheme({ size, state });
|
|
||||||
|
|
||||||
const getFilteredItems = () => {
|
|
||||||
if (!multiple && !searchable) {
|
|
||||||
return items;
|
|
||||||
}
|
|
||||||
return items.filter(
|
|
||||||
(item) =>
|
|
||||||
item.label.toLowerCase().includes(inputValue.toLowerCase()) &&
|
|
||||||
!(
|
|
||||||
multiple ? selectedItems : selectedItem ? [selectedItem] : []
|
|
||||||
).includes(item),
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const {
|
|
||||||
getSelectedItemProps,
|
|
||||||
getDropdownProps,
|
|
||||||
addSelectedItem,
|
|
||||||
removeSelectedItem,
|
|
||||||
selectedItems,
|
|
||||||
} = useMultipleSelection<Item>({
|
|
||||||
onSelectedItemsChange: multiple
|
|
||||||
? undefined
|
|
||||||
: ({ selectedItems }) => {
|
|
||||||
setSelectedItem(selectedItems?.[0] || null);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const {
|
|
||||||
isOpen,
|
|
||||||
getMenuProps,
|
|
||||||
getInputProps,
|
|
||||||
highlightedIndex,
|
|
||||||
getItemProps,
|
|
||||||
openMenu,
|
|
||||||
} = useCombobox({
|
|
||||||
items: getFilteredItems(),
|
|
||||||
onInputValueChange: ({ inputValue = '' }) => setInputValue(inputValue),
|
|
||||||
onSelectedItemChange: ({ selectedItem }) => {
|
|
||||||
if (!multiple && selectedItem) {
|
|
||||||
setSelectedItem(selectedItem);
|
|
||||||
if (searchable) {
|
|
||||||
setInputValue(''); // Clear input value for searchable when an item is selected
|
|
||||||
} else {
|
|
||||||
setInputValue(selectedItem.label); // Set input value to selectedItem's label if searchable is off
|
|
||||||
}
|
|
||||||
} else if (multiple && selectedItem) {
|
|
||||||
addSelectedItem(selectedItem);
|
|
||||||
setInputValue('');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onIsOpenChange: ({ isOpen }) => {
|
|
||||||
// @ts-expect-error - isOpen is not defined in Downshift's types
|
|
||||||
setDropdownOpen(isOpen);
|
|
||||||
if (!isOpen) {
|
|
||||||
// Reset input value to selected item's label when dropdown is closed and it's not multiple
|
|
||||||
if (!multiple && selectedItem && searchable) {
|
|
||||||
setInputValue('');
|
|
||||||
}
|
|
||||||
} else if (isOpen && !multiple && selectedItem && searchable) {
|
|
||||||
// Clear input value and show selectedItem's label as placeholder when dropdown opens
|
|
||||||
setInputValue('');
|
|
||||||
}
|
|
||||||
},
|
|
||||||
selectedItem: multiple ? null : selectedItem,
|
|
||||||
itemToString: (item) => (item && !multiple ? item.label : ''),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={container()}>
|
|
||||||
<div
|
|
||||||
className={inputWrapper({
|
|
||||||
hasValue: multiple && selectedItems.length > 0,
|
|
||||||
})}
|
|
||||||
onClick={() => !dropdownOpen && openMenu()}
|
|
||||||
>
|
|
||||||
{multiple &&
|
|
||||||
selectedItems.length > 0 &&
|
|
||||||
selectedItems.map((item, index) => (
|
|
||||||
<span
|
|
||||||
key={`selected-item-${index}`}
|
|
||||||
{...getSelectedItemProps({ selectedItem: item, index })}
|
|
||||||
className={inputValueClass({ multiple })}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
<Button
|
|
||||||
onClick={() => removeSelectedItem(item)}
|
|
||||||
iconOnly
|
|
||||||
variant="unstyled"
|
|
||||||
size="xs"
|
|
||||||
>
|
|
||||||
<CrossIcon size={14} />
|
|
||||||
</Button>
|
|
||||||
</span>
|
|
||||||
))}
|
|
||||||
<input
|
|
||||||
{...getInputProps(getDropdownProps())}
|
|
||||||
placeholder="Select an option..."
|
|
||||||
readOnly={!searchable} // Control readOnly based on searchable
|
|
||||||
className={inputClass({ searchable })}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ul {...getMenuProps()} className={popover({ isOpen })}>
|
|
||||||
{isOpen && (getFilteredItems().length !== 0 || !searchable) ? (
|
|
||||||
getFilteredItems().map((item, index) => (
|
|
||||||
<li
|
|
||||||
{...getItemProps({ item, index })}
|
|
||||||
key={item.value}
|
|
||||||
className={itemClass({ selected: highlightedIndex === index })}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</li>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<li className={itemClass()}>{'No results found'}</li>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user