diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx index 4a2ebb10c..6cc3f7577 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx @@ -68,9 +68,6 @@ export const RecordTableCellSoftFocusMode = ({ }, TableHotkeyScope.TableSoftFocus, [clearField, isFieldClearable, isFieldInputOnly], - { - enabled: !isFieldInputOnly, - }, ); useScopedHotkeys( diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx index d23690fc3..c92bd3c96 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx @@ -1,8 +1,7 @@ -import { useState } from 'react'; -import { Controller, useFormContext } from 'react-hook-form'; import styled from '@emotion/styled'; import { DropResult } from '@hello-pangea/dnd'; -import { Key } from 'ts-key-enum'; +import { useState } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; import { IconPlus } from 'twenty-ui'; import { z } from 'zod'; @@ -21,8 +20,6 @@ import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardFooter } from '@/ui/layout/card/components/CardFooter'; import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { toSpliced } from '~/utils/array/toSpliced'; @@ -189,19 +186,15 @@ export const SettingsDataModelFieldSelectForm = ({ setFormValue('options', newOptions); }; - useScopedHotkeys( - Key.Enter, - () => { - const newOptions = getOptionsWithNewOption(); + const handleInputEnter = () => { + const newOptions = getOptionsWithNewOption(); - setFormValue('options', newOptions); + setFormValue('options', newOptions); - const lastOptionId = newOptions[newOptions.length - 1].id; + const lastOptionId = newOptions[newOptions.length - 1].id; - setFocusedOptionId(lastOptionId); - }, - AppHotkeyScope.App, - ); + setFocusedOptionId(lastOptionId); + }; return ( <> @@ -270,6 +263,7 @@ export const SettingsDataModelFieldSelectForm = ({ onRemoveAsDefault={() => handleRemoveOptionAsDefault(option.value) } + onInputEnter={handleInputEnter} /> } /> diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx index 0cb96fc76..121aa15e0 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx @@ -1,6 +1,6 @@ -import { useEffect, useMemo, useRef } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useMemo } from 'react'; import { ColorSample, IconCheck, @@ -31,6 +31,7 @@ type SettingsDataModelFieldSelectFormOptionRowProps = { onRemove?: () => void; onSetAsDefault?: () => void; onRemoveAsDefault?: () => void; + onInputEnter?: () => void; option: FieldMetadataItemOption; focused?: boolean; }; @@ -64,11 +65,10 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({ onRemove, onSetAsDefault, onRemoveAsDefault, + onInputEnter, option, focused, }: SettingsDataModelFieldSelectFormOptionRowProps) => { - const inputRef = useRef(null); - const theme = useTheme(); const dropdownIds = useMemo(() => { @@ -84,11 +84,9 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({ dropdownIds.actions, ); - useEffect(() => { - if (focused === true) { - inputRef.current?.focus(); - } - }, [focused]); + const handleInputEnter = () => { + onInputEnter?.(); + }; return ( @@ -123,8 +121,6 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({ } /> onChange({ @@ -133,8 +129,10 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({ value: getOptionValueFromLabel(label), }) } + focused={focused} RightIcon={isDefault ? IconCheck : undefined} maxLength={OPTION_VALUE_MAXIMUM_LENGTH} + onInputEnter={handleInputEnter} /> void; + focused?: boolean; }; -const TextInputComponent: ForwardRefRenderFunction< - HTMLInputElement, - TextInputComponentProps -> = ({ onFocus, onBlur, disableHotkeys = false, ...props }, ref) => { +export const TextInput = ({ + onFocus, + onBlur, + onInputEnter, + disableHotkeys = false, + focused, + ...props +}: TextInputProps) => { + const inputRef = useRef(null); + + const [isFocused, setIsFocused] = useState(false); + + useEffect(() => { + if (focused === true) { + inputRef.current?.focus(); + setIsFocused(true); + } + }, [focused]); + const { goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope, @@ -25,6 +42,7 @@ const TextInputComponent: ForwardRefRenderFunction< const handleFocus: FocusEventHandler = (e) => { onFocus?.(e); + setIsFocused(true); if (!disableHotkeys) { setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput); @@ -33,6 +51,7 @@ const TextInputComponent: ForwardRefRenderFunction< const handleBlur: FocusEventHandler = (e) => { onBlur?.(e); + setIsFocused(false); if (!disableHotkeys) { goBackToPreviousHotkeyScope(); @@ -40,19 +59,48 @@ const TextInputComponent: ForwardRefRenderFunction< }; useScopedHotkeys( - [Key.Escape, Key.Enter], + [Key.Escape], () => { - if (isDefined(ref) && 'current' in ref) { - ref.current?.blur(); + if (!isFocused) { + return; + } + + if (isDefined(inputRef) && 'current' in inputRef) { + inputRef.current?.blur(); + setIsFocused(false); } }, InputHotkeyScope.TextInput, - { enabled: !disableHotkeys }, + [inputRef, isFocused], + { + preventDefault: false, + }, + ); + + useScopedHotkeys( + [Key.Enter], + () => { + if (!isFocused) { + return; + } + + onInputEnter?.(); + + if (isDefined(inputRef) && 'current' in inputRef) { + inputRef.current?.blur(); + setIsFocused(false); + } + }, + InputHotkeyScope.TextInput, + [inputRef, isFocused, onInputEnter], + { + preventDefault: false, + }, ); return ( ); }; - -export const TextInput = forwardRef(TextInputComponent); diff --git a/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInput.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInput.stories.tsx index 85442f198..7664165d5 100644 --- a/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInput.stories.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextInput.stories.tsx @@ -1,10 +1,10 @@ -import { useState } from 'react'; import { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; import { ComponentDecorator } from 'twenty-ui'; -import { TextInput, TextInputComponentProps } from '../TextInput'; +import { TextInput, TextInputProps } from '../TextInput'; -type RenderProps = TextInputComponentProps; +type RenderProps = TextInputProps; const Render = (args: RenderProps) => { const [value, setValue] = useState(args.value); diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts index 13aefa697..bc8d7026c 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts @@ -1,4 +1,7 @@ -import { Hotkey } from 'react-hotkeys-hook/dist/types'; +import { + Hotkey, + OptionsOrDependencyArray, +} from 'react-hotkeys-hook/dist/types'; import { useRecoilCallback } from 'recoil'; import { logDebug } from '~/utils/logDebug'; @@ -7,15 +10,19 @@ import { internalHotkeysEnabledScopesState } from '../states/internal/internalHo export const DEBUG_HOTKEY_SCOPE = false; -export const useScopedHotkeyCallback = () => - useRecoilCallback( +export const useScopedHotkeyCallback = ( + dependencies?: OptionsOrDependencyArray, +) => { + const dependencyArray = Array.isArray(dependencies) ? dependencies : []; + + return useRecoilCallback( ({ snapshot }) => ({ callback, hotkeysEvent, keyboardEvent, scope, - preventDefault = true, + preventDefault, }: { keyboardEvent: KeyboardEvent; hotkeysEvent: Hotkey; @@ -53,7 +60,14 @@ export const useScopedHotkeyCallback = () => ); } - if (preventDefault) { + if (preventDefault === true) { + if (DEBUG_HOTKEY_SCOPE) { + logDebug( + `DEBUG: %cI prevent default for hotkey (${hotkeysEvent.keys})`, + 'color: gray;', + ); + } + keyboardEvent.stopPropagation(); keyboardEvent.preventDefault(); keyboardEvent.stopImmediatePropagation(); @@ -61,5 +75,7 @@ export const useScopedHotkeyCallback = () => return callback(keyboardEvent, hotkeysEvent); }, - [], + // eslint-disable-next-line react-hooks/exhaustive-deps + dependencyArray, ); +}; diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts index b8c7ff31d..6722214b3 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts @@ -1,30 +1,36 @@ import { useHotkeys } from 'react-hotkeys-hook'; -import { - HotkeyCallback, - Keys, - Options, - OptionsOrDependencyArray, -} from 'react-hotkeys-hook/dist/types'; +import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types'; import { useRecoilState } from 'recoil'; import { pendingHotkeyState } from '../states/internal/pendingHotkeysState'; +import { isDefined } from 'twenty-ui'; import { useScopedHotkeyCallback } from './useScopedHotkeyCallback'; +type UseHotkeysOptionsWithoutBuggyOptions = Omit; + export const useScopedHotkeys = ( keys: Keys, callback: HotkeyCallback, scope: string, - dependencies?: OptionsOrDependencyArray, - options: Options = { - enableOnContentEditable: true, - enableOnFormTags: true, - preventDefault: true, - }, + dependencies?: unknown[], + options?: UseHotkeysOptionsWithoutBuggyOptions, ) => { const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); - const callScopedHotkeyCallback = useScopedHotkeyCallback(); + const callScopedHotkeyCallback = useScopedHotkeyCallback(dependencies); + + const enableOnContentEditable = isDefined(options?.enableOnContentEditable) + ? options.enableOnContentEditable + : true; + + const enableOnFormTags = isDefined(options?.enableOnFormTags) + ? options.enableOnFormTags + : true; + + const preventDefault = isDefined(options?.preventDefault) + ? options.preventDefault === true + : true; return useHotkeys( keys, @@ -40,12 +46,12 @@ export const useScopedHotkeys = ( setPendingHotkey(null); }, scope, - preventDefault: !!options.preventDefault, + preventDefault, }); }, { - enableOnContentEditable: options.enableOnContentEditable, - enableOnFormTags: options.enableOnFormTags, + enableOnContentEditable, + enableOnFormTags, }, dependencies, );