Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Manager: Add options to hide sidebar and toolbar per story #29516

Open
wants to merge 6 commits into
base: next
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 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
18 changes: 17 additions & 1 deletion code/.storybook/manager.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,25 @@
import { addons } from 'storybook/internal/manager-api';
import { type State, addons } from 'storybook/internal/manager-api';

Sidnioulz marked this conversation as resolved.
Show resolved Hide resolved
import { startCase } from 'es-toolkit/compat';

addons.setConfig({
sidebar: {
renderLabel: ({ name, type }) => (type === 'story' ? name : startCase(name)),
},
layoutCustomisations: {
showSidebar(state: State, defaultValue: boolean) {
if (state.viewMode === 'story' && state.storyId.startsWith('😀')) {
return false;
Sidnioulz marked this conversation as resolved.
Show resolved Hide resolved
}

return defaultValue;
},
showToolbar(state: State, defaultValue: boolean) {
Sidnioulz marked this conversation as resolved.
Show resolved Hide resolved
if (state.viewMode === 'docs') {
return false;
}

return defaultValue;
},
},
});
11 changes: 4 additions & 7 deletions code/core/src/common/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { interopRequireDefault } from './utils/interpret-require';
import { loadCustomPresets } from './utils/load-custom-presets';
import { safeResolve, safeResolveFrom } from './utils/safeResolve';
import { stripAbsNodeModulesPath } from './utils/strip-abs-node-modules-path';
import { isFunction, isObject } from './utils/type-guards';

type InterPresetOptions = Omit<
CLIOptions &
Expand All @@ -29,10 +30,6 @@ type InterPresetOptions = Omit<
'frameworkPresets'
>;

const isObject = (val: unknown): val is Record<string, any> =>
val != null && typeof val === 'object' && Array.isArray(val) === false;
const isFunction = (val: unknown): val is Function => typeof val === 'function';

