diff --git a/lib/components/MultiSelectDropdown/MultiSelectDropdown.stories.tsx b/lib/components/MultiSelectDropdown/MultiSelectDropdown.stories.tsx index dc0d6dd0..e5343fbb 100644 --- a/lib/components/MultiSelectDropdown/MultiSelectDropdown.stories.tsx +++ b/lib/components/MultiSelectDropdown/MultiSelectDropdown.stories.tsx @@ -34,8 +34,6 @@ export const MultiSelectDropdown: Story = { value: 'prod', }, ], - onBlur: () => console.log('blur'), - onChange: (args) => console.log('change', args), }, render: (args) => (
diff --git a/lib/components/MultiSelectDropdown/MultiSelectDropdown.tsx b/lib/components/MultiSelectDropdown/MultiSelectDropdown.tsx index 97c43430..f7ea21d0 100644 --- a/lib/components/MultiSelectDropdown/MultiSelectDropdown.tsx +++ b/lib/components/MultiSelectDropdown/MultiSelectDropdown.tsx @@ -10,7 +10,17 @@ export const MultiSelectDropdown: FC = forwardRef< MultiSelectDropdownProps >( ( - { options, multiselect, value, onChange, onBlur, name, ...delegated }, + { + options, + multiselect, + value, + onChange, + onBlur, + name, + isLoading, + noOptionsText, + ...delegated + }, ref, ) => ( = forwardRef< onChange={onChange} onBlur={onBlur} name={name} + isLoading={isLoading} + noOptionsText={noOptionsText} > ), ); + +MultiSelectDropdown.displayName = 'MultiSelectDropdown'; diff --git a/lib/components/MultiSelectDropdown/MultiSelectDropdown.types.ts b/lib/components/MultiSelectDropdown/MultiSelectDropdown.types.ts index 0f3cbb3a..f8b2da97 100644 --- a/lib/components/MultiSelectDropdown/MultiSelectDropdown.types.ts +++ b/lib/components/MultiSelectDropdown/MultiSelectDropdown.types.ts @@ -8,7 +8,7 @@ import { multiSelectDropdownVariants } from './MultiSelectDropdown.variants'; export type MultiSelectDropdownOption = { id: string | number; label: string; - tagLabel: string; + tagLabel?: string; tagColor?: TagProps['color']; value?: string; }; @@ -17,6 +17,11 @@ type OnChangeFn = (params: { target: { value: MultiSelectDropdownOption[]; name: string }; }) => void; +type OnBlurFn = (event: { + target: HTMLInputElement | null; + type?: string; +}) => void; + export interface MultiSelectDropdownProps extends VariantProps, @@ -33,5 +38,7 @@ export interface MultiSelectDropdownProps multiselect?: boolean; value?: MultiSelectDropdownOption[]; onChange?: OnChangeFn; - onBlur?: VoidFunction; + onBlur?: OnBlurFn; + isLoading?: boolean; + noOptionsText?: string; } diff --git a/lib/components/MultiSelectDropdown/components/Item/Item.tsx b/lib/components/MultiSelectDropdown/components/Item/Item.tsx index d9d2e2d1..8557cabf 100644 --- a/lib/components/MultiSelectDropdown/components/Item/Item.tsx +++ b/lib/components/MultiSelectDropdown/components/Item/Item.tsx @@ -8,25 +8,32 @@ import { ItemProps } from './Item.types'; import { wrapperVariants } from './Item.variants'; import { Tag, Typography } from '@/components'; -export const Item: FC = ({ option, theme, isSelected }) => { +export const Item: FC = ({ + option, + theme, + isSelected, + className, +}) => { const { onSelectOption } = useMultiSelectDropdown(); return (
  • onSelectOption(option)} > {option.label} - + {option.tagLabel && ( + + )}
  • ); }; diff --git a/lib/components/MultiSelectDropdown/components/Item/Item.types.ts b/lib/components/MultiSelectDropdown/components/Item/Item.types.ts index efc5d574..b7f33be9 100644 --- a/lib/components/MultiSelectDropdown/components/Item/Item.types.ts +++ b/lib/components/MultiSelectDropdown/components/Item/Item.types.ts @@ -6,4 +6,5 @@ export type ItemProps = { option: MultiSelectDropdownOption; theme?: Theme; isSelected?: boolean; + className?: string; }; diff --git a/lib/components/MultiSelectDropdown/components/List/List.tsx b/lib/components/MultiSelectDropdown/components/List/List.tsx index 5da46e72..99d74e41 100644 --- a/lib/components/MultiSelectDropdown/components/List/List.tsx +++ b/lib/components/MultiSelectDropdown/components/List/List.tsx @@ -1,20 +1,28 @@ import { FC } from 'react'; import { cn } from '@/utils'; +import { Typography } from '@/components'; import { Item } from '../Item/Item'; import { useMultiSelectDropdown } from '../../contexts'; import { ListProps } from './List.types'; import { wrapperVariants } from './List.variants'; -import { Typography } from '@/components'; export const List: FC = ({ theme }) => { - const { options, selectedOptions } = useMultiSelectDropdown(); + const { options, selectedOptions, isLoading, noOptionsText } = + useMultiSelectDropdown(); return (
      - {options.length > 0 ? ( + {isLoading ? ( + + ) : options.length > 0 ? ( options.map((option) => ( = ({ theme }) => { /> )) ) : ( - - No options - +
    • + + {noOptionsText ?? 'No options'} + +
    • )}
    ); diff --git a/lib/components/MultiSelectDropdown/components/Wrapper/Wrapper.tsx b/lib/components/MultiSelectDropdown/components/Wrapper/Wrapper.tsx index 67ad4e91..80021b97 100644 --- a/lib/components/MultiSelectDropdown/components/Wrapper/Wrapper.tsx +++ b/lib/components/MultiSelectDropdown/components/Wrapper/Wrapper.tsx @@ -1,7 +1,8 @@ import { FC, forwardRef, useId, useImperativeHandle } from 'react'; import { ChevronUp } from 'react-feather'; -import { Tag } from '@/components/Tag/Tag'; +import { Tag } from '@/components'; +import Loader from '@/assets/icons/loader.svg'; import { cn } from '@/utils'; import { useMultiSelectDropdown as useMultiSelectDropdownContext } from '../../contexts'; @@ -25,8 +26,14 @@ export const Wrapper: FC = forwardRef< ref, ) => { const id = useId(); - const { selectedOptions, isOpen, onOpen, onRemoveOption, inputRef } = - useMultiSelectDropdownContext(); + const { + selectedOptions, + isOpen, + onOpen, + onRemoveOption, + inputRef, + isLoading, + } = useMultiSelectDropdownContext(); const { wrapperRef, handleOpen } = useMultiSelectDropdown(); useImperativeHandle(ref, () => inputRef!.current!, [inputRef]); @@ -72,7 +79,7 @@ export const Wrapper: FC = forwardRef< = forwardRef<
    )} - + {isLoading ? ( + + ) : ( + + )} = forwardRef< ); }, ); + +Wrapper.displayName = 'MultiSelectDropdownWrapper'; diff --git a/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.context.ts b/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.context.ts index f5cd65d4..13cd77c0 100644 --- a/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.context.ts +++ b/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.context.ts @@ -16,6 +16,8 @@ const initialState: State = { onOpen() { throw new Error('Function not implemented.'); }, + isLoading: false, + noOptionsText: undefined, }; export const MultiSelectDropdownContext = createContext(initialState); diff --git a/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.provider.tsx b/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.provider.tsx index 9b267b74..6534f85f 100644 --- a/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.provider.tsx +++ b/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.provider.tsx @@ -23,6 +23,8 @@ export const MultiSelectDropdownProvider: FC< onChange, onBlur, name, + isLoading, + noOptionsText, }) => { const inputRef = useRef>(null); const [isOpen, setIsOpen] = useToggle(false); @@ -33,7 +35,24 @@ export const MultiSelectDropdownProvider: FC< >([]); const isControlled = value !== undefined; - // Sync value prop to selected options + // Sync defaultOptions to options state (for uncontrolled mode) + useEffect(() => { + if (!isControlled) { + const selectedIdsSet = new Set( + selectedOptions.map((option) => option.id), + ); + setOptions( + multiselect + ? defaultOptions.filter((option) => !selectedIdsSet.has(option.id)) + : defaultOptions.map((option) => ({ + ...option, + isSelected: selectedIdsSet.has(option.id), + })), + ); + } + }, [defaultOptions, multiselect, isControlled, selectedOptions]); + + // Sync value prop to selected options (for controlled mode) useEffect(() => { if (isControlled) { const selected = value || []; @@ -83,11 +102,14 @@ export const MultiSelectDropdownProvider: FC< setIsOpen(value); // Call onBlur when closing the dropdown - if (wasOpen && value === false && onBlur) { - onBlur(); + if (wasOpen && value === false && onBlur && inputRef.current) { + onBlur({ + target: inputRef.current, + type: 'blur', + }); } }, - [isOpen, setIsOpen, onBlur], + [isOpen, setIsOpen, onBlur, inputRef], ); const handleSelectOption = useCallback( @@ -179,6 +201,8 @@ export const MultiSelectDropdownProvider: FC< onSelectOption: handleSelectOption, onRemoveOption: handleRemoveOption, onOpen: handleOpen, + isLoading, + noOptionsText, }} > {children} diff --git a/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.types.ts b/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.types.ts index 8f91cd85..099e8eba 100644 --- a/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.types.ts +++ b/lib/components/MultiSelectDropdown/contexts/MultiSelectDropdown.types.ts @@ -10,6 +10,8 @@ export type State = { onSelectOption: (option: MultiSelectDropdownOption) => void; onRemoveOption: (option: MultiSelectDropdownOption) => void; onOpen: (value?: boolean) => void; + isLoading?: boolean; + noOptionsText?: string; }; export type MultiSelectDropdownProviderProps = PropsWithChildren & { @@ -19,6 +21,8 @@ export type MultiSelectDropdownProviderProps = PropsWithChildren & { onChange?: (params: { target: { value: MultiSelectDropdownOption[]; name: string }; }) => void; - onBlur?: VoidFunction; + onBlur?: (event: { target: HTMLInputElement | null; type?: string }) => void; name?: string; + isLoading?: boolean; + noOptionsText?: string; };