|
|
|
@ -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}
|
|
|
|
|
|
|
|
|
|
{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 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>
|
|
|
|
|
)}
|
|
|
|
|
</Reference>
|
|
|
|
|
<Portal>
|
|
|
|
|
<Popper
|
|
|
|
|
placement="bottom-start"
|
|
|
|
|
modifiers={{
|
|
|
|
|
computeMaxHeight: {
|
|
|
|
|
order: 851,
|
|
|
|
|
enabled: true,
|
|
|
|
|
fn: handleComputeMaxHeight,
|
|
|
|
|
},
|
|
|
|
|
preventOverflow: {
|
|
|
|
|
enabled: true,
|
|
|
|
|
boundariesElement: 'viewport',
|
|
|
|
|
},
|
|
|
|
|
}}
|
|
|
|
|
</Link>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
{isOpen ? (
|
|
|
|
|
<FloatingPortal id="portal-root">
|
|
|
|
|
<div
|
|
|
|
|
ref={refs.setFloating}
|
|
|
|
|
className={styles.optionsContainer}
|
|
|
|
|
style={floatingStyles}
|
|
|
|
|
{...getFloatingProps()}
|
|
|
|
|
>
|
|
|
|
|
{({ ref, style, scheduleUpdate }) => {
|
|
|
|
|
updater.current = scheduleUpdate;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div
|
|
|
|
|
ref={ref}
|
|
|
|
|
id={optionsId}
|
|
|
|
|
className={styles.optionsContainer}
|
|
|
|
|
style={{
|
|
|
|
|
...style,
|
|
|
|
|
minWidth: width,
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{isOpen && !isMobile ? (
|
|
|
|
|
<Scroller
|
|
|
|
|
className={styles.options}
|
|
|
|
|
style={{
|
|
|
|
|
maxHeight: style.maxHeight,
|
|
|
|
|
}}
|
|
|
|
|
{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>
|
|
|
|
|
</>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|