Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useLingui } from '@lingui/react/macro';
import { IconX } from 'twenty-ui/icon';
import { IconButtonWithTooltip } from 'twenty-ui/input';

export const SidePanelCloseButton = () => {
const { t } = useLingui();
const { closeSidePanelMenu } = useSidePanelMenu();

const closeSidePanelLabel = t`Close side panel`;

return (
<IconButtonWithTooltip
tooltipContent={closeSidePanelLabel}
Icon={IconX}
size="small"
variant="primary"
onClick={closeSidePanelMenu}
ariaLabel={closeSidePanelLabel}
/>
);
};
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { isNonEmptyString } from '@sniptt/guards';
import { isDefined } from 'twenty-shared/utils';
import { IconLayoutSidebarRightExpand } from 'twenty-ui/icon';
import { IconButton } from 'twenty-ui/input';
import { IconMaximize } from 'twenty-ui/icon';
import { IconButtonWithTooltip } from 'twenty-ui/input';
import {
getOsControlSymbol,
getOsShortcutSeparator,
} from 'twenty-ui/utilities';

import { useSidePanelExpandTarget } from '@/side-panel/hooks/useSidePanelExpandTarget';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
Expand Down Expand Up @@ -29,13 +33,20 @@ const SidePanelExpandButtonContent = () => {
return null;
}

const tooltipContent = expandTarget.hasExpandShortcut
? `${expandTarget.label} | ${[getOsControlSymbol(), '⏎'].join(
getOsShortcutSeparator(),
)}`
: expandTarget.label;

return (
<>
{expandTarget.hasExpandShortcut && (
<SidePanelExpandShortcutEffect expand={expandTarget.expand} />
)}
<IconButton
Icon={IconLayoutSidebarRightExpand}
<IconButtonWithTooltip
tooltipContent={tooltipContent}
Icon={IconMaximize}
size="small"
variant="tertiary"
onClick={expandTarget.expand}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { SidePanelBackButton } from '@/side-panel/components/SidePanelBackButton';
import { SidePanelCloseButton } from '@/side-panel/components/SidePanelCloseButton';
import { SidePanelPageInfo } from '@/side-panel/components/SidePanelPageInfo';
import { SidePanelTopBarEscapeHotkeyEffect } from '@/side-panel/components/SidePanelTopBarEscapeHotkeyEffect';
import { SidePanelTopBarInputFocusEffect } from '@/side-panel/components/SidePanelTopBarInputFocusEffect';
import { SidePanelExpandButton } from '@/side-panel/components/SidePanelExpandButton';
import { SidePanelTopBarRightCornerIcon } from '@/side-panel/components/SidePanelTopBarRightCornerIcon';
Expand All @@ -9,7 +11,6 @@ import { SIDE_PANEL_TOP_BAR_HEIGHT } from '@/side-panel/constants/SidePanelTopBa
import { SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE } from '@/side-panel/constants/SidePanelTopBarHeightMobile';
import { useHandleSidePanelBackspace } from '@/side-panel/hooks/useHandleSidePanelBackspace';
import { useHandleSidePanelEscape } from '@/side-panel/hooks/useHandleSidePanelEscape';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { useSidePanelContextChips } from '@/side-panel/hooks/useSidePanelContextChips';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
Expand All @@ -24,8 +25,6 @@ import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useContext, useRef } from 'react';
import { Key } from 'ts-key-enum';
import { IconX } from 'twenty-ui/icon';
import { IconButton } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';

