Refactor command menu input (#16773)
- Open input on item click - `CommandMenuItemNumberInput` and `CommandMenuItemTextInput` don't only define the input but the whole menu item now - Fixed behavior on escape ## Video QA https://github.com/user-attachments/assets/bf2f03e9-d07c-4a1e-9bc8-6606839269ff
This commit is contained in:
+82
-27
@@ -1,44 +1,55 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/ui/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useRef, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
import {
|
||||
canBeCastAsNumberOrNull,
|
||||
castAsNumberOrNull,
|
||||
} from '~/utils/cast-as-number-or-null';
|
||||
|
||||
type CommandMenuItemNumberInputProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
Icon?: IconComponent;
|
||||
value: string;
|
||||
onChange: (value: number | null) => void;
|
||||
onValidate?: (value: number | null) => boolean;
|
||||
placeholder?: string;
|
||||
};
|
||||
|
||||
const StyledRightAlignedTextInput = styled(TextInput)`
|
||||
input {
|
||||
text-align: right;
|
||||
}
|
||||
`;
|
||||
|
||||
export const CommandMenuItemNumberInput = ({
|
||||
id,
|
||||
label,
|
||||
Icon,
|
||||
value,
|
||||
onChange,
|
||||
onValidate,
|
||||
placeholder,
|
||||
}: CommandMenuItemNumberInputProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const focusId = `${id}-input`;
|
||||
const [draftValue, setDraftValue] = useState(value);
|
||||
const [hasError, setHasError] = useState(false);
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
setDraftValue(text);
|
||||
if (hasError) {
|
||||
setHasError(false);
|
||||
}
|
||||
};
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const handleCommit = () => {
|
||||
const handleCommit = (draftValue: string) => {
|
||||
if (!canBeCastAsNumberOrNull(draftValue)) {
|
||||
setHasError(true);
|
||||
setDraftValue(value);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -58,32 +69,76 @@ export const CommandMenuItemNumberInput = ({
|
||||
|
||||
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
event.target.select();
|
||||
pushFocusItemToFocusStack({
|
||||
focusId,
|
||||
component: {
|
||||
type: FocusComponentType.TEXT_INPUT,
|
||||
instanceId: focusId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
handleCommit();
|
||||
removeFocusItemFromFocusStackById({ focusId });
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === Key.Enter || event.key === Key.Escape) {
|
||||
event.stopPropagation();
|
||||
handleCommit();
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
const handleEscape = () => {
|
||||
setDraftValue(value);
|
||||
inputRef.current?.blur();
|
||||
};
|
||||
|
||||
const handleClickOutside = () => {
|
||||
handleCommit(draftValue);
|
||||
};
|
||||
|
||||
const handleEnter = () => {
|
||||
handleCommit(draftValue);
|
||||
inputRef.current?.blur();
|
||||
};
|
||||
|
||||
useRegisterInputEvents<string>({
|
||||
focusId,
|
||||
inputRef: inputRef,
|
||||
inputValue: draftValue,
|
||||
onEscape: handleEscape,
|
||||
onEnter: handleEnter,
|
||||
onClickOutside: handleClickOutside,
|
||||
});
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
setDraftValue(text);
|
||||
if (hasError) {
|
||||
setHasError(false);
|
||||
}
|
||||
};
|
||||
|
||||
const focusInput = () => {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledRightAlignedTextInput
|
||||
value={draftValue}
|
||||
sizeVariant="sm"
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
error={hasError ? ' ' : undefined}
|
||||
noErrorHelper
|
||||
<CommandMenuItem
|
||||
id={id}
|
||||
label={label}
|
||||
Icon={Icon}
|
||||
onClick={focusInput}
|
||||
RightComponent={
|
||||
<StyledRightAlignedTextInput
|
||||
ref={inputRef}
|
||||
value={draftValue}
|
||||
sizeVariant="sm"
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
error={hasError ? ' ' : undefined}
|
||||
noErrorHelper
|
||||
textClickOutsideId={focusId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+71
-25
@@ -1,9 +1,17 @@
|
||||
import { CommandMenuItem } from '@/command-menu/components/CommandMenuItem';
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/ui/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import { TextInput } from '@/ui/input/components/TextInput';
|
||||
import { usePushFocusItemToFocusStack } from '@/ui/utilities/focus/hooks/usePushFocusItemToFocusStack';
|
||||
import { useRemoveFocusItemFromFocusStackById } from '@/ui/utilities/focus/hooks/useRemoveFocusItemFromFocusStackById';
|
||||
import { FocusComponentType } from '@/ui/utilities/focus/types/FocusComponentType';
|
||||
import styled from '@emotion/styled';
|
||||
import { useState } from 'react';
|
||||
import { Key } from 'ts-key-enum';
|
||||
import { useRef, useState } from 'react';
|
||||
import { type IconComponent } from 'twenty-ui/display';
|
||||
|
||||
type CommandMenuItemTextInputProps = {
|
||||
id: string;
|
||||
label: string;
|
||||
Icon?: IconComponent;
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
@@ -16,46 +24,84 @@ const StyledRightAlignedTextInput = styled(TextInput)`
|
||||
`;
|
||||
|
||||
export const CommandMenuItemTextInput = ({
|
||||
id,
|
||||
label,
|
||||
Icon,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
}: CommandMenuItemTextInputProps) => {
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const focusId = `${id}-input`;
|
||||
const [draftValue, setDraftValue] = useState(value);
|
||||
|
||||
const handleChange = (text: string) => {
|
||||
setDraftValue(text);
|
||||
};
|
||||
|
||||
const handleCommit = () => {
|
||||
onChange(draftValue);
|
||||
};
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const handleFocus = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||
event.target.select();
|
||||
pushFocusItemToFocusStack({
|
||||
focusId,
|
||||
component: {
|
||||
type: FocusComponentType.TEXT_INPUT,
|
||||
instanceId: focusId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
handleCommit();
|
||||
removeFocusItemFromFocusStackById({ focusId });
|
||||
};
|
||||
|
||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (event.key === Key.Enter || event.key === Key.Escape) {
|
||||
event.stopPropagation();
|
||||
handleCommit();
|
||||
} else {
|
||||
event.stopPropagation();
|
||||
}
|
||||
const handleEscape = () => {
|
||||
setDraftValue(value);
|
||||
inputRef.current?.blur();
|
||||
};
|
||||
|
||||
const handleEnter = () => {
|
||||
onChange(draftValue);
|
||||
inputRef.current?.blur();
|
||||
};
|
||||
|
||||
const handleClickOutside = () => {
|
||||
onChange(draftValue);
|
||||
};
|
||||
|
||||
useRegisterInputEvents<string>({
|
||||
focusId,
|
||||
inputRef: inputRef,
|
||||
inputValue: draftValue,
|
||||
onEscape: handleEscape,
|
||||
onEnter: handleEnter,
|
||||
onClickOutside: handleClickOutside,
|
||||
});
|
||||
|
||||
const focusInput = () => {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledRightAlignedTextInput
|
||||
value={draftValue}
|
||||
sizeVariant="sm"
|
||||
onChange={handleChange}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={placeholder}
|
||||
<CommandMenuItem
|
||||
id={id}
|
||||
label={label}
|
||||
Icon={Icon}
|
||||
onClick={focusInput}
|
||||
RightComponent={
|
||||
<StyledRightAlignedTextInput
|
||||
ref={inputRef}
|
||||
value={draftValue}
|
||||
sizeVariant="sm"
|
||||
onChange={setDraftValue}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
placeholder={placeholder}
|
||||
textClickOutsideId={focusId}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+20
-28
@@ -110,28 +110,24 @@ export const ChartSettingItem = ({
|
||||
|
||||
return (
|
||||
<SelectableListItem key={item.id} itemId={item.id}>
|
||||
<CommandMenuItem
|
||||
<CommandMenuItemNumberInput
|
||||
id={item.id}
|
||||
label={t(item.label)}
|
||||
Icon={item.Icon}
|
||||
RightComponent={
|
||||
<CommandMenuItemNumberInput
|
||||
value={stringValue}
|
||||
onChange={handleInputChange}
|
||||
onValidate={(value) =>
|
||||
!isDefined(value) ||
|
||||
isMinMaxRangeValid(
|
||||
item.id as
|
||||
| CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE
|
||||
| CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE,
|
||||
value,
|
||||
configuration,
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
item.inputPlaceholder ? t(item.inputPlaceholder) : undefined
|
||||
}
|
||||
/>
|
||||
value={stringValue}
|
||||
onChange={handleInputChange}
|
||||
onValidate={(value) =>
|
||||
!isDefined(value) ||
|
||||
isMinMaxRangeValid(
|
||||
item.id as
|
||||
| CHART_CONFIGURATION_SETTING_IDS.MIN_RANGE
|
||||
| CHART_CONFIGURATION_SETTING_IDS.MAX_RANGE,
|
||||
value,
|
||||
configuration,
|
||||
)
|
||||
}
|
||||
placeholder={
|
||||
item.inputPlaceholder ? t(item.inputPlaceholder) : undefined
|
||||
}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
@@ -144,18 +140,14 @@ export const ChartSettingItem = ({
|
||||
|
||||
return (
|
||||
<SelectableListItem key={item.id} itemId={item.id}>
|
||||
<CommandMenuItem
|
||||
<CommandMenuItemTextInput
|
||||
id={item.id}
|
||||
label={t(item.label)}
|
||||
Icon={item.Icon}
|
||||
RightComponent={
|
||||
<CommandMenuItemTextInput
|
||||
value={stringValue}
|
||||
onChange={handleTextInputChange}
|
||||
placeholder={
|
||||
item.inputPlaceholder ? t(item.inputPlaceholder) : undefined
|
||||
}
|
||||
/>
|
||||
value={stringValue}
|
||||
onChange={handleTextInputChange}
|
||||
placeholder={
|
||||
item.inputPlaceholder ? t(item.inputPlaceholder) : undefined
|
||||
}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
|
||||
Reference in New Issue
Block a user