fix(autocomplete): ignore pointer events when the clear button is hidden (#3000)

* fix(autocomplete): hide clear button with `visibility: hidden`

* fix(autocomplete): clear button pointer-events

* refactor(autocomplete): improve keyboard reopen issue on mobile

* chore: add changeset

* refactor(autocomplete): apply chain and add type to e

---------

Co-authored-by: WK Wong <wingkwong.code@gmail.com>
This commit is contained in:
chirokas 2024-09-05 17:24:15 +08:00 committed by GitHub
parent 19c331be75
commit 81da063d6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 30 additions and 26 deletions

View File

@ -0,0 +1,6 @@
---
"@nextui-org/autocomplete": patch
"@nextui-org/theme": patch
---
Improve clear button pointer events, keyboard reopen issue on mobile

View File

@ -7,7 +7,7 @@ import {autocomplete} from "@nextui-org/theme";
import {useFilter} from "@react-aria/i18n"; import {useFilter} from "@react-aria/i18n";
import {FilterFn, useComboBoxState} from "@react-stately/combobox"; import {FilterFn, useComboBoxState} from "@react-stately/combobox";
import {ReactRef, useDOMRef} from "@nextui-org/react-utils"; import {ReactRef, useDOMRef} from "@nextui-org/react-utils";
import {ReactNode, useCallback, useEffect, useMemo, useRef} from "react"; import {ReactNode, useEffect, useMemo, useRef} from "react";
import {ComboBoxProps} from "@react-types/combobox"; import {ComboBoxProps} from "@react-types/combobox";
import {PopoverProps} from "@nextui-org/popover"; import {PopoverProps} from "@nextui-org/popover";
import {ListboxProps} from "@nextui-org/listbox"; import {ListboxProps} from "@nextui-org/listbox";
@ -318,10 +318,7 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
}, [inputRef.current]); }, [inputRef.current]);
useEffect(() => { useEffect(() => {
// set input focus
if (isOpen) { if (isOpen) {
onFocus(true);
// apply the same with to the popover as the select // apply the same with to the popover as the select
if (popoverRef.current && inputWrapperRef.current) { if (popoverRef.current && inputWrapperRef.current) {
let rect = inputWrapperRef.current.getBoundingClientRect(); let rect = inputWrapperRef.current.getBoundingClientRect();
@ -361,19 +358,6 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
[objectToDeps(variantProps), isClearable, disableAnimation, className], [objectToDeps(variantProps), isClearable, disableAnimation, className],
); );
const onClear = useCallback(() => {
state.setInputValue("");
state.setSelectedKey(null);
}, [state]);
const onFocus = useCallback(
(isFocused: boolean) => {
inputRef.current?.focus();
state.setFocused(isFocused);
},
[state, inputRef],
);
const getBaseProps: PropGetter = () => ({ const getBaseProps: PropGetter = () => ({
"data-invalid": dataAttr(isInvalid), "data-invalid": dataAttr(isInvalid),
"data-open": dataAttr(state.isOpen), "data-open": dataAttr(state.isOpen),
@ -394,19 +378,23 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
({ ({
...mergeProps(buttonProps, slotsProps.clearButtonProps), ...mergeProps(buttonProps, slotsProps.clearButtonProps),
// disable original focus and state toggle from react aria // disable original focus and state toggle from react aria
onPressStart: () => {}, onPressStart: () => {
// this is in PressStart for mobile so that touching the clear button doesn't remove focus from
// the input and close the keyboard
inputRef.current?.focus();
},
onPress: (e: PressEvent) => { onPress: (e: PressEvent) => {
slotsProps.clearButtonProps?.onPress?.(e); slotsProps.clearButtonProps?.onPress?.(e);
if (state.selectedItem) { if (state.selectedItem) {
onClear(); state.setInputValue("");
state.setSelectedKey(null);
} else { } else {
if (allowsCustomValue) { if (allowsCustomValue) {
state.setInputValue(""); state.setInputValue("");
state.close(); state.close();
} }
} }
inputRef?.current?.focus();
}, },
"data-visible": !!state.selectedItem || state.inputValue?.length > 0, "data-visible": !!state.selectedItem || state.inputValue?.length > 0,
className: slots.clearButton({ className: slots.clearButton({
@ -488,13 +476,19 @@ export function useAutocomplete<T extends object>(originalProps: UseAutocomplete
className: slots.endContentWrapper({ className: slots.endContentWrapper({
class: clsx(classNames?.endContentWrapper, props?.className), class: clsx(classNames?.endContentWrapper, props?.className),
}), }),
onClick: (e) => { onPointerDown: chain(props.onPointerDown, (e: React.PointerEvent) => {
const inputFocused = inputRef.current === document.activeElement; if (e.button === 0 && e.currentTarget === e.target) {
inputRef.current?.focus();
if (!inputFocused && !state.isFocused && e.currentTarget === e.target) {
onFocus(true);
} }
}, }),
onMouseDown: chain(props.onMouseDown, (e: React.MouseEvent) => {
if (e.button === 0 && e.currentTarget === e.target) {
// Chrome and Firefox on touch Windows devices require mouse down events
// to be canceled in addition to pointer events, or an extra asynchronous
// focus event will be fired.
e.preventDefault();
}
}),
}); });
return { return {

View File

@ -14,12 +14,16 @@ const autocomplete = tv({
"translate-x-1", "translate-x-1",
"cursor-text", "cursor-text",
"opacity-0", "opacity-0",
"pointer-events-none",
"text-default-500", "text-default-500",
"group-data-[invalid=true]:text-danger", "group-data-[invalid=true]:text-danger",
"data-[visible=true]:opacity-100", // on mobile is always visible when there is a value "data-[visible=true]:opacity-100", // on mobile is always visible when there is a value
"data-[visible=true]:pointer-events-auto",
"data-[visible=true]:cursor-pointer", "data-[visible=true]:cursor-pointer",
"sm:data-[visible=true]:opacity-0", // only visible on hover "sm:data-[visible=true]:opacity-0", // only visible on hover
"sm:data-[visible=true]:pointer-events-none",
"sm:group-data-[hover=true]:data-[visible=true]:opacity-100", "sm:group-data-[hover=true]:data-[visible=true]:opacity-100",
"sm:group-data-[hover=true]:data-[visible=true]:pointer-events-auto",
], ],
selectorButton: "text-medium", selectorButton: "text-medium",
}, },