Fix side panel command menu header controls (#21747)

## Summary

Tested the 3 behaviors of the issue locally. Animation is not perfect on
the closing but I think this is a great v1

- Move the side-panel close action to the right side of the top bar
while keeping back navigation on the left.
- Keep the nav side-panel button as the command-menu entry point for
direct side-panel pages and hide it while command-menu pages/history are
active.
- Reset command-menu search/filter state when opening the root command
menu from the nav button.

Fixes twentyhq/core-team-issues#2504

## Videos

### Before


https://github.com/user-attachments/assets/08c1b6b3-5fbd-4154-a85d-5072a3b7690e

### After


https://github.com/user-attachments/assets/11682dea-f21c-47b5-91a8-869f30b09d96


<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/21747?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
Thomas des Francs
2026-06-18 15:30:05 +02:00
committed by GitHub
parent cc4659ce11
commit 454758471f
8 changed files with 373 additions and 168 deletions
@@ -1,7 +1,6 @@
import { useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useContext } from 'react';
import { isDefined } from 'twenty-shared/utils';
import { Tag } from 'twenty-ui/data-display';
import {
@@ -21,7 +20,7 @@ import {
import { H2Title } from 'twenty-ui/typography';
import { Section } from 'twenty-ui/layout';
import { type ThemeColor } from 'twenty-ui/theme';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useApolloAdminClient } from '@/settings/admin-panel/apollo/hooks/useApolloAdminClient';
import { GET_WORKSPACE_BILLING_ADMIN_PANEL } from '@/settings/admin-panel/graphql/queries/getWorkspaceBillingAdminPanel';
@@ -75,10 +74,6 @@ const StyledItemValue = styled.div`
gap: ${themeCssVariables.spacing[2]};
`;
const StyledProgressBarContainer = styled.div`
margin-bottom: ${themeCssVariables.spacing[3]};
`;
const STATUS_COLORS: Record<SubscriptionStatus, ThemeColor> = {
[SubscriptionStatus.Active]: 'green',
[SubscriptionStatus.Trialing]: 'blue',
@@ -143,7 +138,6 @@ export const SettingsAdminWorkspaceBillingContent = ({
}: SettingsAdminWorkspaceBillingContentProps) => {
const { t } = useLingui();
const { formatNumber } = useNumberFormat();
const { theme } = useContext(ThemeContext);
const apolloAdminClient = useApolloAdminClient();
const { data, loading } = useQuery<WorkspaceBillingAdminPanelQuery>(
@@ -7,6 +7,7 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
import { useOpenDropdown } from '@/ui/layout/dropdown/hooks/useOpenDropdown';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { IconChevronLeft } from 'twenty-ui/icon';
import { IconButton } from 'twenty-ui/input';
import { MenuItem } from 'twenty-ui/navigation';
@@ -54,6 +55,7 @@ export const SidePanelBackButton = () => {
size="small"
variant="tertiary"
onClick={goBackFromSidePanel}
ariaLabel={t`Back`}
/>
</StyledNavigationIcon>
}
@@ -1,20 +1,20 @@
import { SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE } from '@/side-panel/constants/SidePanelTopBarHeightMobile';
import { COMMAND_MENU_SIDE_PANEL_PAGES } from '@/side-panel/constants/CommandMenuSidePanelPages';
import { useSidePanelMenu } from '@/side-panel/hooks/useSidePanelMenu';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { SidePanelPages } from 'twenty-shared/types';
import { isLayoutCustomizationModeEnabledState } from '@/layout-customization/states/isLayoutCustomizationModeEnabledState';
import { RootStackingContextZIndices } from '@/ui/layout/constants/RootStackingContextZIndices';
import { PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderSidePanelButtonClickOutsideId';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { motion } from 'framer-motion';
import { useContext } from 'react';
import { IconDotsVertical } from 'twenty-ui/icon';
import { IconButton } from 'twenty-ui/input';
import { AppTooltip, TooltipDelay, TooltipPosition } from 'twenty-ui/surfaces';
import { AnimatedButton } from 'twenty-ui/input';
import { useIsMobile } from 'twenty-ui/utilities';
import { ThemeContext, themeCssVariables } from 'twenty-ui/theme-constants';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledButtonWrapper = styled.div<{ alignToTop: boolean }>`
align-items: ${({ alignToTop }) => (alignToTop ? 'center' : 'initial')};
@@ -32,100 +32,13 @@ const StyledTooltipWrapper = styled.div`
font-size: ${themeCssVariables.font.size.md};
`;
const xPaths = {
topLeft: `M12 12 L6 6`,
topRight: `M12 12 L18 6`,
bottomLeft: `M12 12 L6 18`,
bottomRight: `M12 12 L18 18`,
};
const AnimatedIcon = ({
isSidePanelOpened,
}: {
isSidePanelOpened: boolean;
}) => {
const { theme } = useContext(ThemeContext);
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={theme.icon.size.sm}
height={theme.icon.size.sm}
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth={theme.icon.stroke.md}
strokeLinecap="round"
strokeLinejoin="round"
style={{ display: 'block' }}
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<motion.circle
cx={12}
cy={12}
r="1"
initial={{ opacity: 0 }}
animate={{
scale: isSidePanelOpened ? 0 : 1,
opacity: isSidePanelOpened ? 0 : 1,
}}
transition={{
duration: theme.animation.duration.fast,
}}
/>
{Object.values(xPaths).map((path, index) => (
<motion.path
key={index}
d={path}
initial={{ pathLength: 0, opacity: 0 }}
animate={{
pathLength: isSidePanelOpened ? 1 : 0,
opacity: isSidePanelOpened ? 1 : 0,
}}
transition={{
duration: theme.animation.duration.fast,
ease: 'easeInOut',
delay: isSidePanelOpened ? 0.1 : 0,
}}
/>
))}
<motion.circle
cx="12"
cy="5"
r="1"
initial={{ opacity: 0 }}
animate={{
scale: isSidePanelOpened ? 0 : 1,
opacity: isSidePanelOpened ? 0 : 1,
}}
transition={{
duration: theme.animation.duration.fast,
}}
/>
<motion.circle
cx="12"
cy="19"
r="1"
initial={{ opacity: 0 }}
animate={{
scale: isSidePanelOpened ? 0 : 1,
opacity: isSidePanelOpened ? 0 : 1,
}}
transition={{
duration: theme.animation.duration.fast,
}}
/>
</svg>
);
};
export const SidePanelToggleButton = () => {
const { toggleSidePanelMenu } = useSidePanelMenu();
const { openSidePanelMenu } = useSidePanelMenu();
const isSidePanelOpened = useAtomStateValue(isSidePanelOpenedState);
const sidePanelPage = useAtomStateValue(sidePanelPageState);
const sidePanelNavigationStack = useAtomStateValue(
sidePanelNavigationStackState,
);
const isLayoutCustomizationModeEnabled = useAtomStateValue(
isLayoutCustomizationModeEnabledState,
);
@@ -133,43 +46,39 @@ export const SidePanelToggleButton = () => {
const isMobile = useIsMobile();
const isCommandMenuOpened =
isSidePanelOpened &&
[
SidePanelPages.CommandMenuDisplay,
SidePanelPages.CommandMenuEdit,
SidePanelPages.SearchRecords,
].includes(sidePanelPage);
const showAsOpen = isSidePanelOpened && !isCommandMenuOpened;
isSidePanelOpened && COMMAND_MENU_SIDE_PANEL_PAGES.includes(sidePanelPage);
const alignWithSidePanelTopBar =
isMobile && isLayoutCustomizationModeEnabled && isSidePanelOpened;
const ariaLabel = isSidePanelOpened
? t`Close side panel`
: t`Open side panel`;
const hasCommandMenuPageInNavigationStack = sidePanelNavigationStack.some(
({ page }) => COMMAND_MENU_SIDE_PANEL_PAGES.includes(page),
);
const shouldHideButton =
isCommandMenuOpened ||
(isSidePanelOpened && hasCommandMenuPageInNavigationStack);
if (shouldHideButton) {
return null;
}
const ariaLabel = t`Command Menu`;
const { theme } = useContext(ThemeContext);
return (
<StyledButtonWrapper alignToTop={alignWithSidePanelTopBar}>
<div id="toggle-side-panel-button">
<AnimatedButton
animatedSvg={<AnimatedIcon isSidePanelOpened={showAsOpen} />}
dataClickOutsideId={PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID}
<div
id="toggle-side-panel-button"
data-click-outside-id={PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID}
>
<IconButton
Icon={IconDotsVertical}
dataTestId="page-header-side-panel-button"
size={isMobile ? 'medium' : 'small'}
square
variant="secondary"
accent="default"
ariaLabel={ariaLabel}
onClick={toggleSidePanelMenu}
animate={{
rotate: showAsOpen ? 90 : 0,
}}
transition={{
duration: theme.animation.duration.normal,
ease: 'easeInOut',
}}
onClick={openSidePanelMenu}
/>
</div>
@@ -2,6 +2,7 @@ import { SidePanelBackButton } from '@/side-panel/components/SidePanelBackButton
import { SidePanelPageInfo } from '@/side-panel/components/SidePanelPageInfo';
import { SidePanelTopBarInputFocusEffect } from '@/side-panel/components/SidePanelTopBarInputFocusEffect';
import { SidePanelTopBarRightCornerIcon } from '@/side-panel/components/SidePanelTopBarRightCornerIcon';
import { COMMAND_MENU_SIDE_PANEL_PAGES } from '@/side-panel/constants/CommandMenuSidePanelPages';
import { SIDE_PANEL_TOP_BAR_HEIGHT } from '@/side-panel/constants/SidePanelTopBarHeight';
import { SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE } from '@/side-panel/constants/SidePanelTopBarHeightMobile';
import { SIDE_PANEL_FOCUS_ID } from '@/side-panel/constants/SidePanelFocusId';
@@ -19,18 +20,11 @@ import { AnimatePresence, motion } from 'framer-motion';
import { useAtomState } from '@/ui/utilities/state/jotai/hooks/useAtomState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useContext, useRef } from 'react';
import { SidePanelPages } from 'twenty-shared/types';
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';
const COMMAND_MENU_SIDE_PANEL_PAGES = [
SidePanelPages.CommandMenuDisplay,
SidePanelPages.CommandMenuEdit,
SidePanelPages.SearchRecords,
];
const StyledInputContainer = styled.div<{ isMobile: boolean }>`
align-items: center;
background-color: ${themeCssVariables.background.secondary};
@@ -46,7 +40,6 @@ const StyledInputContainer = styled.div<{ isMobile: boolean }>`
height: ${({ isMobile }) =>
isMobile ? SIDE_PANEL_TOP_BAR_HEIGHT_MOBILE : SIDE_PANEL_TOP_BAR_HEIGHT}px;
justify-content: space-between;
justify-content: space-between;
margin: 0;
outline: none;
@@ -82,6 +75,13 @@ const StyledContentContainer = styled.div`
overflow: hidden;
`;
const StyledRightControlsContainer = styled.div`
align-items: center;
display: flex;
flex-shrink: 0;
gap: ${themeCssVariables.spacing[1]};
`;
export const SidePanelTopBar = () => {
const [sidePanelSearch, setSidePanelSearch] =
useAtomState(sidePanelSearchState);
@@ -133,9 +133,6 @@ export const SidePanelTopBar = () => {
const currentPage = sidePanelNavigationStack.at(-1)?.page;
const previousPage = sidePanelNavigationStack.at(-2)?.page;
// A command-menu page is the root of a fresh command-menu session, so it only
// offers a back chevron when it was opened from another command-menu page.
// Every other side-panel page keeps standard "go back when there is history".
const canGoBack =
currentPage !== undefined &&
COMMAND_MENU_SIDE_PANEL_PAGES.includes(currentPage)
@@ -143,10 +140,10 @@ export const SidePanelTopBar = () => {
COMMAND_MENU_SIDE_PANEL_PAGES.includes(previousPage)
: sidePanelNavigationStack.length > 1;
const shouldShowCloseButton = !isMobile && !canGoBack;
const shouldShowBackButton = canGoBack;
const shouldShowCloseButton = !isMobile;
const lastChip = contextChips.at(-1);
return (
@@ -155,6 +152,7 @@ export const SidePanelTopBar = () => {
<AnimatePresence>
{shouldShowBackButton && (
<motion.div
key="side-panel-back-button"
exit={{ opacity: 0, width: 0 }}
transition={{
duration: theme.animation.duration.instant,
@@ -163,21 +161,6 @@ export const SidePanelTopBar = () => {
<SidePanelBackButton />
</motion.div>
)}
{shouldShowCloseButton && (
<motion.div
exit={{ opacity: 0, width: 0 }}
transition={{
duration: theme.animation.duration.instant,
}}
>
<IconButton
Icon={IconX}
size="small"
variant="tertiary"
onClick={closeSidePanelMenu}
/>
</motion.div>
)}
</AnimatePresence>
{lastChip && !COMMAND_MENU_SIDE_PANEL_PAGES.includes(sidePanelPage) && (
<SidePanelPageInfo pageChip={lastChip} />
@@ -197,7 +180,18 @@ export const SidePanelTopBar = () => {
</>
)}
</StyledContentContainer>
<SidePanelTopBarRightCornerIcon />
<StyledRightControlsContainer>
<SidePanelTopBarRightCornerIcon />
{shouldShowCloseButton && (
<IconButton
Icon={IconX}
size="small"
variant="secondary"
onClick={closeSidePanelMenu}
ariaLabel={t`Close side panel`}
/>
)}
</StyledRightControlsContainer>
</StyledInputContainer>
);
};
@@ -0,0 +1,194 @@
import { i18n } from '@lingui/core';
import { I18nProvider } from '@lingui/react';
import { fireEvent, render, screen } from '@testing-library/react';
import { createStore, Provider as JotaiProvider } from 'jotai';
import { MemoryRouter } from 'react-router-dom';
import { SidePanelToggleButton } from '@/side-panel/components/SidePanelToggleButton';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { sidePanelSearchObjectFilterState } from '@/side-panel/states/sidePanelSearchObjectFilterState';
import { sidePanelSearchState } from '@/side-panel/states/sidePanelSearchState';
import { PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID } from '@/ui/layout/page-header/constants/PageHeaderSidePanelButtonClickOutsideId';
import { SidePanelPages } from 'twenty-shared/types';
import { IconDotsVertical } from 'twenty-ui/icon';
jest.mock('twenty-ui/utilities', () => ({
useIsMobile: () => false,
}));
jest.mock('twenty-ui/surfaces', () => ({
...jest.requireActual('twenty-ui/surfaces'),
AppTooltip: () => null,
}));
const renderSidePanelToggleButton = ({
isSidePanelOpened = false,
sidePanelPage = SidePanelPages.CommandMenuDisplay,
sidePanelNavigationStack = [],
sidePanelSearch = '',
sidePanelSearchObjectFilter = null,
}: {
isSidePanelOpened?: boolean;
sidePanelPage?: SidePanelPages;
sidePanelNavigationStack?: Array<{
page: SidePanelPages;
pageTitle: string;
pageIcon: typeof IconDotsVertical;
pageId: string;
}>;
sidePanelSearch?: string;
sidePanelSearchObjectFilter?: string | null;
} = {}) => {
const store = createStore();
store.set(isSidePanelOpenedState.atom, isSidePanelOpened);
store.set(sidePanelPageState.atom, sidePanelPage);
store.set(sidePanelNavigationStackState.atom, sidePanelNavigationStack);
store.set(sidePanelSearchState.atom, sidePanelSearch);
store.set(sidePanelSearchObjectFilterState.atom, sidePanelSearchObjectFilter);
render(
<I18nProvider i18n={i18n}>
<JotaiProvider store={store}>
<MemoryRouter
future={{
v7_relativeSplatPath: true,
v7_startTransition: true,
}}
>
<SidePanelToggleButton />
</MemoryRouter>
</JotaiProvider>
</I18nProvider>,
);
return { store };
};
describe('SidePanelToggleButton', () => {
it('opens the command menu when the side panel is closed', () => {
const { store } = renderSidePanelToggleButton();
fireEvent.click(screen.getByTestId('page-header-side-panel-button'));
expect(store.get(isSidePanelOpenedState.atom)).toBe(true);
expect(store.get(sidePanelPageState.atom)).toBe(
SidePanelPages.CommandMenuDisplay,
);
expect(store.get(sidePanelNavigationStackState.atom)).toHaveLength(1);
});
it('hides the navbar command menu button while the command menu is open', () => {
renderSidePanelToggleButton({
isSidePanelOpened: true,
sidePanelPage: SidePanelPages.CommandMenuDisplay,
sidePanelNavigationStack: [
{
page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Command Menu',
pageIcon: IconDotsVertical,
pageId: 'command-menu',
},
],
});
expect(
screen.queryByTestId('page-header-side-panel-button'),
).not.toBeInTheDocument();
});
it('hides the navbar command menu button when the side panel has command-menu history', () => {
renderSidePanelToggleButton({
isSidePanelOpened: true,
sidePanelPage: SidePanelPages.ViewRecord,
sidePanelNavigationStack: [
{
page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Command Menu',
pageIcon: IconDotsVertical,
pageId: 'command-menu',
},
{
page: SidePanelPages.ViewRecord,
pageTitle: 'Company',
pageIcon: IconDotsVertical,
pageId: 'view-record',
},
],
});
expect(
screen.queryByTestId('page-header-side-panel-button'),
).not.toBeInTheDocument();
});
it('keeps the navbar command menu button on unrelated side-panel drill-down pages', () => {
renderSidePanelToggleButton({
isSidePanelOpened: true,
sidePanelPage: SidePanelPages.WorkflowStepCreate,
sidePanelNavigationStack: [
{
page: SidePanelPages.WorkflowStepEdit,
pageTitle: 'Edit step',
pageIcon: IconDotsVertical,
pageId: 'workflow-step-edit',
},
{
page: SidePanelPages.WorkflowStepCreate,
pageTitle: 'Create step',
pageIcon: IconDotsVertical,
pageId: 'workflow-step-create',
},
],
});
expect(screen.getByTestId('page-header-side-panel-button')).toBeVisible();
});
it('marks the command menu button as a click-outside exclusion', () => {
renderSidePanelToggleButton();
expect(
screen
.getByTestId('page-header-side-panel-button')
.closest('[data-click-outside-id]'),
).toHaveAttribute(
'data-click-outside-id',
PAGE_HEADER_SIDE_PANEL_BUTTON_CLICK_OUTSIDE_ID,
);
});
it('replaces a directly opened side-panel page with the root command menu', () => {
const { store } = renderSidePanelToggleButton({
isSidePanelOpened: true,
sidePanelPage: SidePanelPages.ViewRecord,
sidePanelSearch: 'acme',
sidePanelSearchObjectFilter: 'company',
sidePanelNavigationStack: [
{
page: SidePanelPages.ViewRecord,
pageTitle: 'Company',
pageIcon: IconDotsVertical,
pageId: 'view-record',
},
],
});
fireEvent.click(screen.getByTestId('page-header-side-panel-button'));
expect(store.get(isSidePanelOpenedState.atom)).toBe(true);
expect(store.get(sidePanelPageState.atom)).toBe(
SidePanelPages.CommandMenuDisplay,
);
expect(store.get(sidePanelNavigationStackState.atom)).toMatchObject([
{
page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Command Menu',
},
]);
expect(store.get(sidePanelSearchState.atom)).toBe('');
expect(store.get(sidePanelSearchObjectFilterState.atom)).toBeNull();
});
});
@@ -36,8 +36,10 @@ jest.mock('@/side-panel/hooks/useSidePanelMenu', () => ({
}),
}));
let mockIsMobile = false;
jest.mock('twenty-ui/utilities', () => ({
useIsMobile: () => true,
useIsMobile: () => mockIsMobile,
}));
const recordIndexFocusItem = {
@@ -52,27 +54,36 @@ const recordIndexFocusItem = {
},
};
const createSidePanelTopBarStore = () => {
const store = createStore();
store.set(isSidePanelOpenedState.atom, true);
store.set(sidePanelPageState.atom, SidePanelPages.CommandMenuDisplay);
store.set(sidePanelNavigationStackState.atom, [
const createSidePanelTopBarStore = ({
sidePanelPage = SidePanelPages.CommandMenuDisplay,
sidePanelNavigationStack = [
{
page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Command Menu',
pageIcon: IconDotsVertical,
pageId: 'command-menu',
},
]);
],
}: {
sidePanelPage?: SidePanelPages;
sidePanelNavigationStack?: Array<{
page: SidePanelPages;
pageTitle: string;
pageIcon: typeof IconDotsVertical;
pageId: string;
}>;
} = {}) => {
const store = createStore();
store.set(isSidePanelOpenedState.atom, true);
store.set(sidePanelPageState.atom, sidePanelPage);
store.set(sidePanelNavigationStackState.atom, sidePanelNavigationStack);
store.set(focusStackState.atom, [recordIndexFocusItem]);
return store;
};
const renderSidePanelCommandMenu = () => {
const store = createSidePanelTopBarStore();
const renderSidePanelCommandMenu = (store = createSidePanelTopBarStore()) => {
render(
<I18nProvider i18n={i18n}>
<JotaiProvider store={store}>
@@ -93,6 +104,10 @@ const renderSidePanelCommandMenu = () => {
};
describe('SidePanelTopBar', () => {
beforeEach(() => {
mockIsMobile = false;
});
it('keeps the command menu search input focused while arrowing through items', async () => {
const { store } = renderSidePanelCommandMenu();
@@ -134,4 +149,91 @@ describe('SidePanelTopBar', () => {
},
});
});
it('shows only the close button on the root command menu', () => {
renderSidePanelCommandMenu();
expect(
screen.getByRole('button', { name: 'Close side panel' }),
).toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'Back' }),
).not.toBeInTheDocument();
});
it('does not show the close button on mobile', () => {
mockIsMobile = true;
renderSidePanelCommandMenu();
expect(
screen.queryByRole('button', { name: 'Close side panel' }),
).not.toBeInTheDocument();
});
it('renders the close button after the command menu content', () => {
renderSidePanelCommandMenu();
const input = screen.getByTestId(SIDE_PANEL_FOCUS_ID);
const closeButton = screen.getByRole('button', {
name: 'Close side panel',
});
expect(
Boolean(
input.compareDocumentPosition(closeButton) &
Node.DOCUMENT_POSITION_FOLLOWING,
),
).toBe(true);
});
it('shows both back and close buttons for command menu subpages', () => {
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',
},
],
}),
);
expect(screen.getByRole('button', { name: 'Back' })).toBeInTheDocument();
expect(
screen.getByRole('button', { name: 'Close side panel' }),
).toBeInTheDocument();
});
it('shows only the close button when a page was opened directly', () => {
renderSidePanelCommandMenu(
createSidePanelTopBarStore({
sidePanelPage: SidePanelPages.ViewRecord,
sidePanelNavigationStack: [
{
page: SidePanelPages.ViewRecord,
pageTitle: 'Company',
pageIcon: IconDotsVertical,
pageId: 'view-record',
},
],
}),
);
expect(
screen.getByRole('button', { name: 'Close side panel' }),
).toBeInTheDocument();
expect(
screen.queryByRole('button', { name: 'Back' }),
).not.toBeInTheDocument();
});
});
@@ -0,0 +1,7 @@
import { SidePanelPages } from 'twenty-shared/types';
export const COMMAND_MENU_SIDE_PANEL_PAGES = [
SidePanelPages.CommandMenuDisplay,
SidePanelPages.CommandMenuEdit,
SidePanelPages.SearchRecords,
];
@@ -60,6 +60,9 @@ export const useSidePanelMenu = () => {
const openSidePanelMenu = useCallback(() => {
emitSidePanelOpenEvent();
closeAnyOpenDropdown();
store.set(sidePanelSearchState.atom, '');
store.set(sidePanelSearchObjectFilterState.atom, null);
const isLayoutCustomizationModeEnabled = store.get(
isLayoutCustomizationModeEnabledState.atom,
);