diff --git a/code/core/src/manager-api/modules/shortcuts.ts b/code/core/src/manager-api/modules/shortcuts.ts index 75e8d6c6f8d5..c44c098c9041 100644 --- a/code/core/src/manager-api/modules/shortcuts.ts +++ b/code/core/src/manager-api/modules/shortcuts.ts @@ -9,6 +9,7 @@ import { global } from '@storybook/global'; import copy from 'copy-to-clipboard'; +import { isReviewManagerRoute } from '../../shared/review/routes.ts'; import type { KeyboardEventLike } from '../lib/shortcut.ts'; import { eventToShortcut, shortcutMatchesShortcut } from '../lib/shortcut.ts'; import type { ModuleFn } from '../lib/types.tsx'; @@ -257,16 +258,23 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => { // warning: event might not have a full prototype chain because it may originate from the channel handleShortcutFeature(feature, event) { + const state = store.getState(); const { ui: { enableShortcuts }, storyId, refId, viewMode, - } = store.getState(); + } = state; if (!enableShortcuts) { return; } + const isSidebarShortcutBlocked = isReviewManagerRoute(state.path, state.customQueryParams); + const isSidebarShortcutFeature = ['focusNav', 'search', 'toggleNav'].includes(feature); + if (isSidebarShortcutBlocked && isSidebarShortcutFeature) { + return; + } + // Event.prototype.preventDefault is missing when received from the MessageChannel. if (event?.preventDefault) { event.preventDefault(); diff --git a/code/core/src/manager/components/layout/Layout.stories.tsx b/code/core/src/manager/components/layout/Layout.stories.tsx index cd4326dfb68e..a20a9a61738e 100644 --- a/code/core/src/manager/components/layout/Layout.stories.tsx +++ b/code/core/src/manager/components/layout/Layout.stories.tsx @@ -514,8 +514,29 @@ export const MobileReview: Story = { args: { managerLayoutState: { ...defaultState, viewMode: 'review' }, }, + decorators: [ + (Story) => ( + true), + }, + }} + > + + + ), + ], play: async ({ canvas }) => { - expect(canvas.getByLabelText('Open navigation menu')).toBeInTheDocument(); + expect(canvas.queryByLabelText('Open navigation menu')).not.toBeInTheDocument(); expect(canvas.getByTestId('preview')).toBeInTheDocument(); }, }; diff --git a/code/core/src/manager/components/layout/Layout.tsx b/code/core/src/manager/components/layout/Layout.tsx index be4daf4eb0dc..25f2db1fb6bd 100644 --- a/code/core/src/manager/components/layout/Layout.tsx +++ b/code/core/src/manager/components/layout/Layout.tsx @@ -3,9 +3,11 @@ import React, { useEffect, useLayoutEffect, useState } from 'react'; import type { API_Layout, API_ViewMode } from 'storybook/internal/types'; -import { useStorybookApi, type API } from 'storybook/manager-api'; +import { useStorybookApi, useStorybookState, type API } from 'storybook/manager-api'; import { styled } from 'storybook/theming'; +import { isReviewManagerRoute } from '../../../shared/review/routes.ts'; + import { MEDIA_DESKTOP_BREAKPOINT, MINIMUM_CONTENT_WIDTH_PX } from '../../constants.ts'; import { Notifications } from '../../container/Notifications.tsx'; import { MobileNavigation } from '../mobile/navigation/MobileNavigation.tsx'; @@ -149,6 +151,9 @@ const OrderedMobileNavigation = styled(MobileNavigation)({ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...slots }: Props) => { const { isDesktop, isMobile } = useLayout(); const api = useStorybookApi(); + const { path, customQueryParams } = useStorybookState(); + const showSidebar = + (api.getIsNavShown?.() ?? true) && !isReviewManagerRoute(path, customQueryParams); const { navSize, @@ -170,6 +175,7 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s <> - {isDesktop && ( + {isDesktop && showSidebar && ( )} @@ -222,7 +229,8 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s const LayoutContainer = styled.div<{ panelPosition: LayoutState['panelPosition']; showPanel: boolean; -}>(({ panelPosition, showPanel }) => ({ + showSidebar: boolean; +}>(({ panelPosition, showPanel, showSidebar }) => ({ width: '100%', height: ['100vh', '100dvh'], overflow: 'hidden', @@ -233,21 +241,33 @@ const LayoutContainer = styled.div<{ [MEDIA_DESKTOP_BREAKPOINT]: { display: 'grid', gap: 0, - // This uses CSS variables to prevent Emotion from generating a new CSS className for every possible value - gridTemplateColumns: `minmax(0, var(--nav-width)) minmax(${MINIMUM_CONTENT_WIDTH_PX}px, 1fr) minmax(0, var(--right-panel-width))`, + gridTemplateColumns: showSidebar + ? `minmax(0, var(--nav-width)) minmax(${MINIMUM_CONTENT_WIDTH_PX}px, 1fr) minmax(0, var(--right-panel-width))` + : `minmax(${MINIMUM_CONTENT_WIDTH_PX}px, 1fr) minmax(0, var(--right-panel-width))`, gridTemplateRows: `1fr minmax(0, var(--bottom-panel-height))`, gridTemplateAreas: (() => { + if (!showSidebar && !showPanel) { + return `"content content" + "content content"`; + } + if (!showSidebar && showPanel) { + if (panelPosition === 'right') { + return `"content panel" + "content panel"`; + } + return `"content content" + "panel panel"`; + } if (!showPanel) { - // showPanel is false by default when viewMode is not 'story', but can be overridden by the user return `"sidebar content content" - "sidebar content content"`; + "sidebar content content"`; } if (panelPosition === 'right') { return `"sidebar content panel" - "sidebar content panel"`; + "sidebar content panel"`; } return `"sidebar content content" - "sidebar panel panel"`; + "sidebar panel panel"`; })(), }, })); diff --git a/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx b/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx index 05c442b090a4..4bf2faf65eb0 100644 --- a/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx +++ b/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx @@ -1,5 +1,5 @@ -import React, { useRef } from 'react'; import type { ComponentProps, FC } from 'react'; +import React, { useLayoutEffect, useRef } from 'react'; import { Button } from 'storybook/internal/components'; import type { API_IndexHash, API_Refs } from 'storybook/internal/types'; @@ -18,6 +18,7 @@ import { MobileMenuDrawer } from './MobileMenuDrawer.tsx'; interface MobileNavigationProps { menu?: React.ReactNode; panel?: React.ReactNode; + showMenu?: boolean; showPanel: boolean; } @@ -75,6 +76,7 @@ interface MobileBottomBarContentProps { setMobileMenuOpen: (isOpen: boolean) => void; isMobilePanelOpen: boolean; setMobilePanelOpen: (isOpen: boolean) => void; + showMenu: boolean; showPanel: boolean; } @@ -91,6 +93,7 @@ const MobileBottomBarContent: FC = ({ setMobileMenuOpen, isMobilePanelOpen, setMobilePanelOpen, + showMenu, showPanel, }) => { const headingId = useId(); @@ -105,17 +108,19 @@ const MobileBottomBarContent: FC = ({

Navigation controls

- setMobileMenuOpen(!isMobileMenuOpen)} - ariaLabel="Open navigation menu" - aria-expanded={isMobileMenuOpen} - aria-controls="storybook-mobile-menu" - > - - {fullStoryName} - + {showMenu && ( + setMobileMenuOpen(!isMobileMenuOpen)} + ariaLabel="Open navigation menu" + aria-expanded={isMobileMenuOpen} + aria-controls="storybook-mobile-menu" + > + + {fullStoryName} + + )} {fullStoryName} @@ -138,6 +143,7 @@ const MobileBottomBarContent: FC = ({ export const MobileNavigation: FC> = ({ menu, panel, + showMenu = true, showPanel, ...props }) => { @@ -145,15 +151,23 @@ export const MobileNavigation: FC { + if (!showMenu) { + setMobileMenuOpen(false); + } + }, [showMenu, setMobileMenuOpen]); + return ( - - {menu} - + {showMenu && ( + + {menu} + + )} - {!isMobilePanelOpen && ( + {!isMobilePanelOpen && (showMenu || showPanel) && ( )} diff --git a/code/core/src/manager/components/preview/Preview.tsx b/code/core/src/manager/components/preview/Preview.tsx index b030bca9eaee..6f88b53e34d9 100644 --- a/code/core/src/manager/components/preview/Preview.tsx +++ b/code/core/src/manager/components/preview/Preview.tsx @@ -13,6 +13,7 @@ import { Helmet } from 'react-helmet-async'; import { Consumer, addons, merge, types, type Combo } from 'storybook/manager-api'; import { useLandmark } from '../../hooks/useLandmark.ts'; +import { REVIEW_COLLECTION_QUERY_PARAM } from '../../../shared/review/routes.ts'; import { ReviewToolbarHeader } from '../review/components/ReviewToolbarHeader.tsx'; import { FramesRenderer } from './FramesRenderer.tsx'; import { ToolbarComp } from './Toolbar.tsx'; @@ -59,6 +60,7 @@ const Preview = React.memo(function Preview(props) { tabs, wrappers, tabId, + queryParams, } = props; // SB11: remove code @@ -90,6 +92,8 @@ const Preview = React.memo(function Preview(props) { const shouldScale = viewMode === 'story'; const { showToolbar } = options; const customisedShowToolbar = api.getShowToolbarWithCustomisations(showToolbar); + const isReviewCollectionStory = + viewMode === 'story' && queryParams?.[REVIEW_COLLECTION_QUERY_PARAM] !== undefined; const previousStoryId = useRef(storyId); @@ -128,7 +132,7 @@ const Preview = React.memo(function Preview(props) { )} - {customisedShowToolbar && viewMode === 'story' ? : null} + {customisedShowToolbar && isReviewCollectionStory ? : null} { shortcut: api.getShortcutKeys().fullScreen, hasPanel: Object.keys(api.getElements(Addon_TypesEnum.PANEL)).length > 0, singleStory: state.singleStory, + isReviewRoute: isReviewManagerRoute(state.path, state.customQueryParams), }; }; @@ -50,7 +52,8 @@ export const fullScreenTool: Addon_BaseType = { return ( - {({ toggle, isFullscreen, shortcut, hasPanel, singleStory }) => + {({ toggle, isFullscreen, shortcut, hasPanel, singleStory, isReviewRoute }) => + !isReviewRoute && (!singleStory || (singleStory && hasPanel)) && (