fix: date input click outside (#9676)

cc @lucasbordeau

---------

Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
Jérémy M
2025-01-16 17:35:59 +01:00
committed by GitHub
parent 1df0603aaa
commit f621af1732
6 changed files with 151 additions and 48 deletions

View File

@ -6,12 +6,12 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
import { FieldMetadataType } from '~/generated/graphql';
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
import { useDateTimeField } from '../../../hooks/useDateTimeField';
import {
DateTimeFieldInput,
DateTimeFieldInputProps,
} from '../DateTimeFieldInput';
const formattedDate = new Date(2022, 0, 1, 2, 0, 0);
const DateFieldValueSetterEffect = ({ value }: { value: Date }) => {
@ -81,6 +81,7 @@ const DateFieldInputWithContext = ({
}}
recordId={recordId}
>
<StorybookFieldInputDropdownFocusIdSetterEffect />
<DateFieldValueSetterEffect value={value} />
<DateFieldValueGater
onEscape={onEscape}
@ -98,7 +99,7 @@ const enterJestFn = fn();
const clickOutsideJestFn = fn();
const meta: Meta = {
title: 'UI/Data/Field/Input/DateFieldInput',
title: 'UI/Data/Field/Input/DateTimeFieldInput',
component: DateFieldInputWithContext,
args: {
value: formattedDate,

View File

@ -7,6 +7,7 @@ import { FieldMetadataType } from '~/generated/graphql';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
import { useNumberField } from '../../../hooks/useNumberField';
import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput';
@ -56,6 +57,7 @@ const NumberFieldInputWithContext = ({
}}
recordId={recordId}
>
<StorybookFieldInputDropdownFocusIdSetterEffect />
<NumberFieldValueSetterEffect value={value} />
<NumberFieldInput
onEnter={onEnter}

View File

@ -6,6 +6,7 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
import { FieldMetadataType } from '~/generated/graphql';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
import { FieldContextProvider } from '../../../components/FieldContextProvider';
import { useTextField } from '../../../hooks/useTextField';
import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput';
@ -56,6 +57,7 @@ const TextFieldInputWithContext = ({
}}
recordId={recordId}
>
<StorybookFieldInputDropdownFocusIdSetterEffect />
<TextFieldValueSetterEffect value={value} />
<TextFieldInput
onEnter={onEnter}

View File

@ -1,7 +1,12 @@
import { Key } from 'ts-key-enum';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { activeDropdownFocusIdState } from '@/ui/layout/dropdown/states/activeDropdownFocusIdState';
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
import { useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from '~/utils/isDefined';
export const useRegisterInputEvents = <T>({
@ -25,9 +30,32 @@ export const useRegisterInputEvents = <T>({
onClickOutside?: (event: MouseEvent | TouchEvent, inputValue: T) => void;
hotkeyScope: string;
}) => {
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
const { recordId, fieldDefinition } = useContext(FieldContext);
useListenClickOutside({
refs: [inputRef, copyRef].filter(isDefined),
callback: (event) => {
const fieldDropdownFocusIdTableCell = getDropdownFocusIdForRecordField(
recordId,
fieldDefinition.fieldMetadataId,
'table-cell',
);
const fieldDropdownFocusIdInlineCell = getDropdownFocusIdForRecordField(
recordId,
fieldDefinition.fieldMetadataId,
'inline-cell',
);
if (
activeDropdownFocusId !== fieldDropdownFocusIdTableCell &&
activeDropdownFocusId !== fieldDropdownFocusIdInlineCell
) {
return;
}
onClickOutside?.(event, inputValue);
},
enabled: isDefined(onClickOutside),

View File

@ -4,6 +4,8 @@ import { useRecoilCallback } from 'recoil';
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
const CLICK_OUTSIDE_DEBUG_MODE = false;
export enum ClickOutsideMode {
comparePixels = 'comparePixels',
compareHTMLRef = 'compareHTMLRef',
@ -60,52 +62,60 @@ export const useListenClickOutside = <T extends Element>({
return;
}
if (mode === ClickOutsideMode.compareHTMLRef) {
const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current)
.some((ref) => ref.current?.contains(event.target as Node));
switch (mode) {
case ClickOutsideMode.compareHTMLRef: {
const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current)
.some((ref) => ref.current?.contains(event.target as Node));
set(
getClickOutsideListenerIsMouseDownInsideState,
clickedOnAtLeastOneRef,
);
}
set(
getClickOutsideListenerIsMouseDownInsideState,
clickedOnAtLeastOneRef,
);
break;
}
if (mode === ClickOutsideMode.comparePixels) {
const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current)
.some((ref) => {
if (!ref.current) {
return false;
}
case ClickOutsideMode.comparePixels: {
const clickedOnAtLeastOneRef = refs
.filter((ref) => !!ref.current)
.some((ref) => {
if (!ref.current) {
return false;
}
const { x, y, width, height } =
ref.current.getBoundingClientRect();
const { x, y, width, height } =
ref.current.getBoundingClientRect();
const clientX =
'clientX' in event
? event.clientX
: event.changedTouches[0].clientX;
const clientY =
'clientY' in event
? event.clientY
: event.changedTouches[0].clientY;
const clientX =
'clientX' in event
? event.clientX
: event.changedTouches[0].clientX;
const clientY =
'clientY' in event
? event.clientY
: event.changedTouches[0].clientY;
if (
clientX < x ||
clientX > x + width ||
clientY < y ||
clientY > y + height
) {
return false;
}
return true;
});
if (
clientX < x ||
clientX > x + width ||
clientY < y ||
clientY > y + height
) {
return false;
}
return true;
});
set(
getClickOutsideListenerIsMouseDownInsideState,
clickedOnAtLeastOneRef,
);
set(
getClickOutsideListenerIsMouseDownInsideState,
clickedOnAtLeastOneRef,
);
break;
}
default: {
break;
}
}
},
[
@ -171,13 +181,30 @@ export const useListenClickOutside = <T extends Element>({
.filter((ref) => !!ref.current)
.some((ref) => ref.current?.contains(event.target as Node));
if (
const shouldTrigger =
isListening &&
hasMouseDownHappened &&
!clickedOnAtLeastOneRef &&
!isMouseDownInside &&
!isClickedOnExcluded
) {
!isClickedOnExcluded;
if (CLICK_OUTSIDE_DEBUG_MODE) {
// eslint-disable-next-line no-console
console.log('click outside compare refs', {
listenerId,
shouldTrigger,
isListening,
hasMouseDownHappened,
clickedOnAtLeastOneRef,
isMouseDownInside,
isClickedOnExcluded,
hotkeyScope,
enabled,
event,
});
}
if (shouldTrigger) {
callback(event);
}
}
@ -213,12 +240,28 @@ export const useListenClickOutside = <T extends Element>({
return true;
});
if (
const shouldTrigger =
!clickedOnAtLeastOneRef &&
!isMouseDownInside &&
isListening &&
hasMouseDownHappened
) {
hasMouseDownHappened;
if (CLICK_OUTSIDE_DEBUG_MODE) {
// eslint-disable-next-line no-console
console.log('click outside compare pixel', {
listenerId,
shouldTrigger,
clickedOnAtLeastOneRef,
isMouseDownInside,
isListening,
hasMouseDownHappened,
hotkeyScope,
enabled,
event,
});
}
if (shouldTrigger) {
callback(event);
}
}
@ -233,6 +276,7 @@ export const useListenClickOutside = <T extends Element>({
refs,
excludeClassNames,
callback,
listenerId,
],
);

View File

@ -0,0 +1,26 @@
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
import { getDropdownFocusIdForRecordField } from '@/object-record/utils/getDropdownFocusIdForRecordField';
import { useSetActiveDropdownFocusIdAndMemorizePrevious } from '@/ui/layout/dropdown/hooks/useSetFocusedDropdownIdAndMemorizePrevious';
import { useContext, useEffect } from 'react';
export const StorybookFieldInputDropdownFocusIdSetterEffect = () => {
const { recordId, fieldDefinition } = useContext(FieldContext);
const { setActiveDropdownFocusIdAndMemorizePrevious } =
useSetActiveDropdownFocusIdAndMemorizePrevious();
const fieldDropdownFocusIdTableCell = getDropdownFocusIdForRecordField(
recordId,
fieldDefinition.fieldMetadataId,
'table-cell',
);
useEffect(() => {
setActiveDropdownFocusIdAndMemorizePrevious(fieldDropdownFocusIdTableCell);
}, [
setActiveDropdownFocusIdAndMemorizePrevious,
fieldDropdownFocusIdTableCell,
]);
return null;
};