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
This commit is contained in:
Weiko
2026-06-25 14:55:04 +02:00
committed by GitHub
parent d8cb4aa15b
commit 864ea452b4
3 changed files with 107 additions and 48 deletions
@@ -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 = () => {
</StyledContentContainer>
<StyledRightControlsContainer>
<SidePanelTopBarRightCornerIcon />
{shouldShowCloseButton && (
{!shouldHideCloseButton && (
<IconButton
Icon={IconX}
size="small"
@@ -0,0 +1,91 @@
import {
type Decorator,
type Meta,
type StoryObj,
} from '@storybook/react-vite';
import { expect, within } from 'storybook/test';
import { SidePanelTopBar } from '@/side-panel/components/SidePanelTopBar';
import { isSidePanelOpenedState } from '@/side-panel/states/isSidePanelOpenedState';
import { sidePanelNavigationStackState } from '@/side-panel/states/sidePanelNavigationStackState';
import { sidePanelPageState } from '@/side-panel/states/sidePanelPageState';
import { jotaiStore } from '@/ui/utilities/state/jotai/jotaiStore';
import { SidePanelPages } from 'twenty-shared/types';
import { IconDotsVertical } from 'twenty-ui/icon';
import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator';
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
type SidePanelNavigationStackItem = {
page: SidePanelPages;
pageTitle: string;
pageIcon: typeof IconDotsVertical;
pageId: string;
};
const ROOT_PAGE: SidePanelNavigationStackItem = {
page: SidePanelPages.CommandMenuDisplay,
pageTitle: 'Command Menu',
pageIcon: IconDotsVertical,
pageId: 'command-menu',
};
const SUBPAGE: SidePanelNavigationStackItem = {
page: SidePanelPages.CommandMenuEdit,
pageTitle: 'Edit',
pageIcon: IconDotsVertical,
pageId: 'command-menu-edit',
};
const createSidePanelDecorator = (
navigationStack: SidePanelNavigationStackItem[],
): Decorator => {
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 <Story />;
};
};
const meta: Meta<typeof SidePanelTopBar> = {
title: 'Modules/SidePanel/SidePanelTopBar',
component: SidePanelTopBar,
decorators: [
ObjectMetadataItemsDecorator,
SnackBarDecorator,
ComponentWithRouterDecorator,
],
};
export default meta;
type Story = StoryObj<typeof SidePanelTopBar>;
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();
},
};
@@ -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();
});
});