import type { TeactNode } from '../../../lib/teact/teact'; import React, { memo, useCallback, useEffect, useMemo, useRef, } from '../../../lib/teact/teact'; import { requestMeasure } from '../../../lib/fasterdom/fasterdom'; import buildClassName from '../../../util/buildClassName'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; import Checkbox from '../../ui/Checkbox'; import InfiniteScroll from '../../ui/InfiniteScroll'; import InputText from '../../ui/InputText'; import Loading from '../../ui/Loading'; import Radio from '../../ui/Radio'; import Icon from '../icons/Icon'; import PickerItem from './PickerItem'; import styles from './PickerStyles.module.scss'; export type ItemPickerOption = { label: TeactNode; subLabel?: string; disabled?: boolean; isLoading?: boolean; value: string; }; type SingleModeProps = { allowMultiple?: false; itemInputType?: 'radio'; selectedValue?: string; selectedValues?: never; // Help TS to throw an error if this is passed onSelectedValueChange?: (value: string) => void; }; type MultipleModeProps = { allowMultiple: true; itemInputType: 'checkbox'; selectedValue?: never; selectedValues: string[]; lockedSelectedValues?: string[]; lockedUnselectedValues?: string[]; onSelectedValuesChange?: (values: string[]) => void; }; type OwnProps = { className?: string; isSearchable?: boolean; searchInputId?: string; items: ItemPickerOption[]; itemClassName?: string; filterValue?: string; filterPlaceholder?: string; notFoundText?: string; isLoading?: boolean; noScrollRestore?: boolean; isViewOnly?: boolean; withDefaultPadding?: boolean; onFilterChange?: (value: string) => void; onDisabledClick?: (value: string, isSelected: boolean) => void; onLoadMore?: () => void; } & (SingleModeProps | MultipleModeProps); // Focus slows down animation, also it breaks transition layout in Chrome const FOCUS_DELAY_MS = 500; const ITEM_CLASS_NAME = 'ItemPickerItem'; const ItemPicker = ({ className, isSearchable, searchInputId, items, filterValue, notFoundText, isLoading, noScrollRestore, filterPlaceholder, isViewOnly, itemInputType, itemClassName, withDefaultPadding, onFilterChange, onDisabledClick, onLoadMore, ...optionalProps }: OwnProps) => { const lang = useOldLang(); // eslint-disable-next-line no-null/no-null const inputRef = useRef(null); const allowMultiple = optionalProps.allowMultiple; const lockedSelectedValues = allowMultiple ? optionalProps.lockedSelectedValues : undefined; const lockedUnselectedValues = allowMultiple ? optionalProps.lockedUnselectedValues : undefined; useEffect(() => { if (!isSearchable) return; setTimeout(() => { requestMeasure(() => { inputRef.current!.focus(); }); }, FOCUS_DELAY_MS); }, [isSearchable]); const selectedValues = useMemo(() => { if (allowMultiple) { return optionalProps.selectedValues; } return optionalProps.selectedValue ? [optionalProps.selectedValue] : MEMO_EMPTY_ARRAY; }, [allowMultiple, optionalProps.selectedValue, optionalProps.selectedValues]); const lockedSelectedValuesSet = useMemo(() => new Set(lockedSelectedValues), [lockedSelectedValues]); const lockedUnselectedValuesSet = useMemo(() => new Set(lockedUnselectedValues), [lockedUnselectedValues]); const sortedItemValuesList = useMemo(() => { if (filterValue) { return items.map((item) => item.value); } const lockedSelectedBucket: ItemPickerOption[] = []; const unlockedBucket: ItemPickerOption[] = []; const lockedUnselectableBucket: ItemPickerOption[] = []; items.forEach((item) => { if (lockedSelectedValuesSet.has(item.value)) { lockedSelectedBucket.push(item); } else if (lockedUnselectedValuesSet.has(item.value)) { lockedUnselectableBucket.push(item); } else { unlockedBucket.push(item); } }); return lockedSelectedBucket.concat(unlockedBucket, lockedUnselectableBucket).map((item) => item.value); }, [filterValue, items, lockedSelectedValuesSet, lockedUnselectedValuesSet]); const handleItemClick = useLastCallback((value: string) => { if (allowMultiple) { const newSelectedValues = selectedValues.slice(); const index = newSelectedValues.indexOf(value); if (index >= 0) { newSelectedValues.splice(index, 1); } else { newSelectedValues.push(value); } optionalProps.onSelectedValuesChange?.(newSelectedValues); return; } optionalProps.onSelectedValueChange?.(value); }); const [viewportValuesList, getMore] = useInfiniteScroll( onLoadMore, sortedItemValuesList, Boolean(filterValue), ); const handleFilterChange = useLastCallback((e: React.ChangeEvent) => { const { value } = e.currentTarget; onFilterChange?.(value); }); const renderItem = useCallback((value: string) => { const item = items.find((itemOption) => itemOption.value === value); if (!item) return undefined; const { label, subLabel, isLoading: isItemLoading } = item; const isAlwaysUnselected = lockedUnselectedValuesSet.has(value); const isAlwaysSelected = lockedSelectedValuesSet.has(value); const isLocked = isAlwaysUnselected || isAlwaysSelected; const isChecked = selectedValues.includes(value); function getInputElement() { if (isLocked) return ; if (itemInputType === 'radio') { return ; } if (itemInputType === 'checkbox') { return ; } return undefined; } return ( handleItemClick(value)} // eslint-disable-next-line react/jsx-no-bind onDisabledClick={onDisabledClick && (() => onDisabledClick(value, isAlwaysSelected))} /> ); }, [ items, lockedUnselectedValuesSet, lockedSelectedValuesSet, selectedValues, isViewOnly, onDisabledClick, itemInputType, itemClassName, ]); return (
{isSearchable && (
)} {viewportValuesList?.length ? ( {viewportValuesList.map((value) => renderItem(value))} ) : !isLoading && viewportValuesList && !viewportValuesList.length ? (

{notFoundText || lang('SearchEmptyViewTitle')}

) : ( )}
); }; export default memo(ItemPicker);