fix: date input click outside (#9676)
cc @lucasbordeau --------- Co-authored-by: Lucas Bordeau <bordeau.lucas@gmail.com>
This commit is contained in:
@ -6,12 +6,12 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
|
|||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
|
|
||||||
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
||||||
|
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
|
||||||
import { useDateTimeField } from '../../../hooks/useDateTimeField';
|
import { useDateTimeField } from '../../../hooks/useDateTimeField';
|
||||||
import {
|
import {
|
||||||
DateTimeFieldInput,
|
DateTimeFieldInput,
|
||||||
DateTimeFieldInputProps,
|
DateTimeFieldInputProps,
|
||||||
} from '../DateTimeFieldInput';
|
} from '../DateTimeFieldInput';
|
||||||
|
|
||||||
const formattedDate = new Date(2022, 0, 1, 2, 0, 0);
|
const formattedDate = new Date(2022, 0, 1, 2, 0, 0);
|
||||||
|
|
||||||
const DateFieldValueSetterEffect = ({ value }: { value: Date }) => {
|
const DateFieldValueSetterEffect = ({ value }: { value: Date }) => {
|
||||||
@ -81,6 +81,7 @@ const DateFieldInputWithContext = ({
|
|||||||
}}
|
}}
|
||||||
recordId={recordId}
|
recordId={recordId}
|
||||||
>
|
>
|
||||||
|
<StorybookFieldInputDropdownFocusIdSetterEffect />
|
||||||
<DateFieldValueSetterEffect value={value} />
|
<DateFieldValueSetterEffect value={value} />
|
||||||
<DateFieldValueGater
|
<DateFieldValueGater
|
||||||
onEscape={onEscape}
|
onEscape={onEscape}
|
||||||
@ -98,7 +99,7 @@ const enterJestFn = fn();
|
|||||||
const clickOutsideJestFn = fn();
|
const clickOutsideJestFn = fn();
|
||||||
|
|
||||||
const meta: Meta = {
|
const meta: Meta = {
|
||||||
title: 'UI/Data/Field/Input/DateFieldInput',
|
title: 'UI/Data/Field/Input/DateTimeFieldInput',
|
||||||
component: DateFieldInputWithContext,
|
component: DateFieldInputWithContext,
|
||||||
args: {
|
args: {
|
||||||
value: formattedDate,
|
value: formattedDate,
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import { FieldMetadataType } from '~/generated/graphql';
|
|||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider';
|
||||||
|
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
|
||||||
import { useNumberField } from '../../../hooks/useNumberField';
|
import { useNumberField } from '../../../hooks/useNumberField';
|
||||||
import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput';
|
import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput';
|
||||||
|
|
||||||
@ -56,6 +57,7 @@ const NumberFieldInputWithContext = ({
|
|||||||
}}
|
}}
|
||||||
recordId={recordId}
|
recordId={recordId}
|
||||||
>
|
>
|
||||||
|
<StorybookFieldInputDropdownFocusIdSetterEffect />
|
||||||
<NumberFieldValueSetterEffect value={value} />
|
<NumberFieldValueSetterEffect value={value} />
|
||||||
<NumberFieldInput
|
<NumberFieldInput
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope
|
|||||||
import { FieldMetadataType } from '~/generated/graphql';
|
import { FieldMetadataType } from '~/generated/graphql';
|
||||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||||
|
|
||||||
|
import { StorybookFieldInputDropdownFocusIdSetterEffect } from '~/testing/components/StorybookFieldInputDropdownFocusIdSetterEffect';
|
||||||
import { FieldContextProvider } from '../../../components/FieldContextProvider';
|
import { FieldContextProvider } from '../../../components/FieldContextProvider';
|
||||||
import { useTextField } from '../../../hooks/useTextField';
|
import { useTextField } from '../../../hooks/useTextField';
|
||||||
import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput';
|
import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput';
|
||||||
@ -56,6 +57,7 @@ const TextFieldInputWithContext = ({
|
|||||||
}}
|
}}
|
||||||
recordId={recordId}
|
recordId={recordId}
|
||||||
>
|
>
|
||||||
|
<StorybookFieldInputDropdownFocusIdSetterEffect />
|
||||||
<TextFieldValueSetterEffect value={value} />
|
<TextFieldValueSetterEffect value={value} />
|
||||||
<TextFieldInput
|
<TextFieldInput
|
||||||
onEnter={onEnter}
|
onEnter={onEnter}
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
import { Key } from 'ts-key-enum';
|
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 { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside';
|
||||||
|
import { useContext } from 'react';
|
||||||
|
import { useRecoilValue } from 'recoil';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export const useRegisterInputEvents = <T>({
|
export const useRegisterInputEvents = <T>({
|
||||||
@ -25,9 +30,32 @@ export const useRegisterInputEvents = <T>({
|
|||||||
onClickOutside?: (event: MouseEvent | TouchEvent, inputValue: T) => void;
|
onClickOutside?: (event: MouseEvent | TouchEvent, inputValue: T) => void;
|
||||||
hotkeyScope: string;
|
hotkeyScope: string;
|
||||||
}) => {
|
}) => {
|
||||||
|
const activeDropdownFocusId = useRecoilValue(activeDropdownFocusIdState);
|
||||||
|
|
||||||
|
const { recordId, fieldDefinition } = useContext(FieldContext);
|
||||||
|
|
||||||
useListenClickOutside({
|
useListenClickOutside({
|
||||||
refs: [inputRef, copyRef].filter(isDefined),
|
refs: [inputRef, copyRef].filter(isDefined),
|
||||||
callback: (event) => {
|
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);
|
onClickOutside?.(event, inputValue);
|
||||||
},
|
},
|
||||||
enabled: isDefined(onClickOutside),
|
enabled: isDefined(onClickOutside),
|
||||||
|
|||||||
@ -4,6 +4,8 @@ import { useRecoilCallback } from 'recoil';
|
|||||||
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
import { internalHotkeysEnabledScopesState } from '@/ui/utilities/hotkey/states/internal/internalHotkeysEnabledScopesState';
|
||||||
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
import { useClickOustideListenerStates } from '@/ui/utilities/pointer-event/hooks/useClickOustideListenerStates';
|
||||||
|
|
||||||
|
const CLICK_OUTSIDE_DEBUG_MODE = false;
|
||||||
|
|
||||||
export enum ClickOutsideMode {
|
export enum ClickOutsideMode {
|
||||||
comparePixels = 'comparePixels',
|
comparePixels = 'comparePixels',
|
||||||
compareHTMLRef = 'compareHTMLRef',
|
compareHTMLRef = 'compareHTMLRef',
|
||||||
@ -60,52 +62,60 @@ export const useListenClickOutside = <T extends Element>({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mode === ClickOutsideMode.compareHTMLRef) {
|
switch (mode) {
|
||||||
const clickedOnAtLeastOneRef = refs
|
case ClickOutsideMode.compareHTMLRef: {
|
||||||
.filter((ref) => !!ref.current)
|
const clickedOnAtLeastOneRef = refs
|
||||||
.some((ref) => ref.current?.contains(event.target as Node));
|
.filter((ref) => !!ref.current)
|
||||||
|
.some((ref) => ref.current?.contains(event.target as Node));
|
||||||
|
|
||||||
set(
|
set(
|
||||||
getClickOutsideListenerIsMouseDownInsideState,
|
getClickOutsideListenerIsMouseDownInsideState,
|
||||||
clickedOnAtLeastOneRef,
|
clickedOnAtLeastOneRef,
|
||||||
);
|
);
|
||||||
}
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (mode === ClickOutsideMode.comparePixels) {
|
case ClickOutsideMode.comparePixels: {
|
||||||
const clickedOnAtLeastOneRef = refs
|
const clickedOnAtLeastOneRef = refs
|
||||||
.filter((ref) => !!ref.current)
|
.filter((ref) => !!ref.current)
|
||||||
.some((ref) => {
|
.some((ref) => {
|
||||||
if (!ref.current) {
|
if (!ref.current) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { x, y, width, height } =
|
const { x, y, width, height } =
|
||||||
ref.current.getBoundingClientRect();
|
ref.current.getBoundingClientRect();
|
||||||
|
|
||||||
const clientX =
|
const clientX =
|
||||||
'clientX' in event
|
'clientX' in event
|
||||||
? event.clientX
|
? event.clientX
|
||||||
: event.changedTouches[0].clientX;
|
: event.changedTouches[0].clientX;
|
||||||
const clientY =
|
const clientY =
|
||||||
'clientY' in event
|
'clientY' in event
|
||||||
? event.clientY
|
? event.clientY
|
||||||
: event.changedTouches[0].clientY;
|
: event.changedTouches[0].clientY;
|
||||||
|
|
||||||
if (
|
if (
|
||||||
clientX < x ||
|
clientX < x ||
|
||||||
clientX > x + width ||
|
clientX > x + width ||
|
||||||
clientY < y ||
|
clientY < y ||
|
||||||
clientY > y + height
|
clientY > y + height
|
||||||
) {
|
) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
set(
|
set(
|
||||||
getClickOutsideListenerIsMouseDownInsideState,
|
getClickOutsideListenerIsMouseDownInsideState,
|
||||||
clickedOnAtLeastOneRef,
|
clickedOnAtLeastOneRef,
|
||||||
);
|
);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
@ -171,13 +181,30 @@ export const useListenClickOutside = <T extends Element>({
|
|||||||
.filter((ref) => !!ref.current)
|
.filter((ref) => !!ref.current)
|
||||||
.some((ref) => ref.current?.contains(event.target as Node));
|
.some((ref) => ref.current?.contains(event.target as Node));
|
||||||
|
|
||||||
if (
|
const shouldTrigger =
|
||||||
isListening &&
|
isListening &&
|
||||||
hasMouseDownHappened &&
|
hasMouseDownHappened &&
|
||||||
!clickedOnAtLeastOneRef &&
|
!clickedOnAtLeastOneRef &&
|
||||||
!isMouseDownInside &&
|
!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);
|
callback(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -213,12 +240,28 @@ export const useListenClickOutside = <T extends Element>({
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
if (
|
const shouldTrigger =
|
||||||
!clickedOnAtLeastOneRef &&
|
!clickedOnAtLeastOneRef &&
|
||||||
!isMouseDownInside &&
|
!isMouseDownInside &&
|
||||||
isListening &&
|
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);
|
callback(event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -233,6 +276,7 @@ export const useListenClickOutside = <T extends Element>({
|
|||||||
refs,
|
refs,
|
||||||
excludeClassNames,
|
excludeClassNames,
|
||||||
callback,
|
callback,
|
||||||
|
listenerId,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user