mirror of
https://github.com/Sonarr/Sonarr.git
synced 2025-04-24 06:17:08 -04:00
Use floading UI for EnhancedSelectInput
This commit is contained in:
parent
c13522eb3b
commit
f4dff35f81
1 changed files with 146 additions and 220 deletions
|
@ -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<T extends EnhancedSelectInputValue<V>, 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<T extends EnhancedSelectInputValue<V>, 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<T extends EnhancedSelectInputValue<V>, 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<T extends EnhancedSelectInputValue<V>, V>(
|
|||
|
||||
const handleFocus = useCallback(() => {
|
||||
if (isOpen) {
|
||||
removeListener();
|
||||
setIsOpen(false);
|
||||
}
|
||||
}, [isOpen, setIsOpen, removeListener]);
|
||||
}, [isOpen, setIsOpen]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(event: KeyboardEvent<HTMLButtonElement>) => {
|
||||
|
@ -389,176 +372,119 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
|
|||
[onChange]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (updater.current) {
|
||||
updater.current();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
addListener();
|
||||
} else {
|
||||
removeListener();
|
||||
}
|
||||
|
||||
return removeListener;
|
||||
}, [isOpen, addListener, removeListener]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Manager>
|
||||
<Reference>
|
||||
{({ ref }) => (
|
||||
<div ref={ref} id={buttonId}>
|
||||
<div ref={measureRef}>
|
||||
{isEditable && typeof value === 'string' ? (
|
||||
<div className={styles.editableContainer}>
|
||||
<TextInput
|
||||
className={className}
|
||||
name={name}
|
||||
value={value}
|
||||
readOnly={isDisabled}
|
||||
hasError={hasError}
|
||||
hasWarning={hasWarning}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleEditChange}
|
||||
/>
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.dropdownArrowContainerEditable,
|
||||
isDisabled
|
||||
? styles.dropdownArrowContainerDisabled
|
||||
: styles.dropdownArrowContainer
|
||||
)}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{isFetching ? (
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
) : null}
|
||||
<>
|
||||
<div ref={refs.setReference} {...getReferenceProps()}>
|
||||
{isEditable && typeof value === 'string' ? (
|
||||
<div className={styles.editableContainer}>
|
||||
<TextInput
|
||||
className={className}
|
||||
name={name}
|
||||
value={value}
|
||||
readOnly={isDisabled}
|
||||
hasError={hasError}
|
||||
hasWarning={hasWarning}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onChange={handleEditChange}
|
||||
/>
|
||||
<Link
|
||||
className={classNames(
|
||||
styles.dropdownArrowContainerEditable,
|
||||
isDisabled
|
||||
? styles.dropdownArrowContainerDisabled
|
||||
: styles.dropdownArrowContainer
|
||||
)}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{isFetching ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
|
||||
{isFetching ? null : <Icon name={icons.CARET_DOWN} />}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPress={handlePress}
|
||||
>
|
||||
<SelectedValueComponent
|
||||
values={values}
|
||||
{...selectedValueOptions}
|
||||
selectedValue={selectedValue}
|
||||
isDisabled={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : selectedValue}
|
||||
</SelectedValueComponent>
|
||||
|
||||
<div
|
||||
className={
|
||||
isDisabled
|
||||
? styles.dropdownArrowContainerDisabled
|
||||
: styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
{isFetching ? (
|
||||
<LoadingIndicator
|
||||
className={styles.loading}
|
||||
size={20}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
{isFetching ? null : <Icon name={icons.CARET_DOWN} />}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Reference>
|
||||
<Portal>
|
||||
<Popper
|
||||
placement="bottom-start"
|
||||
modifiers={{
|
||||
computeMaxHeight: {
|
||||
order: 851,
|
||||
enabled: true,
|
||||
fn: handleComputeMaxHeight,
|
||||
},
|
||||
preventOverflow: {
|
||||
enabled: true,
|
||||
boundariesElement: 'viewport',
|
||||
},
|
||||
}}
|
||||
{isFetching ? null : <Icon name={icons.CARET_DOWN} />}
|
||||
</Link>
|
||||
</div>
|
||||
) : (
|
||||
<Link
|
||||
className={classNames(
|
||||
className,
|
||||
hasError && styles.hasError,
|
||||
hasWarning && styles.hasWarning,
|
||||
isDisabled && disabledClassName
|
||||
)}
|
||||
isDisabled={isDisabled}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
onPress={handlePress}
|
||||
>
|
||||
{({ ref, style, scheduleUpdate }) => {
|
||||
updater.current = scheduleUpdate;
|
||||
<SelectedValueComponent
|
||||
values={values}
|
||||
{...selectedValueOptions}
|
||||
selectedValue={selectedValue}
|
||||
isDisabled={isDisabled}
|
||||
isMultiSelect={isMultiSelect}
|
||||
>
|
||||
{selectedOption ? selectedOption.value : selectedValue}
|
||||
</SelectedValueComponent>
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id={optionsId}
|
||||
className={styles.optionsContainer}
|
||||
style={{
|
||||
...style,
|
||||
minWidth: width,
|
||||
}}
|
||||
>
|
||||
{isOpen && !isMobile ? (
|
||||
<Scroller
|
||||
className={styles.options}
|
||||
style={{
|
||||
maxHeight: style.maxHeight,
|
||||
}}
|
||||
<div
|
||||
className={
|
||||
isDisabled
|
||||
? styles.dropdownArrowContainerDisabled
|
||||
: styles.dropdownArrowContainer
|
||||
}
|
||||
>
|
||||
{isFetching ? (
|
||||
<LoadingIndicator className={styles.loading} size={20} />
|
||||
) : null}
|
||||
|
||||
{isFetching ? null : <Icon name={icons.CARET_DOWN} />}
|
||||
</div>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
{isOpen ? (
|
||||
<FloatingPortal id="portal-root">
|
||||
<div
|
||||
ref={refs.setFloating}
|
||||
className={styles.optionsContainer}
|
||||
style={floatingStyles}
|
||||
{...getFloatingProps()}
|
||||
>
|
||||
{isOpen && !isMobile ? (
|
||||
<Scroller className={styles.options}>
|
||||
{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 (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
depth={depth}
|
||||
isSelected={isSelectedItem(index, value, values)}
|
||||
isDisabled={parentSelected}
|
||||
isMultiSelect={isMultiSelect}
|
||||
{...valueOptions}
|
||||
{...other}
|
||||
isMobile={false}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{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 (
|
||||
<OptionComponent
|
||||
key={v.key}
|
||||
id={v.key}
|
||||
depth={depth}
|
||||
isSelected={isSelectedItem(index, value, values)}
|
||||
isDisabled={parentSelected}
|
||||
isMultiSelect={isMultiSelect}
|
||||
{...valueOptions}
|
||||
{...other}
|
||||
isMobile={false}
|
||||
onSelect={handleSelect}
|
||||
>
|
||||
{v.value}
|
||||
</OptionComponent>
|
||||
);
|
||||
})}
|
||||
</Scroller>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
</Popper>
|
||||
</Portal>
|
||||
</Manager>
|
||||
{v.value}
|
||||
</OptionComponent>
|
||||
);
|
||||
})}
|
||||
</Scroller>
|
||||
) : null}
|
||||
</div>
|
||||
</FloatingPortal>
|
||||
) : null}
|
||||
|
||||
{isMobile ? (
|
||||
<Modal
|
||||
|
@ -613,7 +539,7 @@ function EnhancedSelectInput<T extends EnhancedSelectInputValue<V>, V>(
|
|||
</ModalBody>
|
||||
</Modal>
|
||||
) : null}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue