Fix workflow dropdown hotkey scope (#11518)

The hokey scope was set on focus of the outermost container of the field
inputs. When clicking on a dropdown inside this container, the hokey
scope was set first by the dropdown and then by the focused container.
This led to the previous hotkey scope being overwritten.
The fix consists in moving the hokey scope setter on focus to the inner
container.

Before:


https://github.com/user-attachments/assets/12538c5b-43ab-4b76-a867-7eda6992b1ea


After:


https://github.com/user-attachments/assets/002fedba-010c-41e9-bec6-d5d80cb2dcc5
This commit is contained in:
Raphaël Bosi
2025-04-11 11:07:36 +02:00
committed by GitHub
parent d34ec7b253
commit 637a7f0e64
14 changed files with 133 additions and 90 deletions
@@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@@ -84,7 +84,7 @@ export const FormBooleanFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
@@ -101,7 +101,7 @@ export const FormBooleanFieldInput = ({
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker
@@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@@ -32,7 +32,7 @@ import { isDefined } from 'twenty-shared/utils';
import { TEXT_INPUT_STYLE } from 'twenty-ui/theme';
import { Nullable } from 'twenty-ui/utilities';
const StyledInputContainer = styled(FormFieldInputInputContainer)`
const StyledInputContainer = styled(FormFieldInputInnerContainer)`
display: grid;
grid-template-columns: 1fr;
grid-template-rows: 1fr 0px;
@@ -1,7 +1,5 @@
import styled from '@emotion/styled';
import { ReactNode } from 'react';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { FormFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope';
const StyledFormFieldInputContainer = styled.div`
display: flex;
@@ -16,27 +14,8 @@ export const FormFieldInputContainer = ({
children: ReactNode;
testId?: string;
}) => {
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const onFocus = () => {
setHotkeyScopeAndMemorizePreviousScope(
FormFieldInputHotKeyScope.FormFieldInput,
);
};
const onBlur = () => {
goBackToPreviousHotkeyScope();
};
return (
<StyledFormFieldInputContainer
data-testid={testId}
onFocus={onFocus}
onBlur={onBlur}
>
<StyledFormFieldInputContainer data-testid={testId}>
{children}
</StyledFormFieldInputContainer>
);
@@ -0,0 +1,90 @@
import { FormFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormFieldInputHotKeyScope';
import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope';
import { css } from '@emotion/react';
import styled from '@emotion/styled';
import { forwardRef, HTMLAttributes, Ref } from 'react';
type FormFieldInputInnerContainerProps = {
hasRightElement: boolean;
multiline?: boolean;
readonly?: boolean;
preventSetHotkeyScope?: boolean;
};
const StyledFormFieldInputInnerContainer = styled.div<FormFieldInputInnerContainerProps>`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
${({ multiline, hasRightElement, theme }) =>
multiline || !hasRightElement
? css`
border-right: auto;
border-bottom-right-radius: ${theme.border.radius.sm};
border-top-right-radius: ${theme.border.radius.sm};
`
: css`
border-right: none;
border-bottom-right-radius: none;
border-top-right-radius: none;
`}
box-sizing: border-box;
display: flex;
overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')};
width: 100%;
`;
export const FormFieldInputInnerContainer = forwardRef(
(
{
className,
children,
onFocus,
onBlur,
hasRightElement,
multiline,
readonly,
preventSetHotkeyScope = false,
}: HTMLAttributes<HTMLDivElement> & FormFieldInputInnerContainerProps,
ref: Ref<HTMLDivElement>,
) => {
const {
goBackToPreviousHotkeyScope,
setHotkeyScopeAndMemorizePreviousScope,
} = usePreviousHotkeyScope();
const handleFocus = (e: React.FocusEvent<HTMLDivElement>) => {
onFocus?.(e);
if (!preventSetHotkeyScope) {
setHotkeyScopeAndMemorizePreviousScope(
FormFieldInputHotKeyScope.FormFieldInput,
);
}
};
const handleBlur = (e: React.FocusEvent<HTMLDivElement>) => {
onBlur?.(e);
if (!preventSetHotkeyScope) {
goBackToPreviousHotkeyScope();
}
};
return (
<StyledFormFieldInputInnerContainer
ref={ref}
className={className}
hasRightElement={hasRightElement}
multiline={multiline}
readonly={readonly}
onFocus={handleFocus}
onBlur={handleBlur}
>
{children}
</StyledFormFieldInputInnerContainer>
);
},
);
@@ -1,33 +0,0 @@
import { css } from '@emotion/react';
import styled from '@emotion/styled';
const StyledFormFieldInputInputContainer = styled.div<{
hasRightElement: boolean;
multiline?: boolean;
readonly?: boolean;
}>`
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
border-top-left-radius: ${({ theme }) => theme.border.radius.sm};
border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm};
${({ multiline, hasRightElement, theme }) =>
multiline || !hasRightElement
? css`
border-right: auto;
border-bottom-right-radius: ${theme.border.radius.sm};
border-top-right-radius: ${theme.border.radius.sm};
`
: css`
border-right: none;
border-bottom-right-radius: none;
border-top-right-radius: none;
`}
box-sizing: border-box;
display: flex;
overflow: ${({ multiline }) => (multiline ? 'auto' : 'hidden')};
width: 100%;
`;
export const FormFieldInputInputContainer = StyledFormFieldInputInputContainer;
@@ -1,7 +1,7 @@
import styled from '@emotion/styled';
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { FormMultiSelectFieldInputHotKeyScope } from '@/object-record/record-field/form-types/constants/FormMultiSelectFieldInputHotKeyScope';
@@ -17,9 +17,9 @@ import { isStandaloneVariableString } from '@/workflow/utils/isStandaloneVariabl
import { useTheme } from '@emotion/react';
import { useId, useState } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { VisibilityHidden } from 'twenty-ui/accessibility';
import { IconChevronDown } from 'twenty-ui/display';
import { SelectOption } from 'twenty-ui/input';
import { VisibilityHidden } from 'twenty-ui/accessibility';
type FormMultiSelectFieldInputProps = {
label?: string;
@@ -185,7 +185,7 @@ export const FormMultiSelectFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
@@ -231,7 +231,7 @@ export const FormMultiSelectFieldInput = ({
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
<StyledSelectInputContainer>
{draftValue.type === 'static' &&
draftValue.editingMode === 'edit' && (
@@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@@ -108,7 +108,7 @@ export const FormNumberFieldInput = ({
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
onBlur={onBlur}
>
@@ -128,7 +128,7 @@ export const FormNumberFieldInput = ({
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker
@@ -1,14 +1,14 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { useId } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
import { InputErrorHelper } from '@/ui/input/components/InputErrorHelper';
type FormRawJsonFieldInputProps = {
label?: string;
@@ -70,13 +70,13 @@ export const FormRawJsonFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer multiline>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
multiline
onBlur={onBlur}
>
<TextVariableEditor editor={editor} multiline readonly={readonly} />
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly && (
<VariablePicker
@@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@@ -134,14 +134,14 @@ export const FormSelectFieldInput = ({
disabled={readonly}
/>
) : (
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
>
<VariableChipStandalone
rawVariableName={draftValue.value}
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
)}
{isDefined(VariablePicker) && !readonly && (
@@ -1,6 +1,6 @@
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { FormSingleRecordFieldChip } from '@/object-record/record-field/form-types/components/FormSingleRecordFieldChip';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@@ -20,7 +20,7 @@ import { isDefined, isValidUuid } from 'twenty-shared/utils';
import { IconChevronDown, IconForbid } from 'twenty-ui/display';
import { LightIconButton } from 'twenty-ui/input';
const StyledFormSelectContainer = styled(FormFieldInputInputContainer)`
const StyledFormSelectContainer = styled(FormFieldInputInnerContainer)`
justify-content: space-between;
align-items: center;
padding-right: ${({ theme }) => theme.spacing(1)};
@@ -132,6 +132,7 @@ export const FormSingleRecordPicker = ({
<FormFieldInputRowContainer>
<StyledFormSelectContainer
hasRightElement={isDefined(VariablePicker) && !disabled}
preventSetHotkeyScope={true}
>
<FormSingleRecordFieldChip
draftValue={draftValue}
@@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { TextVariableEditor } from '@/object-record/record-field/form-types/components/TextVariableEditor';
import { useTextVariableEditor } from '@/object-record/record-field/form-types/hooks/useTextVariableEditor';
@@ -70,7 +70,7 @@ export const FormTextFieldInput = ({
{label ? <InputLabel>{label}</InputLabel> : null}
<FormFieldInputRowContainer multiline={multiline}>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
multiline={multiline}
onBlur={onBlur}
@@ -80,7 +80,7 @@ export const FormTextFieldInput = ({
multiline={multiline}
readonly={readonly}
/>
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker
@@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { VariableChipStandalone } from '@/object-record/record-field/form-types/components/VariableChipStandalone';
import { VariablePickerComponent } from '@/object-record/record-field/form-types/types/VariablePickerComponent';
@@ -94,7 +94,7 @@ export const FormUuidFieldInput = ({
{label ? <InputLabel htmlFor={inputId}>{label}</InputLabel> : null}
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={isDefined(VariablePicker) && !readonly}
>
{draftValue.type === 'static' ? (
@@ -113,7 +113,7 @@ export const FormUuidFieldInput = ({
onRemove={readonly ? undefined : handleUnlinkVariable}
/>
)}
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
{VariablePicker && !readonly ? (
<VariablePicker
@@ -1,5 +1,5 @@
import { FormFieldInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputContainer';
import { FormFieldInputInputContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInputContainer';
import { FormFieldInputInnerContainer } from '@/object-record/record-field/form-types/components/FormFieldInputInnerContainer';
import { FormFieldInputRowContainer } from '@/object-record/record-field/form-types/components/FormFieldInputRowContainer';
import { InputLabel } from '@/ui/input/components/InputLabel';
import { WorkflowFormAction } from '@/workflow/types/Workflow';
@@ -200,7 +200,7 @@ export const WorkflowEditActionFormBuilder = ({
onMouseLeave={() => setHoveredField(null)}
>
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={false}
onClick={() => {
handleFieldClick(field.id);
@@ -220,7 +220,7 @@ export const WorkflowEditActionFormBuilder = ({
/>
)}
</StyledFieldContainer>
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
</FormFieldInputRowContainer>
{!actionOptions.readonly &&
(isFieldSelected(field.id) || isFieldHovered(field.id)) && (
@@ -263,7 +263,7 @@ export const WorkflowEditActionFormBuilder = ({
<StyledRowContainer>
<FormFieldInputContainer>
<FormFieldInputRowContainer>
<FormFieldInputInputContainer
<FormFieldInputInnerContainer
hasRightElement={false}
onClick={() => {
const { label, name } = getDefaultFormFieldSettings(
@@ -296,7 +296,7 @@ export const WorkflowEditActionFormBuilder = ({
{t`Add Field`}
</StyledAddFieldButtonContentContainer>
</StyledFieldContainer>
</FormFieldInputInputContainer>
</FormFieldInputInnerContainer>
</FormFieldInputRowContainer>
</FormFieldInputContainer>
</StyledRowContainer>
@@ -1,6 +1,8 @@
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { StyledDropdownButtonContainer } from '@/ui/layout/dropdown/components/StyledDropdownButtonContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2';
import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
import { WorkflowVariablesDropdownFieldItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownFieldItems';
import { WorkflowVariablesDropdownObjectItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownObjectItems';
import { WorkflowVariablesDropdownWorkflowStepItems } from '@/workflow/workflow-variables/components/WorkflowVariablesDropdownWorkflowStepItems';
@@ -11,6 +13,7 @@ import { StepOutputSchema } from '@/workflow/workflow-variables/types/StepOutput
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils';
import { IconVariablePlus } from 'twenty-ui/display';
@@ -43,7 +46,10 @@ export const WorkflowVariablesDropdown = ({
const theme = useTheme();
const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`;
const { isDropdownOpen, closeDropdown } = useDropdown(dropdownId);
const isDropdownOpen = useRecoilValue(
extractComponentState(isDropdownOpenComponentState, dropdownId),
);
const { closeDropdown } = useDropdownV2();
const availableVariablesInWorkflowStep = useAvailableVariablesInWorkflowStep({
objectNameSingularToSelect,
});
@@ -68,7 +74,7 @@ export const WorkflowVariablesDropdown = ({
const handleSubItemSelect = (subItem: string) => {
onVariableSelect(subItem);
setSelectedStep(initialStep);
closeDropdown();
closeDropdown(dropdownId);
};
const handleBack = () => {