From a4364625e24898301f7ab8a9f9fea80dc2468571 Mon Sep 17 00:00:00 2001 From: Asish Kumar Date: Wed, 11 Nov 2026 15:24:22 +0530 Subject: [PATCH] feat(ui): close the mobile story list when a story is selected On phones the story list lives in a bottom sheet drawer, and picking a story left the drawer open on top of it, so every selection needed a second gesture to dismiss the sheet before the story could be seen. Storybook for the web closes its mobile navigation once a story is chosen; do the same on device. Layout's setSelection, which both the tree rows and the search results call, now asks the mobile drawer to close after emitting SET_CURRENT_STORY. Expanding or collapsing a group or component doesn't go through setSelection, so the list stays open while browsing. On desktop the drawer isn't rendered and the ref is empty, so nothing changes there. The change is applied to both @storybook/react-native-ui and @storybook/react-native-ui-lite. Add a Layout test for @storybook/react-native-ui that toggles a group (drawer stays open) and then selects a story (drawer is closed once). Layout imports runtime values from storybook/internal, which ships ESM, so let babel-jest transform the storybook package as well. Closes #924 --- .changeset/close-menu-on-story-select.md | 6 ++ packages/react-native-ui-lite/src/Layout.tsx | 4 ++ packages/react-native-ui/jest.config.js | 2 +- packages/react-native-ui/src/Layout.test.tsx | 69 ++++++++++++++++++++ packages/react-native-ui/src/Layout.tsx | 4 ++ 5 files changed, 84 insertions(+), 1 deletion(-) create mode 100644 .changeset/close-menu-on-story-select.md create mode 100644 packages/react-native-ui/src/Layout.test.tsx diff --git a/.changeset/close-menu-on-story-select.md b/.changeset/close-menu-on-story-select.md new file mode 100644 index 0000000000..9dcb4ac371 --- /dev/null +++ b/.changeset/close-menu-on-story-select.md @@ -0,0 +1,6 @@ +--- +'@storybook/react-native-ui': patch +'@storybook/react-native-ui-lite': patch +--- + +The mobile story list now closes once you pick a story from it, as it does in Storybook for the web, so the selected story is visible straight away. Expanding or collapsing a group still leaves the list open. diff --git a/packages/react-native-ui-lite/src/Layout.tsx b/packages/react-native-ui-lite/src/Layout.tsx index af6128aea5..209eaa713d 100644 --- a/packages/react-native-ui-lite/src/Layout.tsx +++ b/packages/react-native-ui-lite/src/Layout.tsx @@ -260,6 +260,10 @@ export const Layout = ({ const channel = addons.getChannel(); channel.emit(SET_CURRENT_STORY, { storyId: newStoryId }); + + // As on Storybook for the web, the mobile story list closes once a story is chosen from it. + // The drawer isn't rendered on desktop, where the ref stays empty. + mobileMenuDrawerRef.current?.setMobileMenuOpen(false); }, []); return ( diff --git a/packages/react-native-ui/jest.config.js b/packages/react-native-ui/jest.config.js index c226d1506b..c9e77be656 100644 --- a/packages/react-native-ui/jest.config.js +++ b/packages/react-native-ui/jest.config.js @@ -7,7 +7,7 @@ const config = { resolver: 'react-native-reanimated/jest/resolver', ...(process.env.JEST_CACHE_DIRECTORY && { cacheDirectory: process.env.JEST_CACHE_DIRECTORY }), transformIgnorePatterns: [ - 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@gorhom|@storybook))', + 'node_modules/(?!((jest-)?react-native|@react-native(-community)?|@gorhom|@storybook|storybook))', ], }; module.exports = config; diff --git a/packages/react-native-ui/src/Layout.test.tsx b/packages/react-native-ui/src/Layout.test.tsx new file mode 100644 index 0000000000..0b5876aff6 --- /dev/null +++ b/packages/react-native-ui/src/Layout.test.tsx @@ -0,0 +1,69 @@ +import { theme, ThemeProvider } from '@storybook/react-native-theming'; +import { LayoutProvider, StorageProvider } from '@storybook/react-native-ui-common'; +import { fireEvent, render, screen } from '@testing-library/react-native'; +import { Text } from 'react-native'; +import { SET_CURRENT_STORY } from 'storybook/internal/core-events'; +import type { API_IndexHash } from 'storybook/internal/types'; +import { Layout } from './Layout'; +import { mockDataset } from './mockdata'; + +const mockEmit = jest.fn(); + +jest.mock('storybook/manager-api', () => ({ + addons: { + getChannel: () => ({ emit: mockEmit }), + getElements: () => ({}), + }, +})); + +const mockSetMobileMenuOpen = jest.fn(); + +// Record what the layout asks of the drawer. Like the real one, which keeps its content mounted +// while closed, this renders the story list straight away. +jest.mock('./MobileMenuDrawer', () => { + const React = require('react'); + + return { + MobileMenuDrawer: React.forwardRef(function MobileMenuDrawer({ children }, ref) { + React.useImperativeHandle(ref, () => ({ setMobileMenuOpen: mockSetMobileMenuOpen })); + + return children; + }), + }; +}); + +const storage = { + getItem: async () => null, + setItem: async () => {}, +}; + +const renderUI = async () => { + await render( + + + + + Story content + + + + + ); +}; + +describe('Layout', () => { + it('closes the mobile story list once a story is selected, but not when a group is toggled', async () => { + await renderUI(); + + await fireEvent.press(screen.getByText('Group 1'), { preventDefault: jest.fn() }); + + expect(mockEmit).not.toHaveBeenCalled(); + expect(mockSetMobileMenuOpen).not.toHaveBeenCalled(); + + await fireEvent.press(screen.getByText('Child B1')); + + expect(mockEmit).toHaveBeenCalledWith(SET_CURRENT_STORY, { storyId: 'group-1--child-b1' }); + expect(mockSetMobileMenuOpen).toHaveBeenCalledTimes(1); + expect(mockSetMobileMenuOpen).toHaveBeenCalledWith(false); + }); +}); diff --git a/packages/react-native-ui/src/Layout.tsx b/packages/react-native-ui/src/Layout.tsx index 56fbc7ac70..affe3ee98c 100644 --- a/packages/react-native-ui/src/Layout.tsx +++ b/packages/react-native-ui/src/Layout.tsx @@ -234,6 +234,10 @@ export const Layout = ({ const channel = addons.getChannel(); channel.emit(SET_CURRENT_STORY, { storyId: newStoryId }); + + // As on Storybook for the web, the mobile story list closes once a story is chosen from it. + // The drawer isn't rendered on desktop, where the ref stays empty. + mobileMenuDrawerRef.current?.setMobileMenuOpen(false); }, []); return (