downshift-js / downshift

🏎 A set of primitives to build simple, flexible, WAI-ARIA compliant React autocomplete, combobox or select dropdown components.

Home Page:http://downshift-js.com/

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Selecting and de-selecting same item "onSelectedItemChange" not fired

Hmoulvad opened this issue · comments

  • downshift version: 7.6.2
  • node version: 18.18.0
  • npm (or yarn) version: 9.8.1
import { useCombobox } from 'downshift';
import { useEffect, useMemo, useState } from 'react';
import ChevronDown from '~/icons/iconography/chevron/down.svg';
import ClearIcon from '~/icons/iconography/close.svg';
import LoopIcon from '~/icons/search.svg';
import { DropdownItem } from './models';
import {
    ClearButton,
    Input,
    InputContainer,
    Item,
    ItemCheckbox,
    ItemCount,
    ItemLabel,
    ItemLeftContainer,
    List,
    SearchFilter,
    StyledButton,
} from './styled';

type MultipleSelectorProps = {
    options: DropdownItem[];
    initialSelectedItems?: string[];
    label: string;
    noLabel?: boolean;
    id: string;
    onSelectedItemsChange?: (items: DropdownItem[]) => void;
};

const MultipleSelector = ({
    options,
    label,
    id,
    initialSelectedItems,
    noLabel = false,
    onSelectedItemsChange,
}: MultipleSelectorProps) => {
    const [inputValue, setInputValue] = useState<string>('');
    const [selectedItems, setSelectedItems] = useState<DropdownItem[]>(
        options.filter((item) => initialSelectedItems?.includes(item.value)),
    );
    const items = useMemo(() => {
        return options.filter((item) =>
            item.label.toLowerCase().includes(inputValue?.toLowerCase() ?? ''),
        );
    }, [options, inputValue]);

    const {
        getItemProps,
        getMenuProps,
        getToggleButtonProps,
        getLabelProps,
        getInputProps,
        highlightedIndex,
        isOpen,
    } = useCombobox({
        items,
        itemToString: (item) => item?.label ?? '',
        id,
        inputValue,
        stateReducer: ({ inputValue, highlightedIndex }, { changes, type }) => {
            switch (type) {
                case useCombobox.stateChangeTypes.ItemClick:
                case useCombobox.stateChangeTypes.InputKeyDownEnter:
                    return {
                        ...changes,
                        inputValue,
                        isOpen: true,
                        highlightedIndex,
                    };
                default:
                    return changes;
            }
        },
        onSelectedItemChange: ({ selectedItem }) => {
            if (!selectedItem) {
                return;
            }

            const itemIsDeselected = selectedItems.some(
                (item) => item.value === selectedItem.value,
            );

            const newSelectedItemList = itemIsDeselected
                ? selectedItems.filter((item) => item.value !== selectedItem.value)
                : [...selectedItems, selectedItem];

            setSelectedItems(newSelectedItemList);
            onSelectedItemsChange?.(newSelectedItemList);
        },
    });

    // Update selectedItems state on props change
    useEffect(() => {
        setSelectedItems(items.filter((item) => initialSelectedItems?.includes(item.value)));
    }, [initialSelectedItems, items]);

    const isListHidden = !isOpen && !noLabel;
    const isSearchFilterHidden = items.length < 8 || isListHidden;

    return (
        <>
            <StyledButton
                isOpen={isOpen}
                noLabel={noLabel}
                {...getToggleButtonProps()}
                tabIndex={0}
            >
                <span {...getLabelProps()}>{label}</span>
                <ChevronDown />
            </StyledButton>

            <List
                aria-hidden={isListHidden}
                isHidden={isListHidden}
                noLabel={noLabel}
                {...getMenuProps()}
            >
                <SearchFilter aria-hidden={isSearchFilterHidden} isHidden={isSearchFilterHidden}>
                    <InputContainer>
                        <LoopIcon />
                        <Input type="text" placeholder={`Søg i ${label}`} {...getInputProps()} />
                        {inputValue ? (
                            <ClearButton onClick={() => setInputValue('')}>
                                <ClearIcon />
                            </ClearButton>
                        ) : null}
                    </InputContainer>
                </SearchFilter>
                {items.map((item, index) => (
                    <Item
                        highlighted={highlightedIndex === index}
                        key={item.label}
                        {...getItemProps({
                            item,
                            index,
                        })}
                    >
                        <ItemLeftContainer>
                            <ItemCheckbox
                                type="checkbox"
                                checked={selectedItems.some(
                                    (selected) => selected.value === item.value,
                                )}
                                readOnly
                                value={item.value}
                            />
                            <ItemLabel>{item.label}</ItemLabel>
                        </ItemLeftContainer>

                        {item.count ? <ItemCount>({item.count})</ItemCount> : null}
                    </Item>
                ))}
            </List>
        </>
    );
};

export default MultipleSelector;

What you did:
I selected and option and tried to de-select it.

What happened:
The function callback "onSelectedItemChange" doesn't fire since there is no changes in selected item, but I want it to fire so I can de-select the item.

Help:
Does anyone have an idea to get around this... <3

Hi @Hmoulvad ! With https://github.com/downshift-js/downshift/tree/master/src/hooks/useCombobox#statereducer I believe is the easiest.

    stateReducer(state, actionAndChanges) {
      const {type, changes} = actionAndChanges
      // this prevents the menu from being closed when the user selects an item with 'Enter' or mouse
      switch (type) {
        case useCombobox.stateChangeTypes.InputKeyDownEnter:
        case useCombobox.stateChangeTypes.ItemClick: {
          if (changes.selectedItem === state.selectedItem) {
            return {...changes, selectedItem: null, inputValue: ''}
          }

          return changes
        }
        default:
          return changes // otherwise business as usual.
      }
    },

Hi @Hmoulvad ! With https://github.com/downshift-js/downshift/tree/master/src/hooks/useCombobox#statereducer I believe is the easiest.

    stateReducer(state, actionAndChanges) {
      const {type, changes} = actionAndChanges
      // this prevents the menu from being closed when the user selects an item with 'Enter' or mouse
      switch (type) {
        case useCombobox.stateChangeTypes.InputKeyDownEnter:
        case useCombobox.stateChangeTypes.ItemClick: {
          if (changes.selectedItem === state.selectedItem) {
            return {...changes, selectedItem: null, inputValue: ''}
          }

          return changes
        }
        default:
          return changes // otherwise business as usual.
      }
    },

Thanks will check it out!