Fix command menu focus (#16264)

CommandMenu did not push its own focus item to the focus stack when
focused. (See NavigationDrawerInput)

---------

Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
Weiko
2025-12-02 18:19:07 +01:00
committed by GitHub
parent 900401c101
commit 3274c18a90
5 changed files with 51 additions and 8 deletions
@@ -344,7 +344,9 @@ export const ActivityRichTextEditor = ({
dependencies: [editor],
});
const handleAllKeys = (keyboardEvent: KeyboardEvent) => {
const focusRichTextEditorWhenFocusOnSidePanel = (
keyboardEvent: KeyboardEvent,
) => {
if (keyboardEvent.key === Key.Escape) {
return;
}
@@ -378,9 +380,9 @@ export const ActivityRichTextEditor = ({
useHotkeysOnFocusedElement({
keys: '*',
callback: handleAllKeys,
callback: focusRichTextEditorWhenFocusOnSidePanel,
focusId: SIDE_PANEL_FOCUS_ID,
dependencies: [handleAllKeys],
dependencies: [focusRichTextEditorWhenFocusOnSidePanel],
});
const { labelIdentifierFieldMetadataItem } =
@@ -431,7 +433,7 @@ export const ActivityRichTextEditor = ({
focusId: activityId,
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard: false,
enableGlobalHotkeysWithModifiers: false,
enableGlobalHotkeysWithModifiers: true,
},
});
},
@@ -5,12 +5,16 @@ import { CommandMenuTopBarRightCornerIcon } from '@/command-menu/components/Comm
import { COMMAND_MENU_SEARCH_BAR_HEIGHT } from '@/command-menu/constants/CommandMenuSearchBarHeight';
import { COMMAND_MENU_SEARCH_BAR_HEIGHT_MOBILE } from '@/command-menu/constants/CommandMenuSearchBarHeightMobile';
import { COMMAND_MENU_SEARCH_BAR_PADDING } from '@/command-menu/constants/CommandMenuSearchBarPadding';
import { COMMAND_MENU_SEARCH_INPUT_FOCUS_ID } from '@/command-menu/constants/CommandMenuSearchInputFocusId';
import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu';
import { useCommandMenuContextChips } from '@/command-menu/hooks/useCommandMenuContextChips';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { commandMenuPageState } from '@/command-menu/states/commandMenuPageState';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
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 { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
@@ -100,6 +104,29 @@ export const CommandMenuTopBar = () => {
const { contextChips } = useCommandMenuContextChips();
const { pushFocusItemToFocusStack } = usePushFocusItemToFocusStack();
const { removeFocusItemFromFocusStackById } =
useRemoveFocusItemFromFocusStackById();
const handleInputFocus = () => {
pushFocusItemToFocusStack({
focusId: COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
component: {
type: FocusComponentType.TEXT_INPUT,
instanceId: COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
},
globalHotkeysConfig: {
enableGlobalHotkeysConflictingWithKeyboard: false,
},
});
};
const handleInputBlur = () => {
removeFocusItemFromFocusStackById({
focusId: COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
});
};
const canGoBack = commandMenuNavigationStack.length > 1;
const shouldShowCloseButton =
@@ -148,11 +175,13 @@ export const CommandMenuTopBar = () => {
commandMenuPage === CommandMenuPages.SearchRecords) && (
<>
<StyledInput
data-testid="command-menu-search-input"
data-testid={COMMAND_MENU_SEARCH_INPUT_FOCUS_ID}
ref={inputRef}
value={commandMenuSearch}
placeholder={t`Type anything...`}
onChange={handleSearchChange}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
/>
<CommandMenuTopBarInputFocusEffect inputRef={inputRef} />
</>
@@ -20,6 +20,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { CommandMenuRouter } from '@/command-menu/components/CommandMenuRouter';
import { COMMAND_MENU_COMPONENT_INSTANCE_ID } from '@/command-menu/constants/CommandMenuComponentInstanceId';
import { COMMAND_MENU_SEARCH_INPUT_FOCUS_ID } from '@/command-menu/constants/CommandMenuSearchInputFocusId';
import { commandMenuNavigationStackState } from '@/command-menu/states/commandMenuNavigationStackState';
import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState';
import { CommandMenuPages } from '@/command-menu/types/CommandMenuPages';
@@ -151,7 +152,9 @@ export const LimitedPermissions: Story = {
export const MatchingNavigate: Story = {
play: async () => {
const canvas = within(document.body);
const searchInput = await canvas.findByTestId('command-menu-search-input');
const searchInput = await canvas.findByTestId(
COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
);
await sleep(openTimeout);
await userEvent.type(searchInput, 'ta');
expect(await canvas.findByText('Go to Tasks')).toBeVisible();
@@ -161,7 +164,9 @@ export const MatchingNavigate: Story = {
export const MatchingNavigateShortcuts: Story = {
play: async () => {
const canvas = within(document.body);
const searchInput = await canvas.findByTestId('command-menu-search-input');
const searchInput = await canvas.findByTestId(
COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
);
await sleep(openTimeout);
await userEvent.type(searchInput, 'gp');
expect(await canvas.findByText('Go to People')).toBeVisible();
@@ -186,7 +191,9 @@ export const MatchingNavigateShortcuts: Story = {
export const NoResultsSearchFallback: Story = {
play: async () => {
const canvas = within(document.body);
const searchInput = await canvas.findByTestId('command-menu-search-input');
const searchInput = await canvas.findByTestId(
COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
);
await sleep(openTimeout);
await userEvent.type(searchInput, 'input without results');
expect(await canvas.findByText('No results found')).toBeVisible();
@@ -0,0 +1 @@
export const COMMAND_MENU_SEARCH_INPUT_FOCUS_ID = 'command-menu-search-input';
@@ -2,6 +2,7 @@ import { useRecoilCallback } from 'recoil';
import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState';
import { COMMAND_MENU_SEARCH_INPUT_FOCUS_ID } from '@/command-menu/constants/CommandMenuSearchInputFocusId';
import { SIDE_PANEL_FOCUS_ID } from '@/command-menu/constants/SidePanelFocusId';
import { useNavigateCommandMenu } from '@/command-menu/hooks/useNavigateCommandMenu';
import { isCommandMenuClosingState } from '@/command-menu/states/isCommandMenuClosingState';
@@ -33,6 +34,9 @@ export const useCommandMenu = () => {
set(isCommandMenuClosingState, true);
set(isDragSelectionStartEnabledState, true);
closeAnyOpenDropdown();
removeFocusItemFromFocusStackById({
focusId: COMMAND_MENU_SEARCH_INPUT_FOCUS_ID,
});
removeFocusItemFromFocusStackById({
focusId: SIDE_PANEL_FOCUS_ID,
});