/* global document */ import React, { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, Fragment, } from "react"; import { findNodeHandle, ViewStyle, Pressable } from "react-native"; import theme from "mazlo-theme"; import useScroll from "../../hooks/useScroll"; import { Checkbox } from "../Checkbox"; import { Icon } from "../Icon"; import { Input } from "../Input"; import { Portal as MenuPortal } from "../Portal"; import { Text } from "../Text"; import { View } from "../View"; import styles from "./styles"; export type SelectOption = { id?: string; name: string; value: string; content?: any; }; type Offset = { x?: number; y?: number; }; export type Props = { alignHorizontal?: "left" | "right"; alignVertical?: "top" | "bottom"; bordered?: boolean; contained?: boolean; headerComponent?: React.ReactNode; label?: string; menuStyle?: ViewStyle; multiple?: boolean; offset?: Offset; open?: boolean; options?: SelectOption[]; search?: boolean; size?: string; toggleComponent?: React.ReactNode; value?: string | string[]; emptyText?: string; fetchOptions?: () => Promise; onSelect: (selected: string | string[]) => void; renderOption?: (item: SelectOption) => React.ReactNode; renderSelectedOption?: (option: SelectOption) => React.ReactNode; dark?: boolean; decorated?: boolean; disabled?: boolean; inline?: boolean; }; type SelectRowProps = { multiple: boolean; option: SelectOption; selected: boolean; size?: string; onChange: (value: string, selected: boolean) => void; renderOption?: (option: SelectOption) => React.ReactNode; }; const trueFn = () => true; const SelectRow = ({ multiple, option, selected, size, onChange, renderOption, }: SelectRowProps) => { const toggle = useCallback(() => { onChange(option.value, !selected); }, [option.value, selected, onChange]); return ( [ styles.optionContainer, hovered && styles.hoveredOptionContainer, selected && styles.selectedOptionContainer, ]} > {multiple && ( )} {renderOption ? ( renderOption(option) ) : ( {option.name} )} ); }; const Select = ({ alignHorizontal = "left", alignVertical = "bottom", bordered = true, contained = false, emptyText = "No options found", label, fetchOptions, headerComponent, menuStyle, multiple, onSelect, options: optionsProp = [], open, offset, renderOption, search, size = "sm", toggleComponent, value: valueProp, dark, decorated, renderSelectedOption, disabled, inline = false, }: Props) => { const node = useRef(); const menuNode = useRef(); const { x: offsetLeft = 0, y: offsetTop = 0 } = offset || {}; const [showOptions, setShowOptions] = useState(false); const menuOpen = open == null ? showOptions : open; const [layout, setLayout] = useState({ width: 0, height: 0, x: 0, y: 0 }); const { scrollX, scrollY } = useScroll(); useLayoutEffect(() => { const nodeHandle = findNodeHandle(node.current) as any; if (nodeHandle) { setLayout(nodeHandle.getBoundingClientRect().toJSON()); } }, [menuOpen, node.current, scrollX, scrollY]); const [options, setOptions] = useState(optionsProp); useEffect(() => setOptions(optionsProp), [optionsProp]); const values = [] .concat(valueProp) .filter((value) => options.find((option) => option.value === value)); const selectedOptions = useMemo( () => values .map((value) => options.find((option) => option.value === value)) .filter(Boolean), [values, options] ); const [searchText, setSearchText] = useState(""); const filteredOptions = useMemo(() => { if (!searchText) return options; const text = searchText.toLowerCase(); return options.filter((option) => option.name.toLowerCase().includes(text)); }, [options, searchText]); const selectOption = useCallback( (value, selected) => { const newValues = selected ? [...values, value] : values.filter((item) => item !== value); onSelect(multiple ? newValues : newValues.pop()); if (!multiple) { setShowOptions(false); } }, [multiple, values, onSelect] ); const handleMouseDown = useCallback( (e) => { const currentNode = findNodeHandle(node.current) as any; const currentMenu = findNodeHandle(menuNode.current) as any; if ( currentNode && !currentNode.contains(e.target) && currentMenu && !currentMenu.contains(e.target) ) { setShowOptions(false); } }, [node.current, menuNode.current] ); useEffect(() => { if (open == null) { if (menuOpen) { document.addEventListener("mousedown", handleMouseDown); } else { document.removeEventListener("mousedown", handleMouseDown); } return () => { document.removeEventListener("mousedown", handleMouseDown); }; } }, [open, menuOpen]); useEffect(() => { if (fetchOptions) { fetchOptions().then(setOptions); } }, [fetchOptions]); const useLabel = label && !selectedOptions.length; const MenuContainer = useMemo(() => (inline ? Fragment : MenuPortal), [ inline, ]); const toggleHandler = useCallback( () => setShowOptions((value) => !value), [] ); return ( {toggleComponent !== null && ( { e?.preventDefault(); }} // @ts-ignore onStartShouldSetResponderCapture={trueFn} > {toggleComponent || ( {!useLabel && ( <> {multiple && ( {selectedOptions.length} )} {multiple ? "Selected" : selectedOptions.length ? renderSelectedOption?.(selectedOptions[0]) || selectedOptions[0]?.name || "" : ""} )} {useLabel && ( <> {label} )} {menuOpen && decorated && ( )} )} )} {menuOpen && ( {search && ( )} {headerComponent} {filteredOptions.length ? ( filteredOptions.map((option: SelectOption) => ( )) ) : ( {emptyText} )} )} ); }; export default Select;