Skip to content

Commit

Permalink
refactor: Fewer clicks to select a single network
Browse files Browse the repository at this point in the history
  • Loading branch information
karelianpie authored and Majorfi committed Nov 15, 2023
1 parent ca2fe60 commit 7d2f839
Showing 1 changed file with 101 additions and 54 deletions.
155 changes: 101 additions & 54 deletions apps/common/components/MultiSelectDropdown.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -13,6 +13,8 @@ export type TMultiSelectOptionProps = {
value: number | string;
isSelected: boolean;
icon?: ReactElement;
onCheckboxClick?: (event: React.MouseEvent<HTMLElement>) => void;
onContainerClick?: (event: React.MouseEvent<HTMLElement>) => void;
};

type TMultiSelectProps = {
Expand Down Expand Up @@ -44,6 +46,7 @@ function SelectAllOption(option: TMultiSelectOptionProps): ReactElement {
function Option(option: TMultiSelectOptionProps): ReactElement {
return (
<Combobox.Option
onClick={option.onContainerClick}
value={option}
className={'transition-colors hover:bg-neutral-100'}>
<div className={'flex w-full items-center justify-between p-2'}>
Expand All @@ -56,6 +59,10 @@ function Option(option: TMultiSelectOptionProps): ReactElement {
checked={option.isSelected}
onChange={(): void => {}}
className={'checkbox'}
onClick={(event: React.MouseEvent<HTMLElement>): void => {
event.stopPropagation();
option.onCheckboxClick?.(event);
}}
readOnly
/>
</div>
Expand Down Expand Up @@ -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<TMultiSelectOptionProps[]>(props.options);
const [currentOptions, set_currentOptions] = useState<TMultiSelectOptionProps[]>(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 (
<Combobox
ref={componentRef}
value={currentOptions}
onChange={(options): void => {
// 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>
<div className={'relative w-full'}>
Expand All @@ -163,27 +223,12 @@ export function MultiSelectDropdown(props: TMultiSelectProps): ReactElement {
<Combobox.Input
className={cl(
'w-full cursor-default overflow-x-scroll border-none bg-transparent p-0 outline-none scrollbar-none',
props.options.every((option): boolean => !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)}
/>
Expand Down Expand Up @@ -218,14 +263,16 @@ export function MultiSelectDropdown(props: TMultiSelectProps): ReactElement {
<Renderable
shouldRender={filteredOptions.length > 0}
fallback={<DropdownEmpty query={query} />}>
{filteredOptions.map((option): ReactElement => {
return (
{filteredOptions.map(
(option): ReactElement => (
<Option
key={option.value}
onCheckboxClick={(): void => handleOnCheckboxClick(option)}
onContainerClick={(): void => handleOnContainerClick(option)}
{...option}
/>
);
})}
)
)}
</Renderable>
</Combobox.Options>
</Transition>
Expand Down

0 comments on commit 7d2f839

Please sign in to comment.