From e3a048790db4294e2a99cfb36e4695eab7dcb8e2 Mon Sep 17 00:00:00 2001 From: Mark McDowall Date: Mon, 10 Mar 2025 16:59:46 -0700 Subject: [PATCH] Use floading UI for EnhancedSelectInput --- .../Form/Select/EnhancedSelectInput.tsx | 366 +++++++----------- 1 file changed, 146 insertions(+), 220 deletions(-) diff --git a/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx b/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx index abf6a34a7..bfb052ed6 100644 --- a/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx +++ b/frontend/src/Components/Form/Select/EnhancedSelectInput.tsx @@ -1,36 +1,38 @@ +import { + autoUpdate, + flip, + FloatingPortal, + size, + useClick, + useDismiss, + useFloating, + useInteractions, +} from '@floating-ui/react'; import classNames from 'classnames'; import React, { ElementType, KeyboardEvent, ReactNode, useCallback, - useEffect, useMemo, - useRef, useState, } from 'react'; -import { Manager, Popper, Reference } from 'react-popper'; import Icon from 'Components/Icon'; import Link from 'Components/Link/Link'; import LoadingIndicator from 'Components/Loading/LoadingIndicator'; import Modal from 'Components/Modal/Modal'; import ModalBody from 'Components/Modal/ModalBody'; -import Portal from 'Components/Portal'; import Scroller from 'Components/Scroller/Scroller'; -import useMeasure from 'Helpers/Hooks/useMeasure'; import { icons } from 'Helpers/Props'; import ArrayElement from 'typings/Helpers/ArrayElement'; import { EnhancedSelectInputChanged, InputChanged } from 'typings/inputs'; import { isMobile as isMobileUtil } from 'Utilities/browser'; import * as keyCodes from 'Utilities/Constants/keyCodes'; -import getUniqueElementId from 'Utilities/getUniqueElementId'; import TextInput from '../TextInput'; import HintedSelectInputOption from './HintedSelectInputOption'; import HintedSelectInputSelectedValue from './HintedSelectInputSelectedValue'; import styles from './EnhancedSelectInput.css'; -const MINIMUM_DISTANCE_FROM_EDGE = 10; - function isArrowKey(keyCode: number) { return keyCode === keyCodes.UP_ARROW || keyCode === keyCodes.DOWN_ARROW; } @@ -162,10 +164,6 @@ function EnhancedSelectInput, V>( onOpen, } = props; - const [measureRef, { width }] = useMeasure(); - const updater = useRef<(() => void) | null>(null); - const buttonId = useMemo(() => getUniqueElementId(), []); - const optionsId = useMemo(() => getUniqueElementId(), []); const [selectedIndex, setSelectedIndex] = useState( getSelectedIndex(value, values) ); @@ -175,6 +173,32 @@ function EnhancedSelectInput, V>( const isMultiSelect = Array.isArray(value); const selectedOption = getSelectedOption(selectedIndex, values); + const { refs, context, floatingStyles } = useFloating({ + middleware: [ + flip({ + crossAxis: false, + mainAxis: true, + }), + size({ + apply({ rects, elements }) { + Object.assign(elements.floating.style, { + 'min-width': `${rects.reference.width}px`, + }); + }, + }), + ], + placement: 'bottom-start', + whileElementsMounted: autoUpdate, + }); + + const click = useClick(context); + const dismiss = useDismiss(context); + + const { getReferenceProps, getFloatingProps } = useInteractions([ + click, + dismiss, + ]); + const selectedValue = useMemo(() => { if (values.length) { return value; @@ -189,46 +213,6 @@ function EnhancedSelectInput, V>( return ''; }, [value, values, isMultiSelect]); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const handleComputeMaxHeight = useCallback((data: any) => { - const windowHeight = window.innerHeight; - - data.styles.maxHeight = windowHeight - MINIMUM_DISTANCE_FROM_EDGE; - - return data; - }, []); - - const handleWindowClick = useCallback( - (event: MouseEvent) => { - const button = document.getElementById(buttonId); - const options = document.getElementById(optionsId); - const eventTarget = event.target as HTMLElement; - - if (!button || !eventTarget.isConnected || isMobile) { - return; - } - - if ( - !button.contains(eventTarget) && - options && - !options.contains(eventTarget) && - isOpen - ) { - setIsOpen(false); - window.removeEventListener('click', handleWindowClick); - } - }, - [isMobile, isOpen, buttonId, optionsId, setIsOpen] - ); - - const addListener = useCallback(() => { - window.addEventListener('click', handleWindowClick); - }, [handleWindowClick]); - - const removeListener = useCallback(() => { - window.removeEventListener('click', handleWindowClick); - }, [handleWindowClick]); - const handlePress = useCallback(() => { if (!isOpen && onOpen) { onOpen(); @@ -292,10 +276,9 @@ function EnhancedSelectInput, V>( const handleFocus = useCallback(() => { if (isOpen) { - removeListener(); setIsOpen(false); } - }, [isOpen, setIsOpen, removeListener]); + }, [isOpen, setIsOpen]); const handleKeyDown = useCallback( (event: KeyboardEvent) => { @@ -389,176 +372,119 @@ function EnhancedSelectInput, V>( [onChange] ); - useEffect(() => { - if (updater.current) { - updater.current(); - } - }); - - useEffect(() => { - if (isOpen) { - addListener(); - } else { - removeListener(); - } - - return removeListener; - }, [isOpen, addListener, removeListener]); - return ( -
- - - {({ ref }) => ( -
-
- {isEditable && typeof value === 'string' ? ( -
- - - {isFetching ? ( - - ) : null} + <> +
+ {isEditable && typeof value === 'string' ? ( +
+ + + {isFetching ? ( + + ) : null} - {isFetching ? null : } - -
- ) : ( - - - {selectedOption ? selectedOption.value : selectedValue} - - -
- {isFetching ? ( - - ) : null} - - {isFetching ? null : } -
- - )} -
-
- )} - - - } + +
+ ) : ( + - {({ ref, style, scheduleUpdate }) => { - updater.current = scheduleUpdate; + + {selectedOption ? selectedOption.value : selectedValue} + - return ( -
- {isOpen && !isMobile ? ( - + {isFetching ? ( + + ) : null} + + {isFetching ? null : } +
+ + )} +
+ {isOpen ? ( + +
+ {isOpen && !isMobile ? ( + + {values.map((v, index) => { + const hasParent = v.parentKey !== undefined; + const depth = hasParent ? 1 : 0; + const parentSelected = + v.parentKey !== undefined && + Array.isArray(value) && + value.includes(v.parentKey); + + const { key, ...other } = v; + + return ( + - {values.map((v, index) => { - const hasParent = v.parentKey !== undefined; - const depth = hasParent ? 1 : 0; - const parentSelected = - v.parentKey !== undefined && - Array.isArray(value) && - value.includes(v.parentKey); - - const { key, ...other } = v; - - return ( - - {v.value} - - ); - })} - - ) : null} -
- ); - }} - - -
+ {v.value} + + ); + })} + + ) : null} +
+ + ) : null} {isMobile ? ( , V>( ) : null} - + ); }