|
@@ -17,6 +17,7 @@ import {
|
|
} from 'sentry/components/compactSelect/utils';
|
|
} from 'sentry/components/compactSelect/utils';
|
|
import {GrowingInput} from 'sentry/components/growingInput';
|
|
import {GrowingInput} from 'sentry/components/growingInput';
|
|
import Input from 'sentry/components/input';
|
|
import Input from 'sentry/components/input';
|
|
|
|
+import InteractionStateLayer from 'sentry/components/interactionStateLayer';
|
|
import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
import LoadingIndicator from 'sentry/components/loadingIndicator';
|
|
import {Overlay, PositionWrapper} from 'sentry/components/overlay';
|
|
import {Overlay, PositionWrapper} from 'sentry/components/overlay';
|
|
import {t} from 'sentry/locale';
|
|
import {t} from 'sentry/locale';
|
|
@@ -34,7 +35,10 @@ import type {
|
|
} from './types';
|
|
} from './types';
|
|
|
|
|
|
interface ComboBoxProps<Value extends string>
|
|
interface ComboBoxProps<Value extends string>
|
|
- extends ComboBoxStateOptions<ComboBoxOptionOrSection<Value>> {
|
|
|
|
|
|
+ extends Omit<
|
|
|
|
+ ComboBoxStateOptions<ComboBoxOptionOrSection<Value>>,
|
|
|
|
+ 'allowsCustomValue'
|
|
|
|
+ > {
|
|
'aria-label': string;
|
|
'aria-label': string;
|
|
className?: string;
|
|
className?: string;
|
|
disabled?: boolean;
|
|
disabled?: boolean;
|
|
@@ -59,6 +63,7 @@ function ComboBox<Value extends string>({
|
|
sizeLimitMessage,
|
|
sizeLimitMessage,
|
|
menuTrigger = 'focus',
|
|
menuTrigger = 'focus',
|
|
growingInput = false,
|
|
growingInput = false,
|
|
|
|
+ onOpenChange,
|
|
menuWidth,
|
|
menuWidth,
|
|
...props
|
|
...props
|
|
}: ComboBoxProps<Value>) {
|
|
}: ComboBoxProps<Value>) {
|
|
@@ -70,10 +75,25 @@ function ComboBox<Value extends string>({
|
|
const state = useComboBoxState({
|
|
const state = useComboBoxState({
|
|
// Mapping our disabled prop to react-aria's isDisabled
|
|
// Mapping our disabled prop to react-aria's isDisabled
|
|
isDisabled: disabled,
|
|
isDisabled: disabled,
|
|
|
|
+ onOpenChange: (isOpen, ...otherArgs) => {
|
|
|
|
+ onOpenChange?.(isOpen, ...otherArgs);
|
|
|
|
+ if (isOpen) {
|
|
|
|
+ // Ensure the selected element is being focused
|
|
|
|
+ state.selectionManager.setFocusedKey(state.selectedKey);
|
|
|
|
+ }
|
|
|
|
+ },
|
|
...props,
|
|
...props,
|
|
});
|
|
});
|
|
|
|
+
|
|
const {inputProps, listBoxProps} = useComboBox(
|
|
const {inputProps, listBoxProps} = useComboBox(
|
|
- {listBoxRef, inputRef, popoverRef, isDisabled: disabled, ...props},
|
|
|
|
|
|
+ {
|
|
|
|
+ listBoxRef,
|
|
|
|
+ inputRef,
|
|
|
|
+ popoverRef,
|
|
|
|
+ shouldFocusWrap: true,
|
|
|
|
+ isDisabled: disabled,
|
|
|
|
+ ...props,
|
|
|
|
+ },
|
|
state
|
|
state
|
|
);
|
|
);
|
|
|
|
|
|
@@ -89,6 +109,14 @@ function ComboBox<Value extends string>({
|
|
return () => {};
|
|
return () => {};
|
|
}, [menuWidth, state.isOpen]);
|
|
}, [menuWidth, state.isOpen]);
|
|
|
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
+ const popoverElement = popoverRef.current;
|
|
|
|
+ // Reset scroll state on opening the popover
|
|
|
|
+ if (popoverElement) {
|
|
|
|
+ popoverElement.scrollTop = 0;
|
|
|
|
+ }
|
|
|
|
+ }, [state.isOpen]);
|
|
|
|
+
|
|
const selectContext = useContext(SelectContext);
|
|
const selectContext = useContext(SelectContext);
|
|
|
|
|
|
const {overlayProps, triggerProps} = useOverlay({
|
|
const {overlayProps, triggerProps} = useOverlay({
|
|
@@ -97,7 +125,6 @@ function ComboBox<Value extends string>({
|
|
position: 'bottom-start',
|
|
position: 'bottom-start',
|
|
offset: [0, 8],
|
|
offset: [0, 8],
|
|
isDismissable: true,
|
|
isDismissable: true,
|
|
- isKeyboardDismissDisabled: true,
|
|
|
|
onInteractOutside: () => {
|
|
onInteractOutside: () => {
|
|
state.close();
|
|
state.close();
|
|
inputRef.current?.blur();
|
|
inputRef.current?.blur();
|
|
@@ -105,7 +132,7 @@ function ComboBox<Value extends string>({
|
|
shouldCloseOnBlur: true,
|
|
shouldCloseOnBlur: true,
|
|
});
|
|
});
|
|
|
|
|
|
- // The menu opens after selecting an item but the input stais focused
|
|
|
|
|
|
+ // The menu opens after selecting an item but the input stays focused
|
|
// This ensures the user can open the menu again by clicking on the input
|
|
// This ensures the user can open the menu again by clicking on the input
|
|
const handleInputClick = useCallback(() => {
|
|
const handleInputClick = useCallback(() => {
|
|
if (!state.isOpen && menuTrigger === 'focus') {
|
|
if (!state.isOpen && menuTrigger === 'focus') {
|
|
@@ -113,6 +140,26 @@ function ComboBox<Value extends string>({
|
|
}
|
|
}
|
|
}, [state, menuTrigger]);
|
|
}, [state, menuTrigger]);
|
|
|
|
|
|
|
|
+ const handleInputMouseUp = useCallback((event: React.MouseEvent<HTMLInputElement>) => {
|
|
|
|
+ // Prevents the input from being selected when clicking on the trigger
|
|
|
|
+ event.preventDefault();
|
|
|
|
+ }, []);
|
|
|
|
+
|
|
|
|
+ const handleInputFocus = useCallback(
|
|
|
|
+ (event: React.FocusEvent<HTMLInputElement>) => {
|
|
|
|
+ const onFocusProp = inputProps.onFocus;
|
|
|
|
+ onFocusProp?.(event);
|
|
|
|
+ if (menuTrigger === 'focus') {
|
|
|
|
+ state.open();
|
|
|
|
+ }
|
|
|
|
+ // Need to setTimeout otherwise Chrome might reset the selection on padding click
|
|
|
|
+ setTimeout(() => {
|
|
|
|
+ event.target.select();
|
|
|
|
+ }, 0);
|
|
|
|
+ },
|
|
|
|
+ [inputProps.onFocus, menuTrigger, state]
|
|
|
|
+ );
|
|
|
|
+
|
|
const InputComponent = growingInput ? StyledGrowingInput : StyledInput;
|
|
const InputComponent = growingInput ? StyledGrowingInput : StyledInput;
|
|
|
|
|
|
return (
|
|
return (
|
|
@@ -123,10 +170,13 @@ function ComboBox<Value extends string>({
|
|
}}
|
|
}}
|
|
>
|
|
>
|
|
<ControlWrapper className={className}>
|
|
<ControlWrapper className={className}>
|
|
|
|
+ {!state.isFocused && <InteractionStateLayer />}
|
|
<InputComponent
|
|
<InputComponent
|
|
{...inputProps}
|
|
{...inputProps}
|
|
onClick={handleInputClick}
|
|
onClick={handleInputClick}
|
|
placeholder={placeholder}
|
|
placeholder={placeholder}
|
|
|
|
+ onMouseUp={handleInputMouseUp}
|
|
|
|
+ onFocus={handleInputFocus}
|
|
ref={mergeRefs([inputRef, triggerProps.ref])}
|
|
ref={mergeRefs([inputRef, triggerProps.ref])}
|
|
size={size}
|
|
size={size}
|
|
/>
|
|
/>
|
|
@@ -307,15 +357,22 @@ const ControlWrapper = styled('div')`
|
|
width: max-content;
|
|
width: max-content;
|
|
min-width: 150px;
|
|
min-width: 150px;
|
|
max-width: 100%;
|
|
max-width: 100%;
|
|
|
|
+ cursor: pointer;
|
|
`;
|
|
`;
|
|
|
|
|
|
const StyledInput = styled(Input)`
|
|
const StyledInput = styled(Input)`
|
|
max-width: inherit;
|
|
max-width: inherit;
|
|
min-width: inherit;
|
|
min-width: inherit;
|
|
|
|
+ &:not(:focus) {
|
|
|
|
+ pointer-events: none;
|
|
|
|
+ }
|
|
`;
|
|
`;
|
|
const StyledGrowingInput = styled(GrowingInput)`
|
|
const StyledGrowingInput = styled(GrowingInput)`
|
|
max-width: inherit;
|
|
max-width: inherit;
|
|
min-width: inherit;
|
|
min-width: inherit;
|
|
|
|
+ &:not(:focus) {
|
|
|
|
+ cursor: pointer;
|
|
|
|
+ }
|
|
`;
|
|
`;
|
|
|
|
|
|
const StyledPositionWrapper = styled(PositionWrapper, {
|
|
const StyledPositionWrapper = styled(PositionWrapper, {
|