import React, { useCallback, useRef, useState } from 'react'; import { createPortal } from 'react-dom'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { autoUpdate, flip, offset, size, useFloating, } from '@floating-ui/react'; import { TablerIconsProps } from '@tabler/icons-react'; import debounce from 'lodash.debounce'; import { ReadonlyDeep } from 'type-fest'; import type { SelectOption } from '@/spreadsheet-import/types'; import { DropdownMenuInput } from '@/ui/dropdown/components/DropdownMenuInput'; import { DropdownMenuItem } from '@/ui/dropdown/components/DropdownMenuItem'; import { DropdownMenuSelectableItem } from '@/ui/dropdown/components/DropdownMenuSelectableItem'; import { StyledDropdownMenu } from '@/ui/dropdown/components/StyledDropdownMenu'; import { StyledDropdownMenuItemsContainer } from '@/ui/dropdown/components/StyledDropdownMenuItemsContainer'; import { StyledDropdownMenuSeparator } from '@/ui/dropdown/components/StyledDropdownMenuSeparator'; import { IconChevronDown } from '@/ui/icon'; import { AppTooltip } from '@/ui/tooltip/AppTooltip'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { useUpdateEffect } from '~/hooks/useUpdateEffect'; const StyledDropdownItem = styled.div` align-items: center; background-color: ${({ theme }) => theme.background.tertiary}; border-radius: ${({ theme }) => theme.border.radius.sm}; box-sizing: border-box; display: flex; flex-direction: row; height: 32px; padding-left: ${({ theme }) => theme.spacing(2)}; padding-right: ${({ theme }) => theme.spacing(2)}; width: 100%; &:hover { background-color: ${({ theme }) => theme.background.quaternary}; } `; const StyledDropdownLabel = styled.span<{ isPlaceholder: boolean }>` color: ${({ theme, isPlaceholder }) => isPlaceholder ? theme.font.color.tertiary : theme.font.color.primary}; display: flex; flex: 1; font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.regular}; padding-left: ${({ theme }) => theme.spacing(1)}; padding-right: ${({ theme }) => theme.spacing(1)}; `; const StyledFloatingDropdown = styled.div` z-index: ${({ theme }) => theme.lastLayerZIndex}; `; interface Props { onChange: (value: ReadonlyDeep | null) => void; value?: ReadonlyDeep; options: readonly ReadonlyDeep[]; placeholder?: string; name?: string; } export const MatchColumnSelect = ({ onChange, value, options: initialOptions, placeholder, name, }: Props) => { const theme = useTheme(); const dropdownItemRef = useRef(null); const dropdownContainerRef = useRef(null); const [isOpen, setIsOpen] = useState(false); const [searchFilter, setSearchFilter] = useState(''); const [options, setOptions] = useState(initialOptions); const { refs, floatingStyles } = useFloating({ strategy: 'absolute', middleware: [ offset(() => { return parseInt(theme.spacing(2), 10); }), flip(), size(), ], whileElementsMounted: autoUpdate, open: isOpen, placement: 'bottom-start', }); const handleSearchFilterChange = useCallback( (text: string) => { setOptions( initialOptions.filter((option) => option.label.includes(text)), ); }, [initialOptions], ); const debouncedHandleSearchFilter = debounce(handleSearchFilterChange, 100, { leading: true, }); function handleFilterChange(event: React.ChangeEvent) { const value = event.currentTarget.value; setSearchFilter(value); debouncedHandleSearchFilter(value); } function handleDropdownItemClick() { setIsOpen(true); } function handleChange(option: ReadonlyDeep) { onChange(option); setIsOpen(false); } function renderIcon(icon: ReadonlyDeep) { if (icon && React.isValidElement(icon)) { return React.cloneElement(icon as any, { size: 16, color: theme.font.color.primary, }); } return null; } useListenClickOutside({ refs: [dropdownContainerRef], callback: () => { setIsOpen(false); }, }); useUpdateEffect(() => { setOptions(initialOptions); }, [initialOptions]); return ( <> { // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-expect-error dropdownItemRef.current = node; refs.setReference(node); }} onClick={handleDropdownItemClick} > {renderIcon(value?.icon)} {value?.label ?? placeholder} {isOpen && createPortal( {options?.map((option) => ( <> handleChange(option)} disabled={ option.disabled && value?.value !== option.value } > {renderIcon(option?.icon)} {option.label} {option.disabled && value?.value !== option.value && createPortal( , document.body, )} ))} {options?.length === 0 && ( No result )} , document.body, )} ); };