export function filterPresetsConfig(presetsConfig: PresetConfig[]): PresetConfig[] {
return presetsConfig.filter((preset) => {
const presetName = typeof preset === 'string' ? preset : preset.name;
Expand All @@ -50,7 +47,7 @@ function resolvePathToMjs(filePath: string): string {
}

function resolvePresetFunction<T = any>(
input: T[] | Function,
input: T[] | CallableFunction,
presetOptions: any,
storybookOptions: InterPresetOptions
): T[] {
Expand Down Expand Up @@ -189,8 +186,8 @@ export const resolveAddonName = (
const map =
({ configDir }: InterPresetOptions) =>
(item: any) => {
const options = isObject(item) ? item['options'] || undefined : undefined;
const name = isObject(item) ? item['name'] : item;
const options = isObject(item) ? item.options || undefined : undefined;
const name = isObject(item) ? item.name : item;

let resolved;

Expand Down
78 changes: 78 additions & 0 deletions code/core/src/common/utils/__tests__/type-guards.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { describe, expect, it, vi } from 'vitest';

import { isFunction, isObject } from '../type-guards';

describe('type-guards - isFunction', () => {
it('should return true for regular functions', () => {
function testFn() {}
expect(isFunction(testFn)).toBe(true);
});

it('should return true for arrow functions', () => {
const arrowFn = () => {};
expect(isFunction(arrowFn)).toBe(true);
});

it('should return true for class methods', () => {
class TestClass {
method() {}
}
const instance = new TestClass();
expect(isFunction(instance.method)).toBe(true);
});

it('should return true for built-in functions', () => {
expect(isFunction(setTimeout)).toBe(true);
expect(isFunction(console.log)).toBe(true);
});

it('should return false for non-function values', () => {
expect(isFunction(null)).toBe(false);
expect(isFunction(undefined)).toBe(false);
expect(isFunction(42)).toBe(false);
expect(isFunction('string')).toBe(false);
expect(isFunction({})).toBe(false);
expect(isFunction([])).toBe(false);
expect(isFunction(true)).toBe(false);
});
});

describe('type-guards - isObject()', () => {
it('should return true for plain objects', () => {
expect(isObject({})).toBe(true);
expect(isObject({ a: 1 })).toBe(true);
});

it('should return true for class instances', () => {
class TestClass {}
const instance = new TestClass();
expect(isObject(instance)).toBe(true);
});

it('should return false for null and undefined', () => {
expect(isObject(null)).toBe(false);
expect(isObject(undefined)).toBe(false);
});

it('should return false for arrays', () => {
expect(isObject([])).toBe(false);
expect(isObject([1, 2, 3])).toBe(false);
});

it('should return false for primitive values', () => {
expect(isObject(42)).toBe(false);
expect(isObject('string')).toBe(false);
expect(isObject(true)).toBe(false);
expect(isObject(Symbol('test'))).toBe(false);
});

it('should return true for complex objects', () => {
const date = new Date();
const map = new Map();
const set = new Set();

expect(isObject(date)).toBe(true);
expect(isObject(map)).toBe(true);
expect(isObject(set)).toBe(true);
});
});
3 changes: 3 additions & 0 deletions code/core/src/common/utils/type-guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const isObject = (val: unknown): val is Record<string, any> =>
val != null && typeof val === 'object' && Array.isArray(val) === false;
export const isFunction = (val: unknown): val is CallableFunction => typeof val === 'function';
54 changes: 52 additions & 2 deletions code/core/src/manager-api/modules/layout.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
import type { ThemeVars } from '@storybook/core/theming';
import { create } from '@storybook/core/theming/create';
import type { API_Layout, API_PanelPositions, API_UI } from '@storybook/core/types';
import type {
API_Layout,
API_LayoutCustomisations,
API_PanelPositions,
API_UI,
} from '@storybook/core/types';
import { global } from '@storybook/global';

import { SET_CONFIG } from '@storybook/core/core-events';

import { isEqual as deepEqual, pick, toMerged } from 'es-toolkit';

import { isFunction } from '../../common/utils/type-guards';
import merge from '../lib/merge';
import type { ModuleFn } from '../lib/types';
import type { State } from '../root';
Expand All @@ -21,6 +27,7 @@ export const ActiveTabs = {

export interface SubState {
layout: API_Layout;
layoutCustomisations: API_LayoutCustomisations;
ui: API_UI;
selectedPanel: string | undefined;
theme: ThemeVars;
Expand Down Expand Up @@ -78,6 +85,16 @@ export interface SubAPI {
getIsPanelShown: () => boolean;
/** GetIsNavShown - Returns the current visibility of the navigation bar in the Storybook UI. */
getIsNavShown: () => boolean;
/**
* GetShowToolbarWithCustomisations - Returns the current visibility of the toolbar, taking into
* account customisations requested by the end user via a layoutCustomisations function.
*/
getShowToolbarWithCustomisations: (showToolbar: boolean) => boolean;
/**
* GetNavSizeWithCustomisations - Returns the size to apply to the sidebar/nav, taking into
* account customisations requested by the end user via a layoutCustomisations function.
*/
getNavSizeWithCustomisations: (navSize: number) => number;
}

type PartialSubState = Partial<SubState>;
Expand All @@ -100,6 +117,10 @@ export const defaultLayoutState: SubState = {
panelPosition: 'bottom',
showTabs: true,
},
layoutCustomisations: {
showSidebar: undefined,
showToolbar: undefined,
},
selectedPanel: undefined,
theme: create(),
};
Expand Down Expand Up @@ -313,7 +334,7 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, provider, singleStory
},

getInitialOptions() {
const { theme, selectedPanel, ...options } = provider.getConfig();
const { theme, selectedPanel, layoutCustomisations, ...options } = provider.getConfig();

return {
...defaultLayoutState,
Expand All @@ -324,6 +345,10 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, provider, singleStory
),
...(singleStory && { navSize: 0 }),
},
layoutCustomisations: {
...defaultLayoutState.layoutCustomisations,
...(layoutCustomisations ?? {}),
},
ui: toMerged(defaultLayoutState.ui, pick(options, Object.keys(defaultLayoutState.ui))),
selectedPanel: selectedPanel || defaultLayoutState.selectedPanel,
theme: theme || defaultLayoutState.theme,
Expand All @@ -340,6 +365,31 @@ export const init: ModuleFn<SubAPI, SubState> = ({ store, provider, singleStory
return getIsNavShown(store.getState());
},

getShowToolbarWithCustomisations(showToolbar: boolean) {
const state = store.getState();

if (isFunction(state.layoutCustomisations.showToolbar)) {
return state.layoutCustomisations.showToolbar(state, showToolbar);
}

return showToolbar;
},

getNavSizeWithCustomisations(navSize: number) {
const state = store.getState();

if (isFunction(state.layoutCustomisations.showSidebar)) {
const shouldShowNav = state.layoutCustomisations.showSidebar(state, navSize !== 0);
if (navSize === 0 && shouldShowNav === true) {
return state.layout.recentVisibleSizes.navSize;
} else if (navSize !== 0 && shouldShowNav === false) {
return 0;
}
}
Sidnioulz marked this conversation as resolved.
Show resolved Hide resolved

return navSize;
},

setOptions: (options: any) => {
Sidnioulz marked this conversation as resolved.
Show resolved Hide resolved
const { layout, ui, selectedPanel, theme } = store.getState();

Expand Down
6 changes: 5 additions & 1 deletion code/core/src/manager/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,30 @@ import React from 'react';
import { Global, createGlobal } from '@storybook/core/theming';
import type { Addon_PageType } from '@storybook/core/types';

import type { API } from '@storybook/core/manager-api';

import { Layout } from './components/layout/Layout';
import { useLayout } from './components/layout/LayoutProvider';
import Panel from './container/Panel';
import Preview from './container/Preview';
import Sidebar from './container/Sidebar';

type Props = {
api: API;
managerLayoutState: ComponentProps<typeof Layout>['managerLayoutState'];
setManagerLayoutState: ComponentProps<typeof Layout>['setManagerLayoutState'];
pages: Addon_PageType[];
hasTab: boolean;
};

export const App = ({ managerLayoutState, setManagerLayoutState, pages, hasTab }: Props) => {
export const App = ({ api, managerLayoutState, setManagerLayoutState, pages, hasTab }: Props) => {
const { setMobileAboutOpen } = useLayout();

return (
<>
<Global styles={createGlobal} />
<Layout
api={api}
hasTab={hasTab}
managerLayoutState={managerLayoutState}
setManagerLayoutState={setManagerLayoutState}
Expand Down
19 changes: 16 additions & 3 deletions code/core/src/manager/components/layout/Layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import { styled } from '@storybook/core/theming';
import type { API_Layout, API_ViewMode } from '@storybook/core/types';

import type { API } from '@storybook/core/manager-api';

import { MEDIA_DESKTOP_BREAKPOINT } from '../../constants';
import { Notifications } from '../../container/Notifications';
import { MobileNavigation } from '../mobile/navigation/MobileNavigation';
Expand All @@ -22,6 +24,7 @@
export type LayoutState = InternalLayoutState & ManagerLayoutState;

interface Props {
api: API;
managerLayoutState: ManagerLayoutState;
setManagerLayoutState: (state: Partial<Omit<ManagerLayoutState, 'viewMode'>>) => void;
slotMain?: React.ReactNode;
Expand All @@ -44,11 +47,13 @@
* manager store to the internal state here when necessary
*/
const useLayoutSyncingState = ({
api,
managerLayoutState,
setManagerLayoutState,
isDesktop,
hasTab,
}: {
api: API;
managerLayoutState: Props['managerLayoutState'];
setManagerLayoutState: Props['setManagerLayoutState'];
isDesktop: boolean;
Expand Down Expand Up @@ -108,8 +113,10 @@
? internalDraggingSizeState
: managerLayoutState;

const customisedNavSize = api.getNavSizeWithCustomisations(navSize);

Check failure on line 116 in code/core/src/manager/components/layout/Layout.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

../core/src/manager/components/layout/Layout.stories.tsx > Desktop

TypeError: Click to debug the error directly in Storybook: http://localhost:6006/?path=/story/manager-layout--desktop&addonPanel=storybook/test/panel Cannot read properties of undefined (reading 'getNavSizeWithCustomisations') ❯ useLayoutSyncingState ../core/src/manager/components/layout/Layout.tsx:116:32 ❯ Layout ../core/src/manager/components/layout/Layout.tsx:150:6

Check failure on line 116 in code/core/src/manager/components/layout/Layout.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

../core/src/manager/components/layout/Layout.stories.tsx > Dark

TypeError: Click to debug the error directly in Storybook: http://localhost:6006/?path=/story/manager-layout--dark&addonPanel=storybook/test/panel Cannot read properties of undefined (reading 'getNavSizeWithCustomisations') ❯ useLayoutSyncingState ../core/src/manager/components/layout/Layout.tsx:116:32 ❯ Layout ../core/src/manager/components/layout/Layout.tsx:150:6

Check failure on line 116 in code/core/src/manager/components/layout/Layout.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

../core/src/manager/components/layout/Layout.stories.tsx > Desktop Horizontal

TypeError: Click to debug the error directly in Storybook: http://localhost:6006/?path=/story/manager-layout--desktop-horizontal&addonPanel=storybook/test/panel Cannot read properties of undefined (reading 'getNavSizeWithCustomisations') ❯ useLayoutSyncingState ../core/src/manager/components/layout/Layout.tsx:116:32 ❯ Layout ../core/src/manager/components/layout/Layout.tsx:150:6

Check failure on line 116 in code/core/src/manager/components/layout/Layout.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

../core/src/manager/components/layout/Layout.stories.tsx > Desktop Docs

TypeError: Click to debug the error directly in Storybook: http://localhost:6006/?path=/story/manager-layout--desktop-docs&addonPanel=storybook/test/panel Cannot read properties of undefined (reading 'getNavSizeWithCustomisations') ❯ useLayoutSyncingState ../core/src/manager/components/layout/Layout.tsx:116:32 ❯ Layout ../core/src/manager/components/layout/Layout.tsx:150:6

Check failure on line 116 in code/core/src/manager/components/layout/Layout.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

../core/src/manager/components/layout/Layout.stories.tsx > Desktop Pages

TypeError: Click to debug the error directly in Storybook: http://localhost:6006/?path=/story/manager-layout--desktop-pages&addonPanel=storybook/test/panel Cannot read properties of undefined (reading 'getNavSizeWithCustomisations') ❯ useLayoutSyncingState ../core/src/manager/components/layout/Layout.tsx:116:32 ❯ Layout ../core/src/manager/components/layout/Layout.tsx:150:6

Check failure on line 116 in code/core/src/manager/components/layout/Layout.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

../core/src/manager/components/layout/Layout.stories.tsx > Mobile

TypeError: Click to debug the error directly in Storybook: http://localhost:6006/?path=/story/manager-layout--mobile&addonPanel=storybook/test/panel Cannot read properties of undefined (reading 'getNavSizeWithCustomisations') ❯ useLayoutSyncingState ../core/src/manager/components/layout/Layout.tsx:116:32 ❯ Layout ../core/src/manager/components/layout/Layout.tsx:150:6

Check failure on line 116 in code/core/src/manager/components/layout/Layout.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

../core/src/manager/components/layout/Layout.stories.tsx > Mobile Dark

TypeError: Click to debug the error directly in Storybook: http://localhost:6006/?path=/story/manager-layout--mobile-dark&addonPanel=storybook/test/panel Cannot read properties of undefined (reading 'getNavSizeWithCustomisations') ❯ useLayoutSyncingState ../core/src/manager/components/layout/Layout.tsx:116:32 ❯ Layout ../core/src/manager/components/layout/Layout.tsx:150:6

Check failure on line 116 in code/core/src/manager/components/layout/Layout.tsx

View workflow job for this annotation

GitHub Actions / Core Unit Tests, windows-latest

../core/src/manager/components/layout/Layout.stories.tsx > Mobile Docs

TypeError: Click to debug the error directly in Storybook: http://localhost:6006/?path=/story/manager-layout--mobile-docs&addonPanel=storybook/test/panel Cannot read properties of undefined (reading 'getNavSizeWithCustomisations') ❯ useLayoutSyncingState ../core/src/manager/components/layout/Layout.tsx:116:32 ❯ Layout ../core/src/manager/components/layout/Layout.tsx:150:6
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this fails in CI, as I've added the signature to the SubAPI for layout. Any advice on what I did wrong?


return {
navSize,
navSize: customisedNavSize,
rightPanelWidth,
bottomPanelHeight,
panelPosition: managerLayoutState.panelPosition,
Expand All @@ -121,7 +128,13 @@
};
};

export const Layout = ({ managerLayoutState, setManagerLayoutState, hasTab, ...slots }: Props) => {
export const Layout = ({
api,
managerLayoutState,
setManagerLayoutState,
hasTab,
...slots
}: Props) => {
const { isDesktop, isMobile } = useLayout();

const {
Expand All @@ -134,7 +147,7 @@
showPages,
showPanel,
isDragging,
} = useLayoutSyncingState({ managerLayoutState, setManagerLayoutState, isDesktop, hasTab });
} = useLayoutSyncingState({ api, managerLayoutState, setManagerLayoutState, isDesktop, hasTab });

return (
<LayoutContainer
Expand Down
3 changes: 2 additions & 1 deletion code/core/src/manager/components/preview/Preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ const Preview = React.memo<PreviewProps>(function Preview(props) {

const shouldScale = viewMode === 'story';
const { showToolbar } = options;
const customisedShowToolbar = api.getShowToolbarWithCustomisations(showToolbar);

const previousStoryId = useRef(storyId);

Expand Down Expand Up @@ -94,7 +95,7 @@ const Preview = React.memo<PreviewProps>(function Preview(props) {
<S.PreviewContainer>
<ToolbarComp
key="tools"
isShown={showToolbar}
isShown={customisedShowToolbar}
// @ts-expect-error (non strict)
tabId={tabId}
tabs={tabs}
Expand Down
1 change: 1 addition & 0 deletions code/core/src/manager/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const Main: FC<{ provider: Provider }> = ({ provider }) => {
<LayoutProvider>
<App
key="app"
api={api}
pages={pages}
managerLayoutState={{
...state.layout,
Expand Down
Loading
Loading