From 7d2f83909b76be814f3033cb5f05988b5119f303 Mon Sep 17 00:00:00 2001 From: Karelian Pie Date: Tue, 14 Nov 2023 19:42:58 +0200 Subject: [PATCH] refactor: Fewer clicks to select a single network --- .../common/components/MultiSelectDropdown.tsx | 155 ++++++++++++------ 1 file changed, 101 insertions(+), 54 deletions(-) diff --git a/apps/common/components/MultiSelectDropdown.tsx b/apps/common/components/MultiSelectDropdown.tsx index c0bc57e8f..c2cb4ad4c 100755 --- a/apps/common/components/MultiSelectDropdown.tsx +++ b/apps/common/components/MultiSelectDropdown.tsx @@ -1,4 +1,4 @@ -import {Fragment, useEffect, useRef, useState} from 'react'; +import {Fragment, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {Combobox, Transition} from '@headlessui/react'; import {useClickOutside, useThrottledState} from '@react-hookz/web'; import {Renderable} from '@yearn-finance/web-lib/components/Renderable'; @@ -13,6 +13,8 @@ export type TMultiSelectOptionProps = { value: number | string; isSelected: boolean; icon?: ReactElement; + onCheckboxClick?: (event: React.MouseEvent) => void; + onContainerClick?: (event: React.MouseEvent) => void; }; type TMultiSelectProps = { @@ -44,6 +46,7 @@ function SelectAllOption(option: TMultiSelectOptionProps): ReactElement { function Option(option: TMultiSelectOptionProps): ReactElement { return (
@@ -56,6 +59,10 @@ function Option(option: TMultiSelectOptionProps): ReactElement { checked={option.isSelected} onChange={(): void => {}} className={'checkbox'} + onClick={(event: React.MouseEvent): void => { + event.stopPropagation(); + option.onCheckboxClick?.(event); + }} readOnly />
@@ -95,62 +102,115 @@ function DropdownEmpty({query}: {query: string}): ReactElement { ); } -export function MultiSelectDropdown(props: TMultiSelectProps): ReactElement { +const getFilteredOptions = ({ + query, + currentOptions +}: { + query: string; + currentOptions: TMultiSelectOptionProps[]; +}): TMultiSelectOptionProps[] => { + if (query === '') { + return currentOptions; + } + + return currentOptions.filter((option): boolean => { + return option.label.toLowerCase().includes(query.toLowerCase()); + }); +}; + +export function MultiSelectDropdown({options, onSelect, placeholder = '', ...props}: TMultiSelectProps): ReactElement { const [isOpen, set_isOpen] = useThrottledState(false, 400); - const [currentOptions, set_currentOptions] = useState(props.options); + const [currentOptions, set_currentOptions] = useState(options); const [areAllSelected, set_areAllSelected] = useState(false); const [query, set_query] = useState(''); const componentRef = useRef(null); useEffect((): void => { - set_currentOptions(props.options); - }, [props.options]); + set_currentOptions(options); + }, [options]); useEffect((): void => { - set_areAllSelected(currentOptions.every((option): boolean => option.isSelected)); + set_areAllSelected(currentOptions.every(({isSelected}): boolean => isSelected)); }, [currentOptions]); useClickOutside(componentRef, (): void => { set_isOpen(false); }); - const filteredOptions = - query === '' - ? currentOptions - : currentOptions.filter((option): boolean => { - return option.label.toLowerCase().includes(query.toLowerCase()); - }); + const filteredOptions = useMemo( + (): TMultiSelectOptionProps[] => getFilteredOptions({query, currentOptions}), + [currentOptions, query] + ); + + const getDisplayName = useCallback( + (options: TMultiSelectOptionProps[]): string => { + if (areAllSelected) { + return 'All'; + } + + const selectedOptions = options.filter(({isSelected}): boolean => isSelected); + + if (selectedOptions.length === 0) { + return placeholder; + } + + if (selectedOptions.length === 1) { + return selectedOptions[0].label; + } + + return 'Multiple'; + }, + [areAllSelected, placeholder] + ); + + const handleOnCheckboxClick = useCallback( + ({value}: TMultiSelectOptionProps): void => { + const currentState = currentOptions.map( + (o): TMultiSelectOptionProps => (o.value === value ? {...o, isSelected: !o.isSelected} : o) + ); + set_areAllSelected(!currentState.some(({isSelected}): boolean => !isSelected)); + set_currentOptions(currentState); + onSelect(currentState); + }, + [currentOptions, onSelect] + ); + + const handleOnContainerClick = useCallback( + ({value}: TMultiSelectOptionProps): void => { + const currentState = currentOptions.map( + (o): TMultiSelectOptionProps => + o.value === value ? {...o, isSelected: true} : {...o, isSelected: false} + ); + set_areAllSelected(false); + onSelect(currentState); + }, + [currentOptions, onSelect] + ); return ( { - // Hack(ish) because with this Combobox component we cannot unselect items + // Just used for the select/desect all options const lastIndex = options.length - 1; const elementSelected = options[lastIndex]; - const currentElements = options.slice(0, lastIndex); - let currentState: TMultiSelectOptionProps[] = []; - - if (elementSelected.value === 'select_all') { - currentState = currentElements.map((option): TMultiSelectOptionProps => { - return { - ...option, - isSelected: !elementSelected.isSelected - }; - }); - set_areAllSelected(!elementSelected.isSelected); - } else { - currentState = currentElements.map((option): TMultiSelectOptionProps => { - return option.value === elementSelected.value - ? {...option, isSelected: !option.isSelected} - : option; - }); - set_areAllSelected(!currentState.some((option): boolean => !option.isSelected)); + + if (elementSelected.value !== 'select_all') { + return; } + const currentElements = options.slice(0, lastIndex); + const currentState = currentElements.map( + (option): TMultiSelectOptionProps => ({ + ...option, + isSelected: !elementSelected.isSelected + }) + ); + + set_areAllSelected(!elementSelected.isSelected); set_currentOptions(currentState); - props.onSelect(currentState); + onSelect(currentState); }} multiple>
@@ -163,27 +223,12 @@ export function MultiSelectDropdown(props: TMultiSelectProps): ReactElement { !option.isSelected) + options.every(({isSelected}): boolean => !isSelected) ? 'text-neutral-400' : 'text-neutral-900' )} - displayValue={(options: TMultiSelectOptionProps[]): string => { - const selectedOptions = options.filter((option): boolean => option.isSelected); - if (selectedOptions.length === 0) { - return props.placeholder || ''; - } - - if (selectedOptions.length === 1) { - return selectedOptions[0].label; - } - - if (areAllSelected) { - return 'All'; - } - - return 'Multiple'; - }} - placeholder={props.placeholder || ''} + displayValue={getDisplayName} + placeholder={placeholder} spellCheck={false} onChange={(event): void => set_query(event.target.value)} /> @@ -218,14 +263,16 @@ export function MultiSelectDropdown(props: TMultiSelectProps): ReactElement { 0} fallback={}> - {filteredOptions.map((option): ReactElement => { - return ( + {filteredOptions.map( + (option): ReactElement => (