Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions code/core/src/manager-api/modules/layout.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
API_LayoutCustomisations,
API_PanelPositions,
API_UI,
API_ViewMode,
} from 'storybook/internal/types';

import { global } from '@storybook/global';
Expand All @@ -14,6 +15,7 @@ import type { ThemeVars } from 'storybook/theming';
import { deprecate } from 'storybook/internal/client-logger';
import { create } from 'storybook/theming/create';

import { isReviewManagerRoute } from '../../shared/review/routes.ts';
import merge from '../lib/merge.ts';
import type { ModuleFn } from '../lib/types.tsx';
import type { State } from '../root.tsx';
Expand All @@ -36,6 +38,16 @@ export interface SubState {
theme: ThemeVars;
}

/**
* Availability of the sidebar/nav: 'unavailable' means the current route suppresses the nav
* entirely (review routes), otherwise it is 'shown' or 'hidden' based on the layout state.
*/
export type NavAvailability = 'shown' | 'hidden' | 'unavailable';

/** True when the route renders a full-screen page (e.g. settings) instead of the preview canvas. */
export const isPagesViewMode = (viewMode: API_ViewMode): boolean =>
viewMode !== undefined && viewMode !== 'story' && viewMode !== 'docs' && viewMode !== 'review';

export interface SubAPI {
/**
* Toggles the fullscreen mode of the Storybook UI.
Expand Down Expand Up @@ -88,6 +100,11 @@ export interface SubAPI {
getIsPanelShown: () => boolean;
/** GetIsNavShown - Returns the current visibility of the navigation bar in the Storybook UI. */
getIsNavShown: () => boolean;
/**
* GetNavAvailability - Returns whether the sidebar/nav is shown, hidden (but can be shown by the
* user), or unavailable because the current route suppresses it entirely (review routes).
*/
getNavAvailability: () => NavAvailability;
/**
* GetShowToolbarWithCustomisations - Returns the current visibility of the toolbar, taking into
* account customisations requested by the end user via a layoutCustomisations function.
Expand Down Expand Up @@ -545,6 +562,13 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, provider, singleStory
getIsNavShown() {
return getIsNavShown(store.getState());
},
getNavAvailability(): NavAvailability {
const state = store.getState();
if (isReviewManagerRoute(state.path, state.customQueryParams)) {
return 'unavailable';
}
return getIsNavShown(state) ? 'shown' : 'hidden';
},

getShowToolbarWithCustomisations(showToolbar: boolean) {
const state = store.getState();
Expand Down
3 changes: 1 addition & 2 deletions code/core/src/manager-api/modules/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ 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';
Expand Down Expand Up @@ -269,7 +268,7 @@ export const init: ModuleFn = ({ store, fullAPI, provider }) => {
return;
}

const isSidebarShortcutBlocked = isReviewManagerRoute(state.path, state.customQueryParams);
const isSidebarShortcutBlocked = fullAPI.getNavAvailability() === 'unavailable';
Comment thread
ghengeveld marked this conversation as resolved.
const isSidebarShortcutFeature = ['focusNav', 'search', 'toggleNav'].includes(feature);
if (isSidebarShortcutBlocked && isSidebarShortcutFeature) {
return;
Expand Down
25 changes: 25 additions & 0 deletions code/core/src/manager-api/tests/layout.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -700,6 +700,31 @@ describe('layout API', () => {
expect(layoutApi.getIsNavShown()).toBe(true);
});

it('should get nav availability with getNavAvailability', () => {
expect(layoutApi.getNavAvailability()).toBe('shown');

layoutApi.toggleNav();

expect(layoutApi.getNavAvailability()).toBe('hidden');

layoutApi.toggleNav();

Object.assign(currentState, { path: '/review/', customQueryParams: {} });

expect(layoutApi.getNavAvailability()).toBe('unavailable');

Object.assign(currentState, {
path: '/story/foo--bar',
customQueryParams: { collection: '0' },
});

expect(layoutApi.getNavAvailability()).toBe('unavailable');

Object.assign(currentState, { path: '/story/foo--bar', customQueryParams: {} });

expect(layoutApi.getNavAvailability()).toBe('shown');
});

it('should get panelShwon with getIsPanelShown', () => {
expect(layoutApi.getIsPanelShown()).toBe(true);

Expand Down
15 changes: 13 additions & 2 deletions code/core/src/manager/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,21 @@ import { ManagerErrorBoundary } from './components/error-boundary/ManagerErrorBo
import { Layout } from './components/layout/Layout.tsx';
import { useLayout } from './components/layout/LayoutProvider.tsx';
import { ReviewPersistentLayer } from './components/review/components/ReviewPersistentLayer.tsx';
import { useReview } from './components/review/review-store.ts';
import Panel from './container/Panel.tsx';
import Preview from './container/Preview.tsx';
import Sidebar from './container/Sidebar.tsx';

/**
* The main preview, unmounted while the review summary covers the content cell. Keeping it mounted
* there would leave the previously selected story's iframe alive, so it flashes through for a frame
* when navigating to a curated story. Unmounting makes each such navigation boot a fresh preview.
*/
const MainPreview = () => {
const { isSummaryVisible } = useReview();
return isSummaryVisible ? null : <Preview id="main" withLoader />;
};

Comment thread
ghengeveld marked this conversation as resolved.
type Props = {
managerLayoutState: ComponentProps<typeof Layout>['managerLayoutState'];
setManagerLayoutState: ComponentProps<typeof Layout>['setManagerLayoutState'];
Expand Down Expand Up @@ -65,12 +76,12 @@ export const App = ({ managerLayoutState, setManagerLayoutState, pages, hasTab }
<>
<Global styles={createGlobal} />
<ManagerErrorBoundary>
<ReviewPersistentLayer />
<Layout
hasTab={hasTab}
managerLayoutState={managerLayoutState}
setManagerLayoutState={setManagerLayoutState}
slotMain={<Preview id="main" withLoader />}
slotOverlay={<ReviewPersistentLayer />}
slotMain={<MainPreview />}
slotSidebar={<Sidebar onMenuClick={() => setMobileAboutOpen((state) => !state)} />}
slotPanel={<Panel />}
slotPages={pages.map(({ id, render: Content }) => (
Expand Down
3 changes: 2 additions & 1 deletion code/core/src/manager/components/layout/Layout.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,7 @@ const mockManagerStore: any = {
getCurrentStoryData: fn(() => {
return mockManagerStore.state.index.someStoryId;
}),
getNavAvailability: fn(() => 'shown'),
},
};

Expand Down Expand Up @@ -527,7 +528,7 @@ export const MobileReview: Story = {
},
api: {
...mockManagerStore.api,
getIsNavShown: fn(() => true),
getNavAvailability: fn(() => 'unavailable'),
},
}}
>
Expand Down
40 changes: 31 additions & 9 deletions code/core/src/manager/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { API_Layout, API_ViewMode } from 'storybook/internal/types';
import { useStorybookApi, useStorybookState, type API } from 'storybook/manager-api';
import { styled } from 'storybook/theming';

import { isReviewManagerRoute } from '../../../shared/review/routes.ts';
import { isPagesViewMode } from '../../../manager-api/modules/layout.ts';

import { MEDIA_DESKTOP_BREAKPOINT, MINIMUM_CONTENT_WIDTH_PX } from '../../constants.ts';
import { Notifications } from '../../container/Notifications.tsx';
Expand Down Expand Up @@ -39,6 +39,11 @@ interface Props {
slotSidebar?: React.ReactNode;
slotPanel?: React.ReactNode;
slotPages?: React.ReactNode;
/**
* Persistent overlay rendered on top of the main content cell. Always mounted, so overlays can
* keep state (e.g. loaded iframes) alive across route changes.
*/
slotOverlay?: React.ReactNode;
hasTab: boolean;
}

Expand Down Expand Up @@ -108,11 +113,7 @@ const useLayoutSyncingState = ({
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [internalDraggingSizeState, setManagerLayoutState]);

const isPagesShown =
managerLayoutState.viewMode !== undefined &&
managerLayoutState.viewMode !== 'story' &&
managerLayoutState.viewMode !== 'docs' &&
managerLayoutState.viewMode !== 'review';
const isPagesShown = isPagesViewMode(managerLayoutState.viewMode);
const isPanelShown = managerLayoutState.viewMode === 'story' && !hasTab;

const { navSize, rightPanelWidth, bottomPanelHeight } = internalDraggingSizeState.isDragging
Expand Down Expand Up @@ -154,9 +155,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);
// Subscribe to manager state so nav availability re-evaluates on route and layout changes.
useStorybookState();
const showSidebar = api.getNavAvailability() === 'shown';

const {
navSize,
Expand Down Expand Up @@ -214,6 +215,8 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
slotPages={slots.slotPages}
/>

{slots.slotOverlay && <ContentOverlayCell>{slots.slotOverlay}</ContentOverlayCell>}

{isDesktop && showPanel && (
<PanelContainer
bottomPanelHeight={bottomPanelHeight}
Expand All @@ -231,6 +234,24 @@ export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...s
</LayoutContainer>
);
};
/**
* Occupies the main content cell (the full container on mobile) without intercepting interaction;
* overlay content re-enables pointer events itself when visible. Kept below the toolbar (z-index 4)
* and the mobile navigation (z-index 10).
*/
Comment thread
ghengeveld marked this conversation as resolved.
const ContentOverlayCell = styled.div({
position: 'absolute',
inset: 0,
pointerEvents: 'none',
zIndex: 2,

[MEDIA_DESKTOP_BREAKPOINT]: {
position: 'relative',
inset: 'auto',
gridArea: 'content',
},
});

const DragShield = styled.div({
position: 'fixed',
inset: 0,
Expand All @@ -242,6 +263,7 @@ const LayoutContainer = styled.div<{
showPanel: boolean;
showSidebar: boolean;
}>(({ panelPosition, showPanel, showSidebar }) => ({
position: 'relative',
width: '100%',
height: ['100vh', '100dvh'],
overflow: 'hidden',
Expand Down
6 changes: 3 additions & 3 deletions code/core/src/manager/components/preview/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +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 { isReviewCollectionStoryRoute } from '../../../shared/review/routes.ts';
import { ReviewToolbarHeader } from '../review/components/ReviewToolbarHeader.tsx';
import { FramesRenderer } from './FramesRenderer.tsx';
import { ToolbarComp } from './Toolbar.tsx';
Expand Down Expand Up @@ -60,6 +60,7 @@ const Preview = React.memo<PreviewProps>(function Preview(props) {
tabs,
wrappers,
tabId,
path,
queryParams,
} = props;

Expand Down Expand Up @@ -92,8 +93,7 @@ const Preview = React.memo<PreviewProps>(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 isReviewCollectionStory = isReviewCollectionStoryRoute(path, queryParams);

const previousStoryId = useRef(storyId);

Expand Down
7 changes: 3 additions & 4 deletions code/core/src/manager/components/preview/Toolbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ import {
} from 'storybook/manager-api';
import { styled } from 'storybook/theming';

import { isReviewManagerRoute } from '../../../shared/review/routes.ts';
import { useLandmark } from '../../hooks/useLandmark.ts';
import { useLayout } from '../layout/LayoutProvider.tsx';
import type { PreviewProps } from './utils/types.tsx';
Expand All @@ -33,7 +32,7 @@ const fullScreenMapper = ({ api, state }: Combo) => {
shortcut: api.getShortcutKeys().fullScreen,
hasPanel: Object.keys(api.getElements(Addon_TypesEnum.PANEL)).length > 0,
singleStory: state.singleStory,
isReviewRoute: isReviewManagerRoute(state.path, state.customQueryParams),
isNavUnavailable: api.getNavAvailability() === 'unavailable',
};
};

Expand All @@ -52,8 +51,8 @@ export const fullScreenTool: Addon_BaseType = {

return (
<Consumer filter={fullScreenMapper}>
{({ toggle, isFullscreen, shortcut, hasPanel, singleStory, isReviewRoute }) =>
!isReviewRoute &&
{({ toggle, isFullscreen, shortcut, hasPanel, singleStory, isNavUnavailable }) =>
!isNavUnavailable &&
(!singleStory || (singleStory && hasPanel)) && (
<Button
key="full"
Expand Down
9 changes: 3 additions & 6 deletions code/core/src/manager/components/preview/tools/menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,10 @@ import { Consumer, types } from 'storybook/manager-api';
import type { Combo } from 'storybook/manager-api';

import { focusableUIElements } from '../../../../manager-api/modules/layout.ts';
import { isReviewManagerRoute } from '../../../../shared/review/routes.ts';

const menuMapper = ({ api, state }: Combo) => ({
isVisible: api.getIsNavShown(),
navAvailability: api.getNavAvailability(),
singleStory: state.singleStory,
isReviewCollectionStory: isReviewManagerRoute(state.path, state.customQueryParams),
showSidebar: async (forceFocus: boolean) => {
api.toggleNav(true);
api.focusOnUIElement(focusableUIElements.sidebarRegion, {
Expand All @@ -33,10 +31,9 @@ export const menuTool: Addon_BaseType = {
render: () => {
return (
<Consumer filter={menuMapper}>
{({ isVisible, showSidebar, singleStory, isReviewCollectionStory }) =>
{({ navAvailability, showSidebar, singleStory }) =>
!singleStory &&
!isVisible &&
!isReviewCollectionStory && (
navAvailability === 'hidden' && (
Comment thread
ghengeveld marked this conversation as resolved.
<>
<Button
padding="small"
Expand Down
1 change: 1 addition & 0 deletions code/core/src/manager/components/preview/utils/types.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface PreviewProps {
showToolbar: boolean;
};
id?: string;
path: State['path'];
queryParams: State['customQueryParams'];
customCanvas?: CustomCanvasRenderer;
description: string;
Expand Down
Loading