From 864ea452b4af901292624561b019661954512aeb Mon Sep 17 00:00:00 2001 From: Weiko Date: Thu, 25 Jun 2026 14:55:04 +0200 Subject: [PATCH] fix mobile side panel close (#22169) The side panel close (X) button was hidden on all mobile views, while the back button only renders when there is navigation history. When the side panel is opened at the root (e.g. viewing a record directly with a single-item navigation stack), neither button was shown, leaving no way to dismiss the panel on mobile. Keep the close button available on mobile whenever there is no back button to fall back on, so the panel is always dismissable. ## Before https://github.com/user-attachments/assets/61891d25-26b8-4ba4-8b05-73fd44f92d89 ## After https://github.com/user-attachments/assets/41342722-bcaf-420b-83bb-3cafaec49516 --- .../side-panel/components/SidePanelTopBar.tsx | 4 +- .../__stories__/SidePanelTopBar.stories.tsx | 91 +++++++++++++++++++ .../__tests__/SidePanelTopBar.test.tsx | 60 +++--------- 3 files changed, 107 insertions(+), 48 deletions(-) create mode 100644 packages/twenty-front/src/modules/side-panel/components/__stories__/SidePanelTopBar.stories.tsx diff --git a/packages/twenty-front/src/modules/side-panel/components/SidePanelTopBar.tsx b/packages/twenty-front/src/modules/side-panel/components/SidePanelTopBar.tsx index 8a6aa5e61c..10c90e2c82 100644 --- a/packages/twenty-front/src/modules/side-panel/components/SidePanelTopBar.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/SidePanelTopBar.tsx @@ -142,7 +142,7 @@ export const SidePanelTopBar = () => { const shouldShowBackButton = canGoBack; - const shouldShowCloseButton = !isMobile; + const shouldHideCloseButton = isMobile && shouldShowBackButton; const lastChip = contextChips.at(-1); @@ -182,7 +182,7 @@ export const SidePanelTopBar = () => { - {shouldShowCloseButton && ( + {!shouldHideCloseButton && ( { + return (Story) => { + const currentPage = navigationStack[navigationStack.length - 1]; + + jotaiStore.set(isSidePanelOpenedState.atom, true); + jotaiStore.set(sidePanelPageState.atom, currentPage.page); + jotaiStore.set(sidePanelNavigationStackState.atom, navigationStack); + + return ; + }; +}; + +const meta: Meta = { + title: 'Modules/SidePanel/SidePanelTopBar', + component: SidePanelTopBar, + decorators: [ + ObjectMetadataItemsDecorator, + SnackBarDecorator, + ComponentWithRouterDecorator, + ], +}; + +export default meta; +type Story = StoryObj; + +export const RootCommandMenu: Story = { + decorators: [createSidePanelDecorator([ROOT_PAGE])], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect( + await canvas.findByRole('button', { name: 'Close side panel' }), + ).toBeVisible(); + expect( + canvas.queryByRole('button', { name: 'Back' }), + ).not.toBeInTheDocument(); + }, +}; + +export const Subpage: Story = { + decorators: [createSidePanelDecorator([ROOT_PAGE, SUBPAGE])], + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + expect(await canvas.findByRole('button', { name: 'Back' })).toBeVisible(); + expect( + await canvas.findByRole('button', { name: 'Close side panel' }), + ).toBeVisible(); + }, +}; diff --git a/packages/twenty-front/src/modules/side-panel/components/__tests__/SidePanelTopBar.test.tsx b/packages/twenty-front/src/modules/side-panel/components/__tests__/SidePanelTopBar.test.tsx index b79da47d61..d04bfddeac 100644 --- a/packages/twenty-front/src/modules/side-panel/components/__tests__/SidePanelTopBar.test.tsx +++ b/packages/twenty-front/src/modules/side-panel/components/__tests__/SidePanelTopBar.test.tsx @@ -150,27 +150,6 @@ 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(); @@ -187,7 +166,19 @@ describe('SidePanelTopBar', () => { ).toBe(true); }); - it('shows both back and close buttons for command menu subpages', () => { + it('shows the close button on mobile when there is no back button to dismiss the panel', () => { + mockIsMobile = true; + + renderSidePanelCommandMenu(); + + expect( + screen.getByRole('button', { name: 'Close side panel' }), + ).toBeInTheDocument(); + }); + + it('hides the close button on mobile when a back button is available', () => { + mockIsMobile = true; + renderSidePanelCommandMenu( createSidePanelTopBarStore({ sidePanelPage: SidePanelPages.SearchRecords, @@ -210,30 +201,7 @@ describe('SidePanelTopBar', () => { 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' }), + screen.queryByRole('button', { name: 'Close side panel' }), ).not.toBeInTheDocument(); }); });