From 9de1e255a1a13ce02aff4ce20aa2e0887e24a6f0 Mon Sep 17 00:00:00 2001 From: Maksim Sviridov Date: Tue, 11 Jul 2023 16:07:01 +0300 Subject: [PATCH] fix(ComboBox): generic type of triggerRef --- src/components/ComboBox.tsx | 285 ++++++++++++++++++------------------ 1 file changed, 145 insertions(+), 140 deletions(-) diff --git a/src/components/ComboBox.tsx b/src/components/ComboBox.tsx index 31fc4ca8..a60c61e8 100644 --- a/src/components/ComboBox.tsx +++ b/src/components/ComboBox.tsx @@ -11,10 +11,10 @@ import { flatten } from '../utils/flatten'; import { Popup } from './Popup'; -interface ComboBoxTriggerProps { - text: ComboBoxProps['text']; - value: ComboBoxProps['value']; - ref: React.Ref; +interface ComboBoxTriggerProps = React.Ref> { + text: ComboBoxProps['text']; + value: ComboBoxProps['value']; + ref: R; disabled?: boolean; onClick: () => void; @@ -34,10 +34,10 @@ interface ComboBoxItemProps { onClick: (value?: any) => void; } -interface ComboBoxProps { +interface ComboBoxProps { renderInput: (props: ComboBoxInputProps) => React.ReactNode; renderItem: (props: ComboBoxItemProps) => React.ReactNode | Record; - renderTrigger?: (props: ComboBoxTriggerProps) => React.ReactNode; + renderTrigger?: (props: ComboBoxTriggerProps) => React.ReactNode; renderItems?: (children: React.ReactNode | Array> | undefined) => React.ReactNode; text?: string; value?: any; @@ -73,145 +73,150 @@ const StyledErrorTrigger = styled.div` z-index: 1; `; -export const ComboBox = React.forwardRef( - ( - { - text, - value, - visible = false, - items = [], - disabled, - error, - maxWidth = 250, - minWidth = 150, - className, - placement = 'bottom-start', - offset = [-4, 8], - renderItem, - renderTrigger, - renderInput, - renderItems, - onChange, - onClose, - onClickOutside, +export function ComboBoxRenderFunction( + props: ComboBoxProps, + ref: React.ForwardedRef, +): React.ReactElement { + const { + text, + value, + visible = false, + items = [], + disabled, + error, + maxWidth = 250, + minWidth = 150, + className, + placement = 'bottom-start', + offset = [-4, 8], + renderItem, + renderTrigger, + renderInput, + renderItems, + onChange, + onClose, + onClickOutside, + } = props; + const popupContentRef = useRef(null); + const popupRef = useRef(null); + const triggerRef = useRef(null); + const inputRef = useRef(null); + const [popupVisible, setPopupVisibility] = useState(visible); + const [editMode, setEditMode] = useState(false); + const downPress = useKeyPress('ArrowDown'); + const upPress = useKeyPress('ArrowUp'); + const [cursor, setCursor] = useState(0); + const flatItems = useMemo(() => flatten(items), [items]); + + useEffect(() => { + setPopupVisibility(visible); + }, [visible]); + + useEffect(() => { + if (renderTrigger) { + setPopupVisibility(editMode); + } + }, [renderTrigger, editMode]); + + const onTriggerClick = useCallback(() => { + setEditMode(true); + }, []); + + const onItemClick = useCallback( + (value: any) => () => { + setEditMode(false); + onChange?.(value); }, - ref, - ) => { - const popupContentRef = useRef(null); - const popupRef = useRef(null); - const buttonRef = useRef(null); - const inputRef = useRef(null); - const [popupVisible, setPopupVisibility] = useState(visible); - const [editMode, setEditMode] = useState(false); - const downPress = useKeyPress('ArrowDown'); - const upPress = useKeyPress('ArrowUp'); - const [cursor, setCursor] = useState(0); - const flatItems = useMemo(() => flatten(items), [items]); - - useEffect(() => { - setPopupVisibility(visible); - }, [visible]); - - useEffect(() => { - if (renderTrigger) { - setPopupVisibility(editMode); - } - }, [renderTrigger, editMode]); - - const onTriggerClick = useCallback(() => { - setEditMode(true); - }, []); - - const onItemClick = useCallback( - (value: any) => () => { + [onChange], + ); + + const [onESC] = useKeyboard([KeyCode.Escape], () => { + setEditMode(false); + onClose?.(); + }); + + const [onENTER] = useKeyboard([KeyCode.Enter], () => { + onItemClick(flatItems[cursor])(); + }); + + useEffect(() => { + if (flatItems.length && downPress) { + setCursor((prevState) => (prevState < flatItems.length - 1 ? prevState + 1 : prevState)); + } + }, [flatItems, downPress]); + + useEffect(() => { + if (flatItems.length && upPress) { + setCursor((prevState) => (prevState > 0 ? prevState - 1 : prevState)); + } + }, [flatItems, upPress]); + + const onErrorMouseEnter = useCallback(() => setPopupVisibility(true), []); + const onErrorMouseLeave = useCallback(() => setPopupVisibility(false), []); + + useClickOutside(inputRef, (e) => { + onClickOutside?.(() => { + // popup is outside of component + if (!popupContentRef.current?.contains(e.target as Node)) { setEditMode(false); - onChange?.(value); - }, - [onChange], - ); - - const [onESC] = useKeyboard([KeyCode.Escape], () => { - setEditMode(false); - onClose?.(); - }); - - const [onENTER] = useKeyboard([KeyCode.Enter], () => { - onItemClick(flatItems[cursor])(); - }); - - useEffect(() => { - if (flatItems.length && downPress) { - setCursor((prevState) => (prevState < flatItems.length - 1 ? prevState + 1 : prevState)); - } - }, [flatItems, downPress]); - - useEffect(() => { - if (flatItems.length && upPress) { - setCursor((prevState) => (prevState > 0 ? prevState - 1 : prevState)); + onClose?.(); } - }, [flatItems, upPress]); - - const onErrorMouseEnter = useCallback(() => setPopupVisibility(true), []); - const onErrorMouseLeave = useCallback(() => setPopupVisibility(false), []); - - useClickOutside(inputRef, (e) => { - onClickOutside?.(() => { - // popup is outside of component - if (!popupContentRef.current?.contains(e.target as Node)) { - setEditMode(false); - onClose?.(); - } - }); }); - - const children = flatItems.map((item: any, index: number) => - renderItem({ item, index, cursor, onClick: onItemClick(item) }), - ); - - return ( - - {nullable(error, (err) => ( + }); + + const children = flatItems.map((item: any, index: number) => + renderItem({ item, index, cursor, onClick: onItemClick(item) }), + ); + + return ( + + {nullable(error, (err) => ( + <> + + + {err.message} + + + ))} + + + {renderTrigger ? ( <> - - - {err.message} - + {editMode + ? renderInput({ value, disabled, ref: inputRef, ...onENTER }) + : renderTrigger({ text, value, disabled, ref: triggerRef, onClick: onTriggerClick })} - ))} - - - {renderTrigger ? ( - <> - {editMode - ? renderInput({ value, disabled, ref: inputRef, ...onENTER }) - : renderTrigger({ text, value, disabled, ref: buttonRef, onClick: onTriggerClick })} - - ) : ( - renderInput({ value, disabled, ref: inputRef, ...onENTER }) - )} - - - -
- {renderItems ? renderItems(children as React.ReactNode) : (children as React.ReactNode)} -
-
-
- ); - }, -); + ) : ( + renderInput({ value, disabled, ref: inputRef, ...onENTER }) + )} + + + +
+ {renderItems ? renderItems(children as React.ReactNode) : (children as React.ReactNode)} +
+
+
+ ); +} + +export const ComboBox = ( + props: React.PropsWithoutRef> & React.RefAttributes, +) => { + return React.forwardRef(ComboBoxRenderFunction)(props); +}; export default ComboBox;