Expand Down Expand Up @@ -99,8 +98,6 @@ export const SidePanelTopBar = () => {

const isMobile = useIsMobile();

const { closeSidePanelMenu } = useSidePanelMenu();

const sidePanelPage = useAtomStateValue(sidePanelPageState);

const sidePanelNavigationStack = useAtomStateValue(
Expand Down Expand Up @@ -152,7 +149,12 @@ export const SidePanelTopBar = () => {
return;
}

if (event.key === Key.Backspace && handleSidePanelBackspace()) {
if (
event.key === Key.Backspace &&
!event.metaKey &&
!event.ctrlKey &&
handleSidePanelBackspace()
) {
event.preventDefault();
event.stopPropagation();
event.nativeEvent.stopImmediatePropagation();
Expand All @@ -177,6 +179,10 @@ export const SidePanelTopBar = () => {

return (
<StyledInputContainer isMobile={isMobile}>
<SidePanelTopBarEscapeHotkeyEffect
inputRef={inputRef}
onEscape={handleSidePanelEscape}
/>
<StyledContentContainer>
<AnimatePresence>
{shouldShowBackButton && (
Expand Down Expand Up @@ -213,15 +219,7 @@ export const SidePanelTopBar = () => {
<StyledRightControlsContainer>
<SidePanelTopBarRightCornerIcon />
<SidePanelExpandButton />
{!shouldHideCloseButton && (
<IconButton
Icon={IconX}
size="small"
variant="primary"
onClick={closeSidePanelMenu}
ariaLabel={t`Close side panel`}
/>
)}
{shouldHideCloseButton ? null : <SidePanelCloseButton />}
</StyledRightControlsContainer>
</StyledInputContainer>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { type RefObject } from 'react';
import { Key } from 'ts-key-enum';

import { useGlobalHotkeys } from '@/ui/utilities/hotkey/hooks/useGlobalHotkeys';

type SidePanelTopBarEscapeHotkeyEffectProps = {
inputRef: RefObject<HTMLInputElement | null>;
onEscape: () => void;
};

export const SidePanelTopBarEscapeHotkeyEffect = ({
inputRef,
onEscape,
}: SidePanelTopBarEscapeHotkeyEffectProps) => {
const handleEscape = (event: KeyboardEvent) => {
if (document.activeElement === inputRef.current) {
return;
}

event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();

onEscape();
};

useGlobalHotkeys({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RecordTableBodyEscapeHotkeyEffect (and the board/calendar ones) also listen for Escape on PageFocusId.RecordIndex and each side stops immediate propagation when it matches, so whichever registered first swallows the other — the table one even with no rows selected. On a record index with the panel open, which one should win, and is that deterministic today?

keys: [Key.Escape],
callback: handleEscape,
containsModifier: false,
dependencies: [handleEscape],
options: {
eventListenerOptions: {
capture: true,
},
preventDefault: false,
},
});

return null;
};
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,26 @@ describe('SidePanelTopBar', () => {
expect(mockCloseSidePanelMenu).toHaveBeenCalledTimes(1);
});

it('handles Escape before the underlying page hotkeys', () => {
const underlyingPageEscapeHandler = jest.fn();

document.addEventListener('keydown', underlyingPageEscapeHandler);

try {
renderSidePanelCommandMenu();

fireEvent.keyDown(document.body, {
key: 'Escape',
code: 'Escape',
});

expect(mockCloseSidePanelMenu).toHaveBeenCalledTimes(1);
expect(underlyingPageEscapeHandler).not.toHaveBeenCalled();
} finally {
document.removeEventListener('keydown', underlyingPageEscapeHandler);
}
});

it('does not navigate with Backspace while search has text', () => {
const { store } = renderSidePanelCommandMenu(
createSidePanelTopBarStore({
Expand Down Expand Up @@ -281,6 +301,36 @@ describe('SidePanelTopBar', () => {
expect(mockCloseSidePanelMenu).not.toHaveBeenCalled();
});

it('goes back with Escape while the search input is not focused', () => {
const { store } = renderSidePanelCommandMenu(
createSidePanelTopBarStore({
sidePanelPage: SidePanelPages.SearchRecords,
sidePanelNavigationStack: [
{
page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Command Menu',
pageIcon: IconDotsVertical,
pageId: 'command-menu',
},
{
page: SidePanelPages.SearchRecords,
pageTitle: 'Search',
pageIcon: IconDotsVertical,
pageId: 'search-records',
},
],
}),
);

fireEvent.keyDown(document.body, {
key: 'Escape',
code: 'Escape',
});

expect(store.get(sidePanelNavigationStackState.atom)).toHaveLength(1);
expect(mockCloseSidePanelMenu).not.toHaveBeenCalled();
});

it('renders the close button after the command menu content', () => {
renderSidePanelCommandMenu();

Expand Down
Loading
Loading