-
- {!isDefault && (
- <>
-
-
-
- >
- )}
+
+
+ {!isDefault && (
+ <>
+
+
+
+ >
+ )}
+
{
const { storyId, refId } = state;
diff --git a/code/core/src/manager/components/preview/tools/zoom.stories.tsx b/code/core/src/manager/components/preview/tools/zoom.stories.tsx
new file mode 100644
index 000000000000..9b4b6e9e320b
--- /dev/null
+++ b/code/core/src/manager/components/preview/tools/zoom.stories.tsx
@@ -0,0 +1,82 @@
+import { useState } from 'react';
+
+import type { StoryContext } from '@storybook/react-vite';
+
+import { fn, screen, within } from 'storybook/test';
+
+import preview from '../../../../../../.storybook/preview';
+import { Zoom } from './zoom';
+
+const openDialog = async (context: StoryContext) => {
+ const zoom = await context.canvas.findByRole('button', { name: 'Change zoom level' });
+ await context.userEvent.click(zoom);
+ return screen.findByRole('dialog');
+};
+
+const meta = preview.meta({
+ component: Zoom,
+ args: {
+ value: 1,
+ zoomIn: fn(),
+ zoomOut: fn(),
+ zoomTo: fn(),
+ },
+ render: (args: Parameters[0]) => {
+ const [value, setValue] = useState(args.value);
+ return (
+ setValue(value + 0.5),
+ zoomOut: () => setValue(value - 0.5),
+ zoomTo: setValue,
+ }}
+ />
+ );
+ },
+ play: async (context) => {
+ await openDialog(context as any);
+ },
+});
+
+export default meta;
+
+export const Default = meta.story({});
+
+export const ZoomIn = meta.story({
+ play: async (context) => {
+ const dialog = await openDialog(context as any);
+ const zoomIn = await within(dialog).findByRole('button', { name: 'Zoom in' });
+ await context.userEvent.click(zoomIn);
+ },
+});
+
+export const ZoomOut = meta.story({
+ play: async (context) => {
+ const dialog = await openDialog(context as any);
+ const zoomOut = await within(dialog).findByRole('button', { name: 'Zoom out' });
+ await context.userEvent.click(zoomOut);
+ },
+});
+
+export const Undo = meta.story({
+ play: async (context) => {
+ const dialog = await openDialog(context as any);
+ const zoomIn = await within(dialog).findByRole('button', { name: 'Zoom in' });
+ await context.userEvent.click(zoomIn);
+ const undo = await within(dialog).findByRole('button', { name: 'Reset zoom' });
+ await context.userEvent.click(undo);
+ },
+});
+
+export const MaxZoom = meta.story({
+ args: {
+ value: 4,
+ },
+});
+
+export const MinZoom = meta.story({
+ args: {
+ value: 0.25,
+ },
+});
diff --git a/code/core/src/manager/components/preview/tools/zoom.tsx b/code/core/src/manager/components/preview/tools/zoom.tsx
index bc11e58a87fc..42b3d9c0eb70 100644
--- a/code/core/src/manager/components/preview/tools/zoom.tsx
+++ b/code/core/src/manager/components/preview/tools/zoom.tsx
@@ -1,23 +1,40 @@
-import type { EventHandler, PropsWithChildren, SyntheticEvent } from 'react';
-import React, { Component, createContext, memo, useCallback } from 'react';
+import type { PropsWithChildren } from 'react';
+import React, { Component, createContext, memo, useCallback, useEffect, useRef } from 'react';
-import { Button, Separator } from 'storybook/internal/components';
+import { ActionList, Button, PopoverProvider } from 'storybook/internal/components';
import type { Addon_BaseType } from 'storybook/internal/types';
-import { ZoomIcon, ZoomOutIcon, ZoomResetIcon } from '@storybook/icons';
+import { UndoIcon, ZoomIcon } from '@storybook/icons';
-import { types } from 'storybook/manager-api';
+import { types, useStorybookApi } from 'storybook/manager-api';
+import { styled } from 'storybook/theming';
-const initialZoom = 1 as const;
+import { Shortcut } from '../../Shortcut';
+import { NumericInput } from '../NumericInput';
-const Context = createContext({ value: initialZoom, set: (v: number) => {} });
+const ZOOM_LEVELS = [0.25, 0.5, 0.75, 0.9, 1, 1.1, 1.25, 1.5, 2, 3, 4] as const;
+const INITIAL_ZOOM_LEVEL = 1;
-class ZoomProvider extends Component<
+const ZoomButton = styled(Button)({
+ minWidth: 48,
+});
+
+const Context = createContext({ value: INITIAL_ZOOM_LEVEL, set: (v: number) => {} });
+
+const ZoomInput = styled(NumericInput)({
+ input: {
+ width: 100,
+ },
+});
+
+export const ZoomConsumer = Context.Consumer;
+
+export class ZoomProvider extends Component<
PropsWithChildren<{ shouldScale: boolean }>,
{ value: number }
> {
state = {
- value: initialZoom,
+ value: INITIAL_ZOOM_LEVEL,
};
set = (value: number) => this.setState({ value });
@@ -27,84 +44,173 @@ class ZoomProvider extends Component<
const { set } = this;
const { value } = this.state;
return (
-
+
{children}
);
}
}
-const { Consumer: ZoomConsumer } = Context;
+export const Zoom = memo<{
+ value: number;
+ zoomIn: () => void;
+ zoomOut: () => void;
+ zoomTo: (value: number) => void;
+}>(function Zoom({ value, zoomIn, zoomOut, zoomTo }) {
+ const inputRef = useRef(null);
-const Zoom = memo<{
- zoomIn: EventHandler;
- zoomOut: EventHandler;
- reset: EventHandler;
-}>(function Zoom({ zoomIn, zoomOut, reset }) {
return (
- <>
-
-
-
- >
+ {Math.round(value * 100)}%
+
+
);
});
-export { Zoom, ZoomConsumer, ZoomProvider };
+const ZoomWrapper = memo<{
+ set: (zoomLevel: number) => void;
+ value: number;
+}>(function ZoomWrapper({ set, value }) {
+ const api = useStorybookApi();
-const ZoomWrapper = memo<{ set: (zoomLevel: number) => void; value: number }>(function ZoomWrapper({
- set,
- value,
-}) {
- const zoomIn = useCallback(
- (e: SyntheticEvent) => {
- e.preventDefault();
- set(0.8 * value);
- },
- [set, value]
- );
- const zoomOut = useCallback(
- (e: SyntheticEvent) => {
- e.preventDefault();
- set(1.25 * value);
- },
- [set, value]
- );
- const reset = useCallback(
- (e: SyntheticEvent) => {
- e.preventDefault();
- set(initialZoom);
+ const zoomIn = useCallback(() => {
+ const higherZoomLevel = ZOOM_LEVELS.find((level) => level > value);
+ if (higherZoomLevel) {
+ set(higherZoomLevel);
+ }
+ }, [set, value]);
+
+ const zoomOut = useCallback(() => {
+ const lowerZoomLevel = ZOOM_LEVELS.findLast((level) => level < value);
+ if (lowerZoomLevel) {
+ set(lowerZoomLevel);
+ }
+ }, [set, value]);
+
+ const zoomTo = useCallback(
+ (value: number) => {
+ set(value);
},
- [set, initialZoom]
+ [set]
);
- return ;
-});
-function ZoomToolRenderer() {
- return (
- <>
- {({ set, value }) => }
-
- >
- );
-}
+ useEffect(() => {
+ api.setAddonShortcut('zoom', {
+ label: 'Zoom to 100%',
+ defaultShortcut: ['alt', '0'],
+ actionName: 'zoomReset',
+ action: () => zoomTo(1),
+ });
+ api.setAddonShortcut('zoom', {
+ label: 'Zoom in',
+ defaultShortcut: ['alt', '='],
+ actionName: 'zoomIn',
+ action: zoomIn,
+ });
+ api.setAddonShortcut('zoom', {
+ label: 'Zoom out',
+ defaultShortcut: ['alt', '-'],
+ actionName: 'zoomOut',
+ action: zoomOut,
+ });
+ }, [api, zoomIn, zoomOut, zoomTo]);
+
+ return ;
+});
export const zoomTool: Addon_BaseType = {
title: 'zoom',
id: 'zoom',
type: types.TOOL,
match: ({ viewMode, tabId }) => viewMode === 'story' && !tabId,
- render: ZoomToolRenderer,
+ render: () => {(zoomContext) => },
};
diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx
index c6d1ed365988..55bd6b9f34da 100644
--- a/code/core/src/manager/components/sidebar/ContextMenu.tsx
+++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx
@@ -18,8 +18,8 @@ import type { API } from 'storybook/manager-api';
import { styled } from 'storybook/theming';
import type { Link } from '../../../components/components/tooltip/TooltipLinkList';
-import { Shortcut } from '../../container/Menu';
import { getMostCriticalStatusValue } from '../../utils/status';
+import { Shortcut } from '../Shortcut';
import { UseSymbol } from './IconSymbols';
import { StatusButton } from './StatusButton';
import { StatusContext } from './StatusContext';
diff --git a/code/core/src/manager/container/Menu.stories.tsx b/code/core/src/manager/container/Menu.stories.tsx
index 4c3758a574b3..5c81f339a7c9 100644
--- a/code/core/src/manager/container/Menu.stories.tsx
+++ b/code/core/src/manager/container/Menu.stories.tsx
@@ -7,8 +7,8 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { action } from 'storybook/actions';
import { initialState } from '../../shared/checklist-store/checklistData.state';
+import { Shortcut } from '../components/Shortcut';
import { internal_universalChecklistStore as mockStore } from '../manager-stores.mock';
-import { Shortcut } from './Menu';
const onLinkClick = action('onLinkClick');
diff --git a/code/core/src/manager/container/Menu.tsx b/code/core/src/manager/container/Menu.tsx
index 08bc657f2711..2590c40e694c 100644
--- a/code/core/src/manager/container/Menu.tsx
+++ b/code/core/src/manager/container/Menu.tsx
@@ -1,4 +1,3 @@
-import type { FC } from 'react';
import React, { useCallback, useMemo } from 'react';
import { ActionList, ProgressSpinner } from 'storybook/internal/components';
@@ -15,52 +14,20 @@ import {
} from '@storybook/icons';
import type { API } from 'storybook/manager-api';
-import { shortcutToHumanString } from 'storybook/manager-api';
import { styled } from 'storybook/theming';
import type { NormalLink } from '../../components/components/tooltip/TooltipLinkList';
+import { Shortcut } from '../components/Shortcut';
import { useChecklist } from '../components/sidebar/useChecklist';
export type MenuItem = NormalLink & {
closeOnClick?: boolean;
};
-const Key = styled.span(({ theme }) => ({
- display: 'inline-flex',
- alignItems: 'center',
- justifyContent: 'center',
- height: 16,
- fontSize: '11px',
- fontWeight: theme.typography.weight.regular,
- background: theme.base === 'light' ? 'rgba(0,0,0,0.05)' : 'rgba(255,255,255,0.05)',
- color: theme.base === 'light' ? theme.color.dark : theme.textMutedColor,
- borderRadius: 2,
- userSelect: 'none',
- pointerEvents: 'none',
- padding: '0 4px',
-}));
-
-const KeyChild = styled.code(({ theme }) => ({
- padding: 0,
- fontFamily: theme.typography.fonts.base,
- verticalAlign: 'middle',
- '& + &': {
- marginLeft: 6,
- },
-}));
-
const ProgressCircle = styled(ProgressSpinner)(({ theme }) => ({
color: theme.color.secondary,
}));
-export const Shortcut: FC<{ keys: string[] }> = ({ keys }) => (
-
- {keys.map((key) => (
- {shortcutToHumanString([key])}
- ))}
-
-);
-
export const useMenu = ({
api,
showToolbar,
diff --git a/code/core/src/manager/container/Preview.tsx b/code/core/src/manager/container/Preview.tsx
index e410ca0aecac..25d4d83e14b4 100644
--- a/code/core/src/manager/container/Preview.tsx
+++ b/code/core/src/manager/container/Preview.tsx
@@ -22,8 +22,8 @@ import { zoomTool } from '../components/preview/tools/zoom';
import type { PreviewProps } from '../components/preview/utils/types';
const defaultTabs = [createCanvasTab()];
-const defaultTools = [menuTool, remountTool, zoomTool];
-const defaultToolsExtra = [addonsTool, fullScreenTool, shareTool, openInEditorTool];
+const defaultTools = [menuTool, remountTool];
+const defaultToolsExtra = [zoomTool, addonsTool, fullScreenTool, shareTool, openInEditorTool];
const emptyTabsList: Addon_BaseType[] = [];