Replace hotkey scopes by focus stack (Part 4 - Inputs) (#12933)
# Replace hotkey scopes by focus stack (Part 4 - Inputs) This PR is the 4th part of a refactoring aiming to deprecate the hotkey scopes api in favor of the new focus stack api which is more robust. Part 1: https://github.com/twentyhq/twenty/pull/12673 Part 2: https://github.com/twentyhq/twenty/pull/12798 Part 3: https://github.com/twentyhq/twenty/pull/12910 In this part, I refactored all inputs in the app so that each input has a unique id which can be used to track the focused element.
This commit is contained in:
@@ -1,271 +0,0 @@
|
||||
import styled from '@emotion/styled';
|
||||
import { useRef, useState } from 'react';
|
||||
import { HotkeysEvent } from 'react-hotkeys-hook/dist/types';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
import { Key } from 'ts-key-enum';
|
||||
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
|
||||
import { IconArrowRight } from 'twenty-ui/display';
|
||||
import { Button, RoundedIconButton } from 'twenty-ui/input';
|
||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||
|
||||
const MAX_ROWS = 5;
|
||||
|
||||
export enum AutosizeTextInputVariant {
|
||||
Default = 'default',
|
||||
Icon = 'icon',
|
||||
Button = 'button',
|
||||
}
|
||||
|
||||
type AutosizeTextInputProps = {
|
||||
onValidate?: (text: string) => void;
|
||||
minRows?: number;
|
||||
placeholder?: string;
|
||||
onFocus?: () => void;
|
||||
variant?: AutosizeTextInputVariant;
|
||||
buttonTitle?: string;
|
||||
value?: string;
|
||||
className?: string;
|
||||
onBlur?: () => void;
|
||||
autoFocus?: boolean;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const StyledContainer = styled.div`
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
const StyledInputContainer = styled.div`
|
||||
display: flex;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type StyledTextAreaProps = {
|
||||
variant: AutosizeTextInputVariant;
|
||||
};
|
||||
|
||||
const StyledTextArea = styled(TextareaAutosize)<StyledTextAreaProps>`
|
||||
background: ${({ theme, variant }) =>
|
||||
variant === AutosizeTextInputVariant.Button
|
||||
? 'transparent'
|
||||
: theme.background.tertiary};
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
color: ${({ theme }) => theme.font.color.primary};
|
||||
font-family: inherit;
|
||||
font-size: ${({ theme }) => theme.font.size.md};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
line-height: 16px;
|
||||
overflow: auto;
|
||||
|
||||
&:focus {
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
}
|
||||
padding: ${({ variant }) =>
|
||||
variant === AutosizeTextInputVariant.Button ? '8px 0' : '8px'};
|
||||
resize: none;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
// TODO: this messes with the layout, fix it
|
||||
const StyledBottomRightRoundedIconButton = styled.div`
|
||||
height: 0;
|
||||
position: relative;
|
||||
right: 26px;
|
||||
top: 6px;
|
||||
width: 0px;
|
||||
`;
|
||||
|
||||
const StyledSendButton = styled(Button)`
|
||||
margin-left: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledWordCounter = styled.div`
|
||||
color: ${({ theme }) => theme.font.color.light};
|
||||
font-weight: ${({ theme }) => theme.font.weight.medium};
|
||||
line-height: 150%;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
type StyledBottomContainerProps = {
|
||||
isTextAreaHidden: boolean;
|
||||
};
|
||||
|
||||
const StyledBottomContainer = styled.div<StyledBottomContainerProps>`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: ${({ theme, isTextAreaHidden }) =>
|
||||
isTextAreaHidden ? 0 : theme.spacing(4)};
|
||||
`;
|
||||
|
||||
const StyledCommentText = styled.div`
|
||||
cursor: text;
|
||||
padding-bottom: ${({ theme }) => theme.spacing(1)};
|
||||
padding-top: ${({ theme }) => theme.spacing(1)};
|
||||
`;
|
||||
|
||||
export const AutosizeTextInput = ({
|
||||
placeholder,
|
||||
onValidate,
|
||||
minRows = 1,
|
||||
onFocus,
|
||||
variant = AutosizeTextInputVariant.Default,
|
||||
buttonTitle,
|
||||
value = '',
|
||||
className,
|
||||
onBlur,
|
||||
autoFocus,
|
||||
disabled,
|
||||
}: AutosizeTextInputProps) => {
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const [isHidden, setIsHidden] = useState(
|
||||
variant === AutosizeTextInputVariant.Button,
|
||||
);
|
||||
const [text, setText] = useState(value);
|
||||
const textInputRef = useRef<HTMLTextAreaElement>(null);
|
||||
const {
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
goBackToPreviousHotkeyScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
const isSendButtonDisabled = !text;
|
||||
const words = text.split(/\s|\n/).filter((word) => word).length;
|
||||
|
||||
useScopedHotkeys(
|
||||
['shift+enter', 'enter'],
|
||||
(event: KeyboardEvent, handler: HotkeysEvent) => {
|
||||
if (handler.shift || !isFocused) {
|
||||
return;
|
||||
} else {
|
||||
event.preventDefault();
|
||||
|
||||
onValidate?.(text);
|
||||
|
||||
setText('');
|
||||
}
|
||||
},
|
||||
InputHotkeyScope.TextInput,
|
||||
[onValidate, text, setText, isFocused],
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
);
|
||||
|
||||
useScopedHotkeys(
|
||||
Key.Escape,
|
||||
(event: KeyboardEvent) => {
|
||||
if (!isFocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
setText('');
|
||||
goBackToPreviousHotkeyScope();
|
||||
textInputRef.current?.blur();
|
||||
},
|
||||
InputHotkeyScope.TextInput,
|
||||
[onValidate, setText, isFocused],
|
||||
{
|
||||
enableOnContentEditable: true,
|
||||
enableOnFormTags: true,
|
||||
},
|
||||
);
|
||||
|
||||
const handleInputChange = (event: React.FormEvent<HTMLTextAreaElement>) => {
|
||||
const newText = event.currentTarget.value;
|
||||
|
||||
setText(newText);
|
||||
};
|
||||
|
||||
const handleOnClickSendButton = () => {
|
||||
onValidate?.(text);
|
||||
|
||||
setText('');
|
||||
};
|
||||
|
||||
const handleFocus = () => {
|
||||
onFocus?.();
|
||||
setIsFocused(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
scope: InputHotkeyScope.TextInput,
|
||||
});
|
||||
};
|
||||
|
||||
const handleBlur = () => {
|
||||
onBlur?.();
|
||||
setIsFocused(false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
};
|
||||
|
||||
const computedMinRows = minRows > MAX_ROWS ? MAX_ROWS : minRows;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StyledContainer className={className}>
|
||||
<StyledInputContainer>
|
||||
{!isHidden && (
|
||||
<StyledTextArea
|
||||
ref={textInputRef}
|
||||
autoFocus={
|
||||
autoFocus || variant === AutosizeTextInputVariant.Button
|
||||
}
|
||||
placeholder={placeholder ?? 'Write a comment'}
|
||||
maxRows={MAX_ROWS}
|
||||
minRows={computedMinRows}
|
||||
onChange={handleInputChange}
|
||||
value={text}
|
||||
onFocus={handleFocus}
|
||||
onBlur={handleBlur}
|
||||
variant={variant}
|
||||
disabled={disabled}
|
||||
/>
|
||||
)}
|
||||
{variant === AutosizeTextInputVariant.Icon && (
|
||||
<StyledBottomRightRoundedIconButton>
|
||||
<RoundedIconButton
|
||||
onClick={handleOnClickSendButton}
|
||||
Icon={IconArrowRight}
|
||||
disabled={isSendButtonDisabled}
|
||||
/>
|
||||
</StyledBottomRightRoundedIconButton>
|
||||
)}
|
||||
</StyledInputContainer>
|
||||
|
||||
{variant === AutosizeTextInputVariant.Button && (
|
||||
<StyledBottomContainer isTextAreaHidden={isHidden}>
|
||||
<StyledWordCounter>
|
||||
{isHidden ? (
|
||||
<StyledCommentText
|
||||
onClick={() => {
|
||||
setIsHidden(false);
|
||||
onFocus?.();
|
||||
}}
|
||||
>
|
||||
Write a comment
|
||||
</StyledCommentText>
|
||||
) : (
|
||||
`${words} word${words === 1 ? '' : 's'}`
|
||||
)}
|
||||
</StyledWordCounter>
|
||||
<StyledSendButton
|
||||
title={buttonTitle ?? 'Comment'}
|
||||
disabled={isSendButtonDisabled}
|
||||
onClick={handleOnClickSendButton}
|
||||
/>
|
||||
</StyledBottomContainer>
|
||||
)}
|
||||
</StyledContainer>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -6,7 +6,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
|
||||
import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput';
|
||||
import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator';
|
||||
import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { arrayToChunks } from '~/utils/array/arrayToChunks';
|
||||
|
||||
import { ICON_PICKER_DROPDOWN_CONTENT_WIDTH } from '@/ui/input/components/constants/IconPickerDropdownContentWidth';
|
||||
@@ -14,7 +13,7 @@ import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent
|
||||
import { DropdownHotkeyScope } from '@/ui/layout/dropdown/constants/DropdownHotkeyScope';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { DropdownOffset } from '@/ui/layout/dropdown/types/DropdownOffset';
|
||||
import { useSelectableListListenToEnterHotkeyOnItem } from '@/ui/layout/selectable-list/hooks/useSelectableListListenToEnterHotkeyOnItem';
|
||||
import { SelectableListItem } from '@/ui/layout/selectable-list/components/SelectableListItem';
|
||||
import { selectedItemIdComponentState } from '@/ui/layout/selectable-list/states/selectedItemIdComponentState';
|
||||
import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2';
|
||||
import { t } from '@lingui/core/macro';
|
||||
@@ -25,7 +24,6 @@ import {
|
||||
IconButtonVariant,
|
||||
LightIconButton,
|
||||
} from 'twenty-ui/input';
|
||||
import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope';
|
||||
|
||||
export type IconPickerProps = {
|
||||
disabled?: boolean;
|
||||
@@ -51,9 +49,16 @@ const StyledMenuIconItemsContainer = styled.div`
|
||||
gap: ${({ theme }) => theme.spacing(0.5)};
|
||||
`;
|
||||
|
||||
const StyledLightIconButton = styled(LightIconButton)<{ isSelected?: boolean }>`
|
||||
background: ${({ theme, isSelected }) =>
|
||||
isSelected ? theme.background.transparent.medium : 'transparent'};
|
||||
const StyledLightIconButton = styled(LightIconButton)<{
|
||||
isSelected?: boolean;
|
||||
isFocused?: boolean;
|
||||
}>`
|
||||
background: ${({ theme, isSelected, isFocused }) =>
|
||||
isSelected
|
||||
? theme.background.transparent.medium
|
||||
: isFocused
|
||||
? theme.background.transparent.light
|
||||
: 'transparent'};
|
||||
`;
|
||||
|
||||
const convertIconKeyToLabel = (iconKey: string) =>
|
||||
@@ -64,6 +69,7 @@ type IconPickerIconProps = {
|
||||
onClick: () => void;
|
||||
selectedIconKey?: string;
|
||||
Icon: IconComponent;
|
||||
focusedIconKey?: string;
|
||||
};
|
||||
|
||||
const IconPickerIcon = ({
|
||||
@@ -71,29 +77,26 @@ const IconPickerIcon = ({
|
||||
onClick,
|
||||
selectedIconKey,
|
||||
Icon,
|
||||
focusedIconKey,
|
||||
}: IconPickerIconProps) => {
|
||||
const isSelectedItemId = useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
iconKey,
|
||||
);
|
||||
|
||||
useSelectableListListenToEnterHotkeyOnItem({
|
||||
focusId: iconKey,
|
||||
itemId: iconKey,
|
||||
onEnter: onClick,
|
||||
hotkeyScope: DropdownHotkeyScope.Dropdown,
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledLightIconButton
|
||||
key={iconKey}
|
||||
aria-label={convertIconKeyToLabel(iconKey)}
|
||||
size="medium"
|
||||
title={iconKey}
|
||||
isSelected={iconKey === selectedIconKey || !!isSelectedItemId}
|
||||
Icon={Icon}
|
||||
onClick={onClick}
|
||||
/>
|
||||
<SelectableListItem itemId={iconKey} onEnter={onClick}>
|
||||
<StyledLightIconButton
|
||||
key={iconKey}
|
||||
aria-label={convertIconKeyToLabel(iconKey)}
|
||||
size="medium"
|
||||
title={iconKey}
|
||||
isSelected={iconKey === selectedIconKey || !!isSelectedItemId}
|
||||
isFocused={iconKey === focusedIconKey}
|
||||
Icon={Icon}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</SelectableListItem>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -114,26 +117,18 @@ export const IconPicker = ({
|
||||
maxIconsVisible = 25,
|
||||
}: IconPickerProps) => {
|
||||
const [searchString, setSearchString] = useState('');
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
|
||||
const [isMouseInsideIconList, setIsMouseInsideIconList] = useState(false);
|
||||
|
||||
const handleMouseEnter = () => {
|
||||
if (!isMouseInsideIconList) {
|
||||
setIsMouseInsideIconList(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
scope: IconPickerHotkeyScope.IconPicker,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseLeave = () => {
|
||||
if (isMouseInsideIconList) {
|
||||
setIsMouseInsideIconList(false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -192,6 +187,14 @@ export const IconPicker = ({
|
||||
|
||||
const icon = selectedIconKey ? getIcon(selectedIconKey) : IconApps;
|
||||
|
||||
const selectableListInstanceId = 'icon-list';
|
||||
|
||||
const focusedIconKey =
|
||||
useRecoilComponentValueV2(
|
||||
selectedItemIdComponentState,
|
||||
selectableListInstanceId,
|
||||
) ?? undefined;
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<Dropdown
|
||||
@@ -217,7 +220,7 @@ export const IconPicker = ({
|
||||
widthInPixels={dropdownWidth || ICON_PICKER_DROPDOWN_CONTENT_WIDTH}
|
||||
>
|
||||
<SelectableList
|
||||
selectableListInstanceId="icon-list"
|
||||
selectableListInstanceId={selectableListInstanceId}
|
||||
selectableItemIdMatrix={iconKeys2d}
|
||||
focusId={dropdownId}
|
||||
hotkeyScope={DropdownHotkeyScope.Dropdown}
|
||||
@@ -246,6 +249,7 @@ export const IconPicker = ({
|
||||
}}
|
||||
selectedIconKey={selectedIconKey}
|
||||
Icon={getIcon(iconKey)}
|
||||
focusedIconKey={focusedIconKey}
|
||||
/>
|
||||
))}
|
||||
</StyledMenuIconItemsContainer>
|
||||
|
||||
@@ -2,14 +2,17 @@ import styled from '@emotion/styled';
|
||||
import { FocusEventHandler, useId } from 'react';
|
||||
import TextareaAutosize from 'react-textarea-autosize';
|
||||
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { InputHotkeyScope } from '@/ui/input/types/InputHotkeyScope';
|
||||
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 { RGBA } from 'twenty-ui/theme';
|
||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||
import { InputHotkeyScope } from '../types/InputHotkeyScope';
|
||||
|
||||
const MAX_ROWS = 5;
|
||||
|
||||
export type TextAreaProps = {
|
||||
textAreaId: string;
|
||||
label?: string;
|
||||
disabled?: boolean;
|
||||
minRows?: number;
|
||||
@@ -69,6 +72,7 @@ const StyledTextArea = styled(TextareaAutosize)`
|
||||
`;
|
||||
|
||||
export const TextArea = ({
|
||||
textAreaId,
|
||||
label,
|
||||
disabled,
|
||||
placeholder,
|
||||
@@ -81,30 +85,37 @@ export const TextArea = ({
|
||||
}: TextAreaProps) => {
|
||||
const computedMinRows = Math.min(minRows, maxRows);
|
||||
|
||||
const inputId = useId();
|
||||
const instanceId = useId();
|
||||
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const handleFocus: FocusEventHandler<HTMLTextAreaElement> = () => {
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
scope: InputHotkeyScope.TextInput,
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: textAreaId,
|
||||
component: {
|
||||
type: FocusComponentType.TEXT_AREA,
|
||||
instanceId: textAreaId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
hotkeyScope: { scope: InputHotkeyScope.TextInput },
|
||||
});
|
||||
};
|
||||
|
||||
const handleBlur: FocusEventHandler<HTMLTextAreaElement> = () => {
|
||||
goBackToPreviousHotkeyScope();
|
||||
removeFocusItemFromFocusStackById({ focusId: textAreaId });
|
||||
onBlur?.();
|
||||
};
|
||||
|
||||
return (
|
||||
<StyledContainer>
|
||||
{label && <StyledLabel htmlFor={inputId}>{label}</StyledLabel>}
|
||||
{label && <StyledLabel htmlFor={instanceId}>{label}</StyledLabel>}
|
||||
|
||||
<StyledTextArea
|
||||
id={inputId}
|
||||
id={instanceId}
|
||||
placeholder={placeholder}
|
||||
maxRows={maxRows}
|
||||
minRows={computedMinRows}
|
||||
|
||||
@@ -6,11 +6,14 @@ import {
|
||||
TextInputV2ComponentProps,
|
||||
} from '@/ui/input/components/TextInputV2';
|
||||
import { InputHotkeyScope } from '@/ui/input/types/InputHotkeyScope';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
|
||||
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 { useHotkeysOnFocusedElement } from '@/ui/utilities/hotkey/hooks/useHotkeysOnFocusedElement';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
export type TextInputProps = TextInputV2ComponentProps & {
|
||||
instanceId: string;
|
||||
disableHotkeys?: boolean;
|
||||
onInputEnter?: () => void;
|
||||
dataTestId?: string;
|
||||
@@ -19,6 +22,7 @@ export type TextInputProps = TextInputV2ComponentProps & {
|
||||
};
|
||||
|
||||
export const TextInput = ({
|
||||
instanceId,
|
||||
onFocus,
|
||||
onBlur,
|
||||
onInputEnter,
|
||||
@@ -45,18 +49,25 @@ export const TextInput = ({
|
||||
}
|
||||
}, [autoSelectOnMount]);
|
||||
|
||||
const {
|
||||
goBackToPreviousHotkeyScope,
|
||||
setHotkeyScopeAndMemorizePreviousScope,
|
||||
} = usePreviousHotkeyScope();
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const handleFocus: FocusEventHandler<HTMLInputElement> = (e) => {
|
||||
onFocus?.(e);
|
||||
setIsFocused(true);
|
||||
|
||||
if (!disableHotkeys) {
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
scope: InputHotkeyScope.TextInput,
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: instanceId,
|
||||
component: {
|
||||
type: FocusComponentType.TEXT_INPUT,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
hotkeyScope: { scope: InputHotkeyScope.TextInput },
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -66,48 +77,51 @@ export const TextInput = ({
|
||||
setIsFocused(false);
|
||||
|
||||
if (!disableHotkeys) {
|
||||
goBackToPreviousHotkeyScope();
|
||||
removeFocusItemFromFocusStackById({ focusId: instanceId });
|
||||
}
|
||||
};
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Escape],
|
||||
() => {
|
||||
if (!isFocused) {
|
||||
return;
|
||||
}
|
||||
const handleEscape = () => {
|
||||
if (!isFocused) {
|
||||
return;
|
||||
}
|
||||
if (isDefined(inputRef) && 'current' in inputRef) {
|
||||
inputRef.current?.blur();
|
||||
setIsFocused(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (isDefined(inputRef) && 'current' in inputRef) {
|
||||
inputRef.current?.blur();
|
||||
setIsFocused(false);
|
||||
}
|
||||
},
|
||||
InputHotkeyScope.TextInput,
|
||||
[inputRef, isFocused],
|
||||
{
|
||||
const handleEnter = () => {
|
||||
if (!isFocused) {
|
||||
return;
|
||||
}
|
||||
onInputEnter?.();
|
||||
if (isDefined(inputRef) && 'current' in inputRef) {
|
||||
setIsFocused(false);
|
||||
}
|
||||
};
|
||||
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Escape],
|
||||
callback: handleEscape,
|
||||
focusId: instanceId,
|
||||
scope: InputHotkeyScope.TextInput,
|
||||
dependencies: [handleEscape],
|
||||
options: {
|
||||
preventDefault: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
useScopedHotkeys(
|
||||
[Key.Enter],
|
||||
() => {
|
||||
if (!isFocused) {
|
||||
return;
|
||||
}
|
||||
|
||||
onInputEnter?.();
|
||||
|
||||
if (isDefined(inputRef) && 'current' in inputRef) {
|
||||
setIsFocused(false);
|
||||
}
|
||||
},
|
||||
InputHotkeyScope.TextInput,
|
||||
[inputRef, isFocused, onInputEnter],
|
||||
{
|
||||
useHotkeysOnFocusedElement({
|
||||
keys: [Key.Enter],
|
||||
callback: handleEnter,
|
||||
focusId: instanceId,
|
||||
scope: InputHotkeyScope.TextInput,
|
||||
dependencies: [handleEnter],
|
||||
options: {
|
||||
preventDefault: false,
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<TextInputV2
|
||||
|
||||
@@ -11,10 +11,10 @@ import {
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { IconComponent, IconEye, IconEyeOff } from 'twenty-ui/display';
|
||||
import { AutogrowWrapper } from 'twenty-ui/utilities';
|
||||
import { useCombinedRefs } from '~/hooks/useCombinedRefs';
|
||||
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
|
||||
import { AutogrowWrapper } from 'twenty-ui/utilities';
|
||||
import { IconComponent, IconEye, IconEyeOff } from 'twenty-ui/display';
|
||||
|
||||
const StyledContainer = styled.div<
|
||||
Pick<TextInputV2ComponentProps, 'fullWidth'>
|
||||
@@ -297,12 +297,12 @@ const TextInputV2Component = forwardRef<
|
||||
onBlur?.(event);
|
||||
};
|
||||
|
||||
const inputId = useId();
|
||||
const instanceId = useId();
|
||||
|
||||
return (
|
||||
<StyledContainer className={className} fullWidth={fullWidth ?? false}>
|
||||
{label && (
|
||||
<InputLabel htmlFor={inputId}>
|
||||
<InputLabel htmlFor={instanceId}>
|
||||
{label + (required ? '*' : '')}
|
||||
</InputLabel>
|
||||
)}
|
||||
@@ -322,7 +322,7 @@ const TextInputV2Component = forwardRef<
|
||||
)}
|
||||
|
||||
<StyledInput
|
||||
id={inputId}
|
||||
id={instanceId}
|
||||
width={width}
|
||||
data-testid={dataTestId}
|
||||
autoComplete={autoComplete || 'off'}
|
||||
|
||||
@@ -6,11 +6,14 @@ import { useRef, useState } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents';
|
||||
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
|
||||
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 { OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
|
||||
type InputProps = {
|
||||
instanceId: string;
|
||||
value?: string;
|
||||
onChange: (value: string) => void;
|
||||
placeholder?: string;
|
||||
@@ -56,6 +59,7 @@ const StyledDiv = styled.div<{
|
||||
`;
|
||||
|
||||
const Input = ({
|
||||
instanceId,
|
||||
value,
|
||||
onChange,
|
||||
placeholder,
|
||||
@@ -78,14 +82,16 @@ const Input = ({
|
||||
}
|
||||
};
|
||||
|
||||
const { goBackToPreviousHotkeyScope } = usePreviousHotkeyScope();
|
||||
const { removeFocusItemFromFocusStackById } =
|
||||
useRemoveFocusItemFromFocusStackById();
|
||||
|
||||
const handleLeaveFocus = () => {
|
||||
setIsOpened(false);
|
||||
goBackToPreviousHotkeyScope();
|
||||
removeFocusItemFromFocusStackById({ focusId: instanceId });
|
||||
};
|
||||
|
||||
useRegisterInputEvents<string>({
|
||||
focusId: instanceId,
|
||||
inputRef: wrapperRef,
|
||||
inputValue: draftValue,
|
||||
onEnter: () => {
|
||||
@@ -131,6 +137,7 @@ const Input = ({
|
||||
};
|
||||
|
||||
export const TitleInput = ({
|
||||
instanceId,
|
||||
disabled,
|
||||
value,
|
||||
sizeVariant = 'md',
|
||||
@@ -145,12 +152,13 @@ export const TitleInput = ({
|
||||
}: TitleInputProps) => {
|
||||
const [isOpened, setIsOpened] = useState(false);
|
||||
|
||||
const { setHotkeyScopeAndMemorizePreviousScope } = usePreviousHotkeyScope();
|
||||
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
|
||||
|
||||
return (
|
||||
<>
|
||||
{isOpened ? (
|
||||
<Input
|
||||
instanceId={instanceId}
|
||||
sizeVariant={sizeVariant}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
@@ -170,8 +178,16 @@ export const TitleInput = ({
|
||||
onClick={() => {
|
||||
if (!disabled) {
|
||||
setIsOpened(true);
|
||||
setHotkeyScopeAndMemorizePreviousScope({
|
||||
scope: hotkeyScope,
|
||||
pushFocusItemToFocusStack({
|
||||
focusId: instanceId,
|
||||
component: {
|
||||
type: FocusComponentType.TEXT_INPUT,
|
||||
instanceId: instanceId,
|
||||
},
|
||||
globalHotkeysConfig: {
|
||||
enableGlobalHotkeysConflictingWithKeyboard: false,
|
||||
},
|
||||
hotkeyScope: { scope: hotkeyScope },
|
||||
});
|
||||
}
|
||||
}}
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
|
||||
import {
|
||||
AutosizeTextInput,
|
||||
AutosizeTextInputVariant,
|
||||
} from '../AutosizeTextInput';
|
||||
import {
|
||||
CatalogDecorator,
|
||||
CatalogStory,
|
||||
ComponentDecorator,
|
||||
} from 'twenty-ui/testing';
|
||||
|
||||
const meta: Meta<typeof AutosizeTextInput> = {
|
||||
title: 'UI/Input/AutosizeTextInput/AutosizeTextInput',
|
||||
component: AutosizeTextInput,
|
||||
decorators: [ComponentDecorator],
|
||||
};
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof AutosizeTextInput>;
|
||||
|
||||
export const Default: Story = {};
|
||||
|
||||
export const ButtonVariant: Story = {
|
||||
args: { variant: AutosizeTextInputVariant.Button },
|
||||
};
|
||||
|
||||
export const Catalog: CatalogStory<Story, typeof AutosizeTextInput> = {
|
||||
parameters: {
|
||||
catalog: {
|
||||
dimensions: [
|
||||
{
|
||||
name: 'variants',
|
||||
values: Object.values(AutosizeTextInputVariant),
|
||||
props: (variant: AutosizeTextInputVariant) => ({ variant }),
|
||||
labels: (variant: AutosizeTextInputVariant) =>
|
||||
`variant -> ${variant}`,
|
||||
},
|
||||
{
|
||||
name: 'minRows',
|
||||
values: [1, 4],
|
||||
props: (minRows: number) => ({ minRows }),
|
||||
labels: (minRows: number) => `minRows -> ${minRows}`,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
decorators: [CatalogDecorator],
|
||||
};
|
||||
+1
@@ -72,6 +72,7 @@ export const RelativeDatePickerHeader = (
|
||||
fullWidth
|
||||
/>
|
||||
<TextInput
|
||||
instanceId="relative-date-picker-amount"
|
||||
width={50}
|
||||
value={textInputValue}
|
||||
onChange={(text) => {
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
export enum IconPickerHotkeyScope {
|
||||
IconPicker = 'icon-picker',
|
||||
}
|
||||
Reference in New Issue
Block a user