Fixed scoped hotkeys (#6322)
- Removed enabled props from useScopedHotkeys becayse it doesn't work - Moved enter useScopedHotkeys in a handler that we drill down to the text inputs on the settings form --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -68,9 +68,6 @@ export const RecordTableCellSoftFocusMode = ({
|
|||||||
},
|
},
|
||||||
TableHotkeyScope.TableSoftFocus,
|
TableHotkeyScope.TableSoftFocus,
|
||||||
[clearField, isFieldClearable, isFieldInputOnly],
|
[clearField, isFieldClearable, isFieldInputOnly],
|
||||||
{
|
|
||||||
enabled: !isFieldInputOnly,
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
|
|||||||
@ -1,8 +1,7 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Controller, useFormContext } from 'react-hook-form';
|
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
import { DropResult } from '@hello-pangea/dnd';
|
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 { IconPlus } from 'twenty-ui';
|
||||||
import { z } from 'zod';
|
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 { CardFooter } from '@/ui/layout/card/components/CardFooter';
|
||||||
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem';
|
||||||
import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList';
|
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 { FieldMetadataType } from '~/generated-metadata/graphql';
|
||||||
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
import { moveArrayItem } from '~/utils/array/moveArrayItem';
|
||||||
import { toSpliced } from '~/utils/array/toSpliced';
|
import { toSpliced } from '~/utils/array/toSpliced';
|
||||||
@ -189,19 +186,15 @@ export const SettingsDataModelFieldSelectForm = ({
|
|||||||
setFormValue('options', newOptions);
|
setFormValue('options', newOptions);
|
||||||
};
|
};
|
||||||
|
|
||||||
useScopedHotkeys(
|
const handleInputEnter = () => {
|
||||||
Key.Enter,
|
const newOptions = getOptionsWithNewOption();
|
||||||
() => {
|
|
||||||
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);
|
setFocusedOptionId(lastOptionId);
|
||||||
},
|
};
|
||||||
AppHotkeyScope.App,
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -270,6 +263,7 @@ export const SettingsDataModelFieldSelectForm = ({
|
|||||||
onRemoveAsDefault={() =>
|
onRemoveAsDefault={() =>
|
||||||
handleRemoveOptionAsDefault(option.value)
|
handleRemoveOptionAsDefault(option.value)
|
||||||
}
|
}
|
||||||
|
onInputEnter={handleInputEnter}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef } from 'react';
|
|
||||||
import { useTheme } from '@emotion/react';
|
import { useTheme } from '@emotion/react';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { useMemo } from 'react';
|
||||||
import {
|
import {
|
||||||
ColorSample,
|
ColorSample,
|
||||||
IconCheck,
|
IconCheck,
|
||||||
@ -31,6 +31,7 @@ type SettingsDataModelFieldSelectFormOptionRowProps = {
|
|||||||
onRemove?: () => void;
|
onRemove?: () => void;
|
||||||
onSetAsDefault?: () => void;
|
onSetAsDefault?: () => void;
|
||||||
onRemoveAsDefault?: () => void;
|
onRemoveAsDefault?: () => void;
|
||||||
|
onInputEnter?: () => void;
|
||||||
option: FieldMetadataItemOption;
|
option: FieldMetadataItemOption;
|
||||||
focused?: boolean;
|
focused?: boolean;
|
||||||
};
|
};
|
||||||
@ -64,11 +65,10 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
onRemove,
|
onRemove,
|
||||||
onSetAsDefault,
|
onSetAsDefault,
|
||||||
onRemoveAsDefault,
|
onRemoveAsDefault,
|
||||||
|
onInputEnter,
|
||||||
option,
|
option,
|
||||||
focused,
|
focused,
|
||||||
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
|
}: SettingsDataModelFieldSelectFormOptionRowProps) => {
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
const dropdownIds = useMemo(() => {
|
const dropdownIds = useMemo(() => {
|
||||||
@ -84,11 +84,9 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
dropdownIds.actions,
|
dropdownIds.actions,
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleInputEnter = () => {
|
||||||
if (focused === true) {
|
onInputEnter?.();
|
||||||
inputRef.current?.focus();
|
};
|
||||||
}
|
|
||||||
}, [focused]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledRow className={className}>
|
<StyledRow className={className}>
|
||||||
@ -123,8 +121,6 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<StyledOptionInput
|
<StyledOptionInput
|
||||||
ref={inputRef}
|
|
||||||
disableHotkeys
|
|
||||||
value={option.label}
|
value={option.label}
|
||||||
onChange={(label) =>
|
onChange={(label) =>
|
||||||
onChange({
|
onChange({
|
||||||
@ -133,8 +129,10 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({
|
|||||||
value: getOptionValueFromLabel(label),
|
value: getOptionValueFromLabel(label),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
focused={focused}
|
||||||
RightIcon={isDefault ? IconCheck : undefined}
|
RightIcon={isDefault ? IconCheck : undefined}
|
||||||
maxLength={OPTION_VALUE_MAXIMUM_LENGTH}
|
maxLength={OPTION_VALUE_MAXIMUM_LENGTH}
|
||||||
|
onInputEnter={handleInputEnter}
|
||||||
/>
|
/>
|
||||||
<Dropdown
|
<Dropdown
|
||||||
dropdownId={dropdownIds.actions}
|
dropdownId={dropdownIds.actions}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { FocusEventHandler, forwardRef, ForwardRefRenderFunction } from 'react';
|
import { FocusEventHandler, useEffect, useRef, useState } from 'react';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -10,14 +10,31 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH
|
|||||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||||
import { isDefined } from '~/utils/isDefined';
|
import { isDefined } from '~/utils/isDefined';
|
||||||
|
|
||||||
export type TextInputComponentProps = TextInputV2ComponentProps & {
|
export type TextInputProps = TextInputV2ComponentProps & {
|
||||||
disableHotkeys?: boolean;
|
disableHotkeys?: boolean;
|
||||||
|
onInputEnter?: () => void;
|
||||||
|
focused?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const TextInputComponent: ForwardRefRenderFunction<
|
export const TextInput = ({
|
||||||
HTMLInputElement,
|
onFocus,
|
||||||
TextInputComponentProps
|
onBlur,
|
||||||
> = ({ onFocus, onBlur, disableHotkeys = false, ...props }, ref) => {
|
onInputEnter,
|
||||||
|
disableHotkeys = false,
|
||||||
|
focused,
|
||||||
|
...props
|
||||||
|
}: TextInputProps) => {
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
|
||||||
|
const [isFocused, setIsFocused] = useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (focused === true) {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
setIsFocused(true);
|
||||||
|
}
|
||||||
|
}, [focused]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
goBackToPreviousHotkeyScope,
|
goBackToPreviousHotkeyScope,
|
||||||
setHotkeyScopeAndMemorizePreviousScope,
|
setHotkeyScopeAndMemorizePreviousScope,
|
||||||
@ -25,6 +42,7 @@ const TextInputComponent: ForwardRefRenderFunction<
|
|||||||
|
|
||||||
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
|
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||||
onFocus?.(e);
|
onFocus?.(e);
|
||||||
|
setIsFocused(true);
|
||||||
|
|
||||||
if (!disableHotkeys) {
|
if (!disableHotkeys) {
|
||||||
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
setHotkeyScopeAndMemorizePreviousScope(InputHotkeyScope.TextInput);
|
||||||
@ -33,6 +51,7 @@ const TextInputComponent: ForwardRefRenderFunction<
|
|||||||
|
|
||||||
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
|
const handleBlur: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||||
onBlur?.(e);
|
onBlur?.(e);
|
||||||
|
setIsFocused(false);
|
||||||
|
|
||||||
if (!disableHotkeys) {
|
if (!disableHotkeys) {
|
||||||
goBackToPreviousHotkeyScope();
|
goBackToPreviousHotkeyScope();
|
||||||
@ -40,19 +59,48 @@ const TextInputComponent: ForwardRefRenderFunction<
|
|||||||
};
|
};
|
||||||
|
|
||||||
useScopedHotkeys(
|
useScopedHotkeys(
|
||||||
[Key.Escape, Key.Enter],
|
[Key.Escape],
|
||||||
() => {
|
() => {
|
||||||
if (isDefined(ref) && 'current' in ref) {
|
if (!isFocused) {
|
||||||
ref.current?.blur();
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(inputRef) && 'current' in inputRef) {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
setIsFocused(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
InputHotkeyScope.TextInput,
|
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 (
|
return (
|
||||||
<TextInputV2
|
<TextInputV2
|
||||||
ref={ref}
|
ref={inputRef}
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
{...props}
|
{...props}
|
||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
@ -60,5 +108,3 @@ const TextInputComponent: ForwardRefRenderFunction<
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const TextInput = forwardRef(TextInputComponent);
|
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { useState } from 'react';
|
|
||||||
import { Meta, StoryObj } from '@storybook/react';
|
import { Meta, StoryObj } from '@storybook/react';
|
||||||
|
import { useState } from 'react';
|
||||||
import { ComponentDecorator } from 'twenty-ui';
|
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 Render = (args: RenderProps) => {
|
||||||
const [value, setValue] = useState(args.value);
|
const [value, setValue] = useState(args.value);
|
||||||
|
|||||||
@ -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 { useRecoilCallback } from 'recoil';
|
||||||
|
|
||||||
import { logDebug } from '~/utils/logDebug';
|
import { logDebug } from '~/utils/logDebug';
|
||||||
@ -7,15 +10,19 @@ import { internalHotkeysEnabledScopesState } from '../states/internal/internalHo
|
|||||||
|
|
||||||
export const DEBUG_HOTKEY_SCOPE = false;
|
export const DEBUG_HOTKEY_SCOPE = false;
|
||||||
|
|
||||||
export const useScopedHotkeyCallback = () =>
|
export const useScopedHotkeyCallback = (
|
||||||
useRecoilCallback(
|
dependencies?: OptionsOrDependencyArray,
|
||||||
|
) => {
|
||||||
|
const dependencyArray = Array.isArray(dependencies) ? dependencies : [];
|
||||||
|
|
||||||
|
return useRecoilCallback(
|
||||||
({ snapshot }) =>
|
({ snapshot }) =>
|
||||||
({
|
({
|
||||||
callback,
|
callback,
|
||||||
hotkeysEvent,
|
hotkeysEvent,
|
||||||
keyboardEvent,
|
keyboardEvent,
|
||||||
scope,
|
scope,
|
||||||
preventDefault = true,
|
preventDefault,
|
||||||
}: {
|
}: {
|
||||||
keyboardEvent: KeyboardEvent;
|
keyboardEvent: KeyboardEvent;
|
||||||
hotkeysEvent: Hotkey;
|
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.stopPropagation();
|
||||||
keyboardEvent.preventDefault();
|
keyboardEvent.preventDefault();
|
||||||
keyboardEvent.stopImmediatePropagation();
|
keyboardEvent.stopImmediatePropagation();
|
||||||
@ -61,5 +75,7 @@ export const useScopedHotkeyCallback = () =>
|
|||||||
|
|
||||||
return callback(keyboardEvent, hotkeysEvent);
|
return callback(keyboardEvent, hotkeysEvent);
|
||||||
},
|
},
|
||||||
[],
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
dependencyArray,
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|||||||
@ -1,30 +1,36 @@
|
|||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import {
|
import { HotkeyCallback, Keys, Options } from 'react-hotkeys-hook/dist/types';
|
||||||
HotkeyCallback,
|
|
||||||
Keys,
|
|
||||||
Options,
|
|
||||||
OptionsOrDependencyArray,
|
|
||||||
} from 'react-hotkeys-hook/dist/types';
|
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
import { pendingHotkeyState } from '../states/internal/pendingHotkeysState';
|
||||||
|
|
||||||
|
import { isDefined } from 'twenty-ui';
|
||||||
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
|
import { useScopedHotkeyCallback } from './useScopedHotkeyCallback';
|
||||||
|
|
||||||
|
type UseHotkeysOptionsWithoutBuggyOptions = Omit<Options, 'enabled'>;
|
||||||
|
|
||||||
export const useScopedHotkeys = (
|
export const useScopedHotkeys = (
|
||||||
keys: Keys,
|
keys: Keys,
|
||||||
callback: HotkeyCallback,
|
callback: HotkeyCallback,
|
||||||
scope: string,
|
scope: string,
|
||||||
dependencies?: OptionsOrDependencyArray,
|
dependencies?: unknown[],
|
||||||
options: Options = {
|
options?: UseHotkeysOptionsWithoutBuggyOptions,
|
||||||
enableOnContentEditable: true,
|
|
||||||
enableOnFormTags: true,
|
|
||||||
preventDefault: true,
|
|
||||||
},
|
|
||||||
) => {
|
) => {
|
||||||
const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState);
|
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(
|
return useHotkeys(
|
||||||
keys,
|
keys,
|
||||||
@ -40,12 +46,12 @@ export const useScopedHotkeys = (
|
|||||||
setPendingHotkey(null);
|
setPendingHotkey(null);
|
||||||
},
|
},
|
||||||
scope,
|
scope,
|
||||||
preventDefault: !!options.preventDefault,
|
preventDefault,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
enableOnContentEditable: options.enableOnContentEditable,
|
enableOnContentEditable,
|
||||||
enableOnFormTags: options.enableOnFormTags,
|
enableOnFormTags,
|
||||||
},
|
},
|
||||||
dependencies,
|
dependencies,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user