+ navigator && navigator.platform ? !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) : false;
diff --git a/code/core/src/manager-api/lib/shortcut.test.ts b/code/core/src/manager-api/lib/shortcut.test.ts
new file mode 100644
index 000000000000..e977dc6b80c6
--- /dev/null
+++ b/code/core/src/manager-api/lib/shortcut.test.ts
@@ -0,0 +1,242 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+
+import { isMacLike } from './platform';
+import type { KeyboardEventLike } from './shortcut';
+import {
+ controlOrMetaKey,
+ controlOrMetaSymbol,
+ eventMatchesShortcut,
+ eventToShortcut,
+ isShortcutTaken,
+ keyToSymbol,
+ optionOrAltSymbol,
+ shortcutMatchesShortcut,
+ shortcutToHumanString,
+} from './shortcut';
+
+// Mock the functions directly
+vi.mock('./platform', async () => {
+ return {
+ isMacLike: vi.fn(),
+ };
+});
+
+describe('shortcut', () => {
+ beforeEach(() => {
+ vi.mocked(isMacLike).mockReset();
+ });
+
+ describe('platform detection', () => {
+ it('isMacLike can be mocked', () => {
+ vi.mocked(isMacLike).mockReturnValue(true);
+ expect(isMacLike()).toBe(true);
+
+ vi.mocked(isMacLike).mockReturnValue(false);
+ expect(isMacLike()).toBe(false);
+ });
+
+ it('controlOrMetaSymbol returns correct symbol based on platform', () => {
+ // For Mac
+ vi.mocked(isMacLike).mockReturnValue(true);
+ expect(controlOrMetaSymbol()).toBe('⌘');
+
+ // For non-Mac
+ vi.mocked(isMacLike).mockReturnValue(false);
+ expect(controlOrMetaSymbol()).toBe('ctrl');
+ });
+
+ it('controlOrMetaKey returns correct key based on platform', () => {
+ // For Mac
+ vi.mocked(isMacLike).mockReturnValue(true);
+ expect(controlOrMetaKey()).toBe('meta');
+
+ // For non-Mac
+ vi.mocked(isMacLike).mockReturnValue(false);
+ expect(controlOrMetaKey()).toBe('control');
+ });
+
+ it('optionOrAltSymbol returns correct symbol based on platform', () => {
+ // For Mac
+ vi.mocked(isMacLike).mockReturnValue(true);
+ expect(optionOrAltSymbol()).toBe('⌥');
+
+ // For non-Mac
+ vi.mocked(isMacLike).mockReturnValue(false);
+ expect(optionOrAltSymbol()).toBe('alt');
+ });
+ });
+
+ describe('isShortcutTaken', () => {
+ it('returns true for identical shortcuts', () => {
+ expect(isShortcutTaken(['alt', 'K'], ['alt', 'K'])).toBe(true);
+ });
+
+ it('returns false for different shortcuts', () => {
+ expect(isShortcutTaken(['alt', 'K'], ['alt', 'J'])).toBe(false);
+ expect(isShortcutTaken(['alt', 'K'], ['meta', 'K'])).toBe(false);
+ expect(isShortcutTaken(['alt', 'K'], ['alt', 'K', 'L'])).toBe(false);
+ });
+ });
+
+ describe('eventToShortcut', () => {
+ it('returns null for meta-only key events and tab', () => {
+ const metaOnlyKeys = ['Meta', 'Alt', 'Control', 'Shift', 'Tab'];
+
+ metaOnlyKeys.forEach((key) => {
+ const event = { key } as KeyboardEventLike;
+ expect(eventToShortcut(event)).toBe(null);
+ });
+ });
+
+ it('processes modifier keys correctly', () => {
+ const event = {
+ key: 'K',
+ altKey: true,
+ ctrlKey: true,
+ metaKey: true,
+ shiftKey: true,
+ } as KeyboardEventLike;
+
+ expect(eventToShortcut(event)).toEqual(['alt', 'control', 'meta', 'shift', 'K']);
+ });
+
+ it('handles single letter keys correctly', () => {
+ const event = {
+ key: 'k',
+ } as KeyboardEventLike;
+
+ expect(eventToShortcut(event)).toEqual(['K']);
+ });
+
+ it('handles space key correctly', () => {
+ const event = {
+ key: ' ',
+ } as KeyboardEventLike;
+
+ expect(eventToShortcut(event)).toEqual(['space']);
+ });
+
+ it('handles escape key correctly', () => {
+ const event = {
+ key: 'Escape',
+ } as KeyboardEventLike;
+
+ expect(eventToShortcut(event)).toEqual(['escape']);
+ });
+
+ it('handles arrow keys correctly', () => {
+ const arrowKeys = ['ArrowRight', 'ArrowDown', 'ArrowUp', 'ArrowLeft'];
+
+ arrowKeys.forEach((key) => {
+ const event = { key } as KeyboardEventLike;
+ expect(eventToShortcut(event)).toEqual([key]);
+ });
+ });
+
+ it('supports different key/code combinations', () => {
+ const event = {
+ key: 'a',
+ code: 'KeyA',
+ } as KeyboardEventLike;
+
+ expect(eventToShortcut(event)).toEqual(['A']);
+
+ // When event.code produces a different value than event.key (e.g., with alt key on Mac)
+ const altEvent = {
+ key: 'å', // A special character
+ code: 'KeyA',
+ } as KeyboardEventLike;
+
+ expect(eventToShortcut(altEvent)).toEqual([['Å', 'A']]);
+ });
+ });
+
+ describe('shortcutMatchesShortcut', () => {
+ it('returns false when either shortcut is null', () => {
+ expect(shortcutMatchesShortcut(null as any, ['alt', 'K'])).toBe(false);
+ expect(shortcutMatchesShortcut(['alt', 'K'], null as any)).toBe(false);
+ });
+
+ it('handles shift/ shortcuts correctly', () => {
+ expect(shortcutMatchesShortcut(['shift', '/'], ['/'])).toBe(true);
+ });
+
+ it('compares shortcuts of different lengths correctly', () => {
+ expect(shortcutMatchesShortcut(['alt', 'K'], ['alt', 'K', 'L'])).toBe(false);
+ expect(shortcutMatchesShortcut(['alt', 'K', 'L'], ['alt', 'K'])).toBe(false);
+ });
+
+ it('compares shortcuts with same length correctly', () => {
+ expect(shortcutMatchesShortcut(['alt', 'K'], ['alt', 'K'])).toBe(true);
+ expect(shortcutMatchesShortcut(['alt', 'K'], ['alt', 'J'])).toBe(false);
+ expect(shortcutMatchesShortcut(['alt', ['K', 'L']], ['alt', 'K'])).toBe(true);
+ expect(shortcutMatchesShortcut(['alt', ['K', 'L']], ['alt', 'L'])).toBe(true);
+ expect(shortcutMatchesShortcut(['alt', ['K', 'L']], ['alt', 'M'])).toBe(false);
+ });
+ });
+
+ describe('eventMatchesShortcut', () => {
+ it('matches keyboard event to shortcut correctly', () => {
+ const event = {
+ key: 'K',
+ altKey: true,
+ ctrlKey: false,
+ metaKey: false,
+ shiftKey: false,
+ } as KeyboardEventLike;
+
+ expect(eventMatchesShortcut(event, ['alt', 'K'])).toBe(true);
+ expect(eventMatchesShortcut(event, ['meta', 'K'])).toBe(false);
+ });
+ });
+
+ describe('keyToSymbol', () => {
+ it('converts modifier keys to symbols', () => {
+ // For Mac
+ vi.mocked(isMacLike).mockReturnValue(true);
+
+ expect(keyToSymbol('alt')).toBe('⌥');
+ expect(keyToSymbol('control')).toBe('⌃');
+ expect(keyToSymbol('meta')).toBe('⌘');
+ expect(keyToSymbol('shift')).toBe('⇧');
+
+ // For non-Mac
+ vi.mocked(isMacLike).mockReturnValue(false);
+
+ expect(keyToSymbol('alt')).toBe('alt');
+ });
+
+ it('converts special keys to symbols', () => {
+ expect(keyToSymbol('Enter')).toBe('');
+ expect(keyToSymbol('Backspace')).toBe('');
+ expect(keyToSymbol('Esc')).toBe('');
+ expect(keyToSymbol('escape')).toBe('');
+ expect(keyToSymbol(' ')).toBe('SPACE');
+ expect(keyToSymbol('ArrowUp')).toBe('↑');
+ expect(keyToSymbol('ArrowDown')).toBe('↓');
+ expect(keyToSymbol('ArrowLeft')).toBe('←');
+ expect(keyToSymbol('ArrowRight')).toBe('→');
+ });
+
+ it('converts regular keys to uppercase', () => {
+ expect(keyToSymbol('a')).toBe('A');
+ expect(keyToSymbol('1')).toBe('1');
+ });
+ });
+
+ describe('shortcutToHumanString', () => {
+ it('converts shortcut to human-readable string', () => {
+ // For Mac
+ vi.mocked(isMacLike).mockReturnValue(true);
+
+ expect(shortcutToHumanString(['alt', 'K'])).toBe('⌥ K');
+ expect(shortcutToHumanString(['control', 'alt', 'shift', 'K'])).toBe('⌃ ⌥ ⇧ K');
+ expect(shortcutToHumanString(['meta', 'ArrowUp'])).toBe('⌘ ↑');
+
+ // For non-Mac
+ vi.mocked(isMacLike).mockReturnValue(false);
+
+ expect(shortcutToHumanString(['alt', 'K'])).toBe('alt K');
+ });
+ });
+});
diff --git a/code/core/src/manager-api/lib/shortcut.ts b/code/core/src/manager-api/lib/shortcut.ts
index afc4472ff718..45af5fab76ce 100644
--- a/code/core/src/manager-api/lib/shortcut.ts
+++ b/code/core/src/manager-api/lib/shortcut.ts
@@ -1,11 +1,8 @@
-import { global } from '@storybook/global';
-
import type { API_KeyCollection } from '../modules/shortcuts';
+import { isMacLike } from './platform';
-const { navigator } = global;
+export type { API_KeyCollection } from '../modules/shortcuts';
-export const isMacLike = () =>
- navigator && navigator.platform ? !!navigator.platform.match(/(Mac|iPhone|iPod|iPad)/i) : false;
export const controlOrMetaSymbol = () => (isMacLike() ? '⌘' : 'ctrl');
export const controlOrMetaKey = () => (isMacLike() ? 'meta' : 'control');
export const optionOrAltSymbol = () => (isMacLike() ? '⌥' : 'alt');
@@ -23,7 +20,7 @@ export type KeyboardEventLike = Pick<
// NOTE: if we change the fields on the event that we need, we'll need to update the serialization in core/preview/start.js
export const eventToShortcut = (e: KeyboardEventLike): (string | string[])[] | null => {
// Meta key only doesn't map to a shortcut
- if (['Meta', 'Alt', 'Control', 'Shift'].includes(e.key)) {
+ if (['Meta', 'Alt', 'Control', 'Shift', 'Tab'].includes(e.key)) {
return null;
}
@@ -131,6 +128,12 @@ export const eventMatchesShortcut = (
return shortcutMatchesShortcut(eventToShortcut(e)!, shortcut);
};
+/**
+ * Returns a human-readable symbol for a keyboard key.
+ *
+ * @param key The key to convert to a symbol.
+ * @returns A string that a human could understand as that keyboard key.
+ */
export const keyToSymbol = (key: string): string => {
if (key === 'alt') {
return optionOrAltSymbol();
@@ -172,3 +175,21 @@ export const keyToSymbol = (key: string): string => {
export const shortcutToHumanString = (shortcut: API_KeyCollection): string => {
return shortcut.map(keyToSymbol).join(' ');
};
+
+// Display the shortcut for use in an aria-keyshortcuts attribute
+export const shortcutToAriaKeyshortcuts = (shortcut: API_KeyCollection): string => {
+ return shortcut
+ .map((shortcut) => {
+ // aria-keyshortcuts needs `+` translated
+ if (shortcut === '+') {
+ return 'plus';
+ }
+
+ if (shortcut === ' ') {
+ return 'space';
+ }
+
+ return shortcut;
+ })
+ .join('+');
+};
diff --git a/code/core/src/manager-api/modules/layout.ts b/code/core/src/manager-api/modules/layout.ts
index f2d87a57e201..27ed403409da 100644
--- a/code/core/src/manager-api/modules/layout.ts
+++ b/code/core/src/manager-api/modules/layout.ts
@@ -327,17 +327,62 @@ export const init: ModuleFn
= ({ store, provider, singleStory
);
},
+ /**
+ * Attempts to focus (and select) an element identified by its ID. It is the responsibility of
+ * the callee to ensure that the element is present in the DOM and that no focus trap is
+ * available. This API polls and attempts to perform the focus for a set duration (max 500ms),
+ * so that race conditions can be avoided with the current API design. Because this API is
+ * historically synchronous, it cannot report errors or failure to focus. It fails silently.
+ *
+ * @param elementId The id of the element to focus.
+ * @param select Whether to call select() on the element after focusing it.
+ */
focusOnUIElement(elementId?: string, select?: boolean) {
+ // See RFC https://github.com/storybookjs/storybook/discussions/32983 for
+ // ways to make this API more robust to focus-trap race conditions.
+
if (!elementId) {
return;
}
- const element = document.getElementById(elementId);
- if (element) {
+
+ const startTime = Date.now();
+ const maxDuration = 500;
+ const pollInterval = 50;
+
+ const attemptFocus = () => {
+ const element = document.getElementById(elementId);
+ if (!element) {
+ return false;
+ }
+
element.focus();
+ if (element !== document.activeElement) {
+ return false;
+ }
+
if (select) {
- (element as any).select();
+ (element as any).select?.();
}
+ return true;
+ };
+
+ if (attemptFocus()) {
+ return;
}
+
+ // Poll every 50ms for up to 500ms to account for race conditions.
+ const intervalId = setInterval(() => {
+ const elapsed = Date.now() - startTime;
+
+ if (elapsed >= maxDuration) {
+ clearInterval(intervalId);
+ return;
+ }
+
+ if (attemptFocus()) {
+ clearInterval(intervalId);
+ }
+ }, pollInterval);
},
getInitialOptions() {
diff --git a/code/core/src/manager-api/root.tsx b/code/core/src/manager-api/root.tsx
index 193a0e892b7d..7388657698a9 100644
--- a/code/core/src/manager-api/root.tsx
+++ b/code/core/src/manager-api/root.tsx
@@ -67,6 +67,7 @@ import type { Options } from './store';
import Store from './store';
export * from './lib/request-response';
+export * from './lib/platform';
export * from './lib/shortcut';
const { ActiveTabs } = layout;
diff --git a/code/core/src/manager/App.tsx b/code/core/src/manager/App.tsx
index ec8fd08bd5c6..962fe6309b2b 100644
--- a/code/core/src/manager/App.tsx
+++ b/code/core/src/manager/App.tsx
@@ -1,8 +1,9 @@
import type { ComponentProps } from 'react';
-import React from 'react';
+import React, { useEffect } from 'react';
import type { Addon_PageType } from 'storybook/internal/types';
+import { addons } from 'storybook/manager-api';
import { Global, createGlobal } from 'storybook/theming';
import { Layout } from './components/layout/Layout';
@@ -21,6 +22,11 @@ type Props = {
export const App = ({ managerLayoutState, setManagerLayoutState, pages, hasTab }: Props) => {
const { setMobileAboutOpen } = useLayout();
+ const { enableShortcuts = true } = addons.getConfig();
+ useEffect(() => {
+ document.body.setAttribute('data-shortcuts-enabled', enableShortcuts ? 'true' : 'false');
+ }, [enableShortcuts]);
+
return (
<>
diff --git a/code/core/src/manager/components/layout/Layout.tsx b/code/core/src/manager/components/layout/Layout.tsx
index 0af2c051dd21..276fcd94cf96 100644
--- a/code/core/src/manager/components/layout/Layout.tsx
+++ b/code/core/src/manager/components/layout/Layout.tsx
@@ -238,16 +238,16 @@ const LayoutContainer = styled.div(
);
const SidebarContainer = styled.div(({ theme }) => ({
- backgroundColor: theme.background.app,
+ backgroundColor: theme.appBg,
gridArea: 'sidebar',
position: 'relative',
- borderRight: `1px solid ${theme.color.border}`,
+ borderRight: `1px solid ${theme.appBorderColor}`,
}));
const ContentContainer = styled.div<{ shown: boolean }>(({ theme, shown }) => ({
flex: 1,
position: 'relative',
- backgroundColor: theme.background.content,
+ backgroundColor: theme.appContentBg,
display: shown ? 'grid' : 'none', // This is needed to make the content container fill the available space
overflow: 'auto',
@@ -262,7 +262,7 @@ const PagesContainer = styled.div(({ theme }) => ({
gridRowEnd: '-1',
gridColumnStart: 'sidebar-end',
gridColumnEnd: '-1',
- backgroundColor: theme.background.content,
+ backgroundColor: theme.appContentBg,
zIndex: 1,
}));
@@ -270,9 +270,9 @@ const PanelContainer = styled.div<{ position: LayoutState['panelPosition'] }>(
({ theme, position }) => ({
gridArea: 'panel',
position: 'relative',
- backgroundColor: theme.background.content,
- borderTop: position === 'bottom' ? `1px solid ${theme.color.border}` : undefined,
- borderLeft: position === 'right' ? `1px solid ${theme.color.border}` : undefined,
+ backgroundColor: theme.appContentBg,
+ borderTop: position === 'bottom' ? `1px solid ${theme.appBorderColor}` : undefined,
+ borderLeft: position === 'right' ? `1px solid ${theme.appBorderColor}` : undefined,
})
);
diff --git a/code/core/src/manager/components/layout/LayoutProvider.tsx b/code/core/src/manager/components/layout/LayoutProvider.tsx
index 204935bd6ba6..78d5e5db053a 100644
--- a/code/core/src/manager/components/layout/LayoutProvider.tsx
+++ b/code/core/src/manager/components/layout/LayoutProvider.tsx
@@ -26,11 +26,15 @@ const LayoutContext = createContext({
isMobile: false,
});
-export const LayoutProvider: FC = ({ children }) => {
+export const LayoutProvider: FC<
+ PropsWithChildren & {
+ forceDesktop?: boolean;
+ }
+> = ({ children, forceDesktop }) => {
const [isMobileMenuOpen, setMobileMenuOpen] = useState(false);
const [isMobileAboutOpen, setMobileAboutOpen] = useState(false);
const [isMobilePanelOpen, setMobilePanelOpen] = useState(false);
- const isDesktop = useMediaQuery(`(min-width: ${BREAKPOINT}px)`);
+ const isDesktop = forceDesktop ?? useMediaQuery(`(min-width: ${BREAKPOINT}px)`);
const isMobile = !isDesktop;
const contextValue = useMemo(
diff --git a/code/core/src/manager/components/mobile/about/MobileAbout.stories.tsx b/code/core/src/manager/components/mobile/about/MobileAbout.stories.tsx
index 439e90a6c997..5433dd882b0b 100644
--- a/code/core/src/manager/components/mobile/about/MobileAbout.stories.tsx
+++ b/code/core/src/manager/components/mobile/about/MobileAbout.stories.tsx
@@ -63,7 +63,7 @@ export const Dark: Story = {
export const Closed: Story = {
play: async ({ canvasElement }) => {
await new Promise((resolve) => setTimeout(resolve, 1000));
- const closeButton = await within(canvasElement).getByTitle('Close about section');
- await closeButton.click();
+ const closeButton = within(canvasElement).getByText('Back');
+ closeButton.click();
},
};
diff --git a/code/core/src/manager/components/mobile/about/MobileAbout.tsx b/code/core/src/manager/components/mobile/about/MobileAbout.tsx
index c117a5410350..8ef534e73ab8 100644
--- a/code/core/src/manager/components/mobile/about/MobileAbout.tsx
+++ b/code/core/src/manager/components/mobile/about/MobileAbout.tsx
@@ -1,12 +1,12 @@
import type { FC } from 'react';
-import React, { useRef } from 'react';
+import React, { useEffect, useRef } from 'react';
-import { Link } from 'storybook/internal/components';
+import { Button, Link, ScrollArea } from 'storybook/internal/components';
import { ArrowLeftIcon, GithubIcon, ShareAltIcon, StorybookIcon } from '@storybook/icons';
-import { Transition, type TransitionStatus } from 'react-transition-group';
-import { styled } from 'storybook/theming';
+import { useTransitionState } from 'react-transition-state';
+import { keyframes, styled } from 'storybook/theming';
import { MOBILE_TRANSITION_DURATION } from '../../../constants';
import { useLayout } from '../../layout/LayoutProvider';
@@ -16,23 +16,44 @@ export const MobileAbout: FC = () => {
const { isMobileAboutOpen, setMobileAboutOpen } = useLayout();
const aboutRef = useRef(null);
+ const [state, toggle] = useTransitionState({
+ timeout: MOBILE_TRANSITION_DURATION,
+ mountOnEnter: true,
+ unmountOnExit: true,
+ });
+
+ // Update transition state when isMobileAboutOpen changes
+ useEffect(() => {
+ toggle(isMobileAboutOpen);
+ }, [isMobileAboutOpen, toggle]);
+
+ if (!state.isMounted) {
+ return null;
+ }
+
return (
-
- {(state) => (
-
- setMobileAboutOpen(false)} title="Close about section">
+
+
+ setMobileAboutOpen(false)}
+ ariaLabel="Close about section"
+ tooltip="Close about section"
+ variant="ghost"
+ >
Back
-
+
-
+
Github
@@ -40,8 +61,9 @@ export const MobileAbout: FC = () => {
@@ -53,72 +75,78 @@ export const MobileAbout: FC = () => {
Open source software maintained by{' '}
-
+
Chromatic
{' '}
and the{' '}
-
+
Storybook Community
-
- )}
-
+
+
+
);
};
-const Container = styled.div<{ state: TransitionStatus; transitionDuration: number }>(
- ({ theme, state, transitionDuration }) => ({
+const slideFromRight = keyframes({
+ from: {
+ opacity: 0,
+ transform: 'translate(20px, 0)',
+ },
+ to: {
+ opacity: 1,
+ transform: 'translate(0, 0)',
+ },
+});
+
+const slideToRight = keyframes({
+ from: {
+ opacity: 1,
+ transform: 'translate(0, 0)',
+ },
+ to: {
+ opacity: 0,
+ transform: 'translate(20px, 0)',
+ },
+});
+
+const Container = styled.div<{ $status: string; $transitionDuration: number }>(
+ ({ theme, $status, $transitionDuration }) => ({
position: 'absolute',
width: '100%',
height: '100%',
top: 0,
left: 0,
zIndex: 11,
- transition: `all ${transitionDuration}ms ease-in-out`,
- overflow: 'scroll',
- padding: '25px 10px 10px',
+ overflow: 'auto',
color: theme.color.defaultText,
background: theme.background.content,
- opacity: `${(() => {
- switch (state) {
- case 'entering':
- case 'entered':
- return 1;
- case 'exiting':
- case 'exited':
- return 0;
- default:
- return 0;
- }
- })()}`,
- transform: `${(() => {
- switch (state) {
- case 'entering':
- case 'entered':
- return 'translateX(0)';
- case 'exiting':
- case 'exited':
- return 'translateX(20px)';
- default:
- return 'translateX(0)';
- }
- })()}`,
+ animation:
+ $status === 'exiting'
+ ? `${slideToRight} ${$transitionDuration}ms`
+ : `${slideFromRight} ${$transitionDuration}ms`,
})
);
-const LinkContainer = styled.div({
- marginTop: 20,
- marginBottom: 20,
+const InnerArea = styled.div({
+ display: 'flex',
+ flexDirection: 'column',
+ gap: 20,
+ padding: '25px 12px 20px',
});
+const LinkContainer = styled.div({});
+
const LinkLine = styled.a(({ theme }) => ({
all: 'unset',
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
fontSize: theme.typography.size.s2 - 1,
- height: 52,
borderBottom: `1px solid ${theme.appBorderColor}`,
cursor: 'pointer',
padding: '0 10px',
@@ -141,12 +169,6 @@ const BottomText = styled.div(({ theme }) => ({
marginTop: 30,
}));
-const Button = styled.button(({ theme }) => ({
- all: 'unset',
- display: 'flex',
- alignItems: 'center',
- gap: 10,
- color: 'currentColor',
- fontSize: theme.typography.size.s2 - 1,
- padding: '0 10px',
-}));
+const CloseButton = styled(Button)({
+ alignSelf: 'start',
+});
diff --git a/code/core/src/manager/components/mobile/navigation/MobileAddonsDrawer.tsx b/code/core/src/manager/components/mobile/navigation/MobileAddonsDrawer.tsx
index 2170a07e3bc3..e2a627bcbb8f 100644
--- a/code/core/src/manager/components/mobile/navigation/MobileAddonsDrawer.tsx
+++ b/code/core/src/manager/components/mobile/navigation/MobileAddonsDrawer.tsx
@@ -1,92 +1,42 @@
import type { FC, ReactNode } from 'react';
-import React, { useCallback, useRef } from 'react';
+import React from 'react';
+
+import { Modal } from 'storybook/internal/components';
-import { Transition } from 'react-transition-group';
-import type { TransitionStatus } from 'react-transition-group/Transition';
import { styled } from 'storybook/theming';
import { MOBILE_TRANSITION_DURATION } from '../../../constants';
-import { useModalDialog } from '../../../hooks/useModalDialog';
interface MobileAddonsDrawerProps {
children: ReactNode;
id?: string;
isOpen: boolean;
- onClose: () => void;
+ onOpenChange: (isOpen: boolean) => void;
}
-const Container = styled.dialog<{ state: TransitionStatus }>(({ theme, state }) => ({
- position: 'fixed',
- bottom: 0,
- left: 0,
- right: 0,
- top: 'auto',
- boxSizing: 'border-box',
- width: '100%',
- maxWidth: '100vw',
+const StyledModal = styled(Modal)(({ theme }) => ({
background: theme.background.content,
- height: '42vh',
- zIndex: 11,
- overflow: 'hidden',
+ borderRadius: '10px 10px 0 0',
border: 'none',
- padding: 0,
- margin: 0,
- transform: `translateY(${(() => {
- if (state === 'entering' || state === 'entered') {
- return '0';
- }
- return '100%';
- })()})`,
- transition: `all ${MOBILE_TRANSITION_DURATION}ms ease-in-out`,
- '&[open]': {
- position: 'fixed',
- bottom: 0,
- left: 0,
- right: 0,
- top: 'auto',
- width: '100%',
- maxWidth: '100vw',
- margin: 0,
- },
-}));
-
-const ContentContainer = styled.div<{ state: TransitionStatus }>(({ state }) => ({
- width: '100%',
- height: '100%',
- transition: `all ${MOBILE_TRANSITION_DURATION}ms ease-in-out`,
- opacity: state === 'entered' || state === 'entering' ? 1 : 0,
}));
export const MobileAddonsDrawer: FC = ({
children,
id,
isOpen,
- onClose,
+ onOpenChange,
}) => {
- const dialogRef = useModalDialog({ isOpen, onClose });
-
- const forceCloseDialog = useCallback(() => {
- if (dialogRef.current && dialogRef.current.hasAttribute('open')) {
- dialogRef.current.close();
- }
- }, []);
-
return (
- {
- forceCloseDialog();
- }}
+
- {(state) => (
-
- {children}
-
- )}
-
+ {children}
+
);
};
diff --git a/code/core/src/manager/components/mobile/navigation/MobileMenuDrawer.tsx b/code/core/src/manager/components/mobile/navigation/MobileMenuDrawer.tsx
index 7e85a47c3d00..b6e450afc0e5 100644
--- a/code/core/src/manager/components/mobile/navigation/MobileMenuDrawer.tsx
+++ b/code/core/src/manager/components/mobile/navigation/MobileMenuDrawer.tsx
@@ -1,205 +1,44 @@
-import type { FC } from 'react';
-import React, { useCallback, useRef } from 'react';
+import type { FC, ReactNode } from 'react';
+import React from 'react';
+
+import { Modal } from 'storybook/internal/components';
-import { Transition } from 'react-transition-group';
-import type { TransitionStatus } from 'react-transition-group/Transition';
import { styled } from 'storybook/theming';
import { MOBILE_TRANSITION_DURATION } from '../../../constants';
-import { useModalDialog } from '../../../hooks/useModalDialog';
-import { useLayout } from '../../layout/LayoutProvider';
import { MobileAbout } from '../about/MobileAbout';
interface MobileMenuDrawerProps {
- children?: React.ReactNode;
+ children: ReactNode;
id?: string;
+ isOpen: boolean;
+ onOpenChange: (isOpen: boolean) => void;
}
-export const MobileMenuDrawer: FC = ({ children, id }) => {
- const sidebarRef = useRef(null);
- const overlayRef = useRef(null);
- const { isMobileMenuOpen, setMobileMenuOpen, isMobileAboutOpen, setMobileAboutOpen } =
- useLayout();
-
- const handleClose = useCallback(() => {
- setMobileMenuOpen(false);
- }, [setMobileMenuOpen]);
-
- const dialogRef = useModalDialog({
- isOpen: isMobileMenuOpen,
- onClose: handleClose,
- });
-
- const forceCloseDialog = useCallback(() => {
- if (dialogRef.current && dialogRef.current.hasAttribute('open')) {
- dialogRef.current.close();
- }
- }, []);
-
- return (
- <>
- {
- setMobileAboutOpen(false);
- forceCloseDialog();
- }}
- >
- {(state) => (
-
-
- {(sidebarState) => (
-
- {children}
-
- )}
-
-
-
- )}
-
-
- {(state) => (
-
- )}
-
- >
- );
-};
-
-const Container = styled.dialog<{ state: TransitionStatus }>(({ theme, state }) => ({
- position: 'fixed',
- bottom: 0,
- left: 0,
- right: 0,
- top: 'auto',
- boxSizing: 'border-box',
- width: '100%',
- maxWidth: '100vw',
+const StyledModal = styled(Modal)(({ theme }) => ({
background: theme.background.content,
- height: '80%',
- zIndex: 11,
borderRadius: '10px 10px 0 0',
- transition: `all ${MOBILE_TRANSITION_DURATION}ms ease-in-out`,
- overflow: 'hidden',
- transform: `${(() => {
- if (state === 'entering') {
- return 'translateY(0)';
- }
-
- if (state === 'entered') {
- return 'translateY(0)';
- }
-
- if (state === 'exiting') {
- return 'translateY(100%)';
- }
-
- if (state === 'exited') {
- return 'translateY(100%)';
- }
- return 'translateY(0)';
- })()}`,
border: 'none',
- padding: 0,
- margin: 0,
- '&[open]': {
- position: 'fixed',
- bottom: 0,
- left: 0,
- right: 0,
- top: 'auto',
- width: '100%',
- maxWidth: '100vw',
- margin: 0,
- },
-}));
-
-const SidebarContainer = styled.div<{ state: TransitionStatus }>(({ theme, state }) => ({
- position: 'absolute',
- width: '100%',
- height: '100%',
- top: 0,
- left: 0,
- zIndex: 1,
- transition: `all ${MOBILE_TRANSITION_DURATION}ms ease-in-out`,
- overflow: 'hidden',
- opacity: `${(() => {
- if (state === 'entered') {
- return 1;
- }
-
- if (state === 'entering') {
- return 1;
- }
-
- if (state === 'exiting') {
- return 0;
- }
-
- if (state === 'exited') {
- return 0;
- }
- return 1;
- })()}`,
- transform: `${(() => {
- switch (state) {
- case 'entering':
- case 'entered':
- return 'translateX(0)';
- case 'exiting':
- case 'exited':
- return 'translateX(-20px)';
- default:
- return 'translateX(0)';
- }
- })()}`,
}));
-const Overlay = styled.div<{ state: TransitionStatus }>(({ state }) => ({
- position: 'fixed',
- boxSizing: 'border-box',
- background: 'rgba(0, 0, 0, 0.5)',
- top: 0,
- bottom: 0,
- right: 0,
- left: 0,
- zIndex: 10,
- transition: `all ${MOBILE_TRANSITION_DURATION}ms ease-in-out`,
- cursor: 'pointer',
- opacity: `${(() => {
- switch (state) {
- case 'entering':
- case 'entered':
- return 1;
- case 'exiting':
- case 'exited':
- return 0;
- default:
- return 0;
- }
- })()}`,
-
- '&:hover': {
- background: 'rgba(0, 0, 0, 0.6)',
- },
-}));
+export const MobileMenuDrawer: FC = ({
+ children,
+ id,
+ isOpen,
+ onOpenChange,
+}) => {
+ return (
+
+ {children}
+
+
+ );
+};
diff --git a/code/core/src/manager/components/mobile/navigation/MobileNavigation.stories.tsx b/code/core/src/manager/components/mobile/navigation/MobileNavigation.stories.tsx
index 3b0dfd53d7a2..804af1d9bbc4 100644
--- a/code/core/src/manager/components/mobile/navigation/MobileNavigation.stories.tsx
+++ b/code/core/src/manager/components/mobile/navigation/MobileNavigation.stories.tsx
@@ -4,11 +4,27 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { startCase } from 'es-toolkit/string';
import { ManagerContext } from 'storybook/manager-api';
-import { fn, within } from 'storybook/test';
+import { fn, screen, userEvent } from 'storybook/test';
import { LayoutProvider, useLayout } from '../../layout/LayoutProvider';
import { MobileNavigation } from './MobileNavigation';
+const MockMenu = () => {
+ const { setMobileMenuOpen } = useLayout();
+ return (
+
+ menu
+ setMobileMenuOpen(false)}
+ >
+ close
+
+
+ );
+};
+
const MockPanel = () => {
const { setMobilePanelOpen } = useLayout();
return (
@@ -83,7 +99,7 @@ const meta = {
chromatic: { viewports: [320] },
},
args: {
- menu: navigation menu
,
+ menu: ,
panel: ,
showPanel: true,
},
@@ -146,9 +162,9 @@ export const LongStoryName: Story = {
};
export const MenuOpen: Story = {
- play: async ({ canvasElement }) => {
- const menuOpen = await within(canvasElement).getByLabelText('Open navigation menu');
- await menuOpen.click();
+ play: async ({ canvas }) => {
+ const menuOpen = await canvas.findByLabelText('Open navigation menu', {}, { timeout: 3000 });
+ await userEvent.click(menuOpen);
},
};
@@ -157,15 +173,15 @@ export const MenuClosed: Story = {
// @ts-expect-error (non strict)
await MenuOpen.play(context);
await new Promise((resolve) => setTimeout(resolve, 500));
- const overlay = await within(context.canvasElement).getByLabelText('Close navigation menu');
- await overlay.click();
+ const overlay = await screen.findByLabelText('Close navigation menu');
+ await userEvent.click(overlay);
},
};
export const PanelOpen: Story = {
- play: async ({ canvasElement }) => {
- const panelButton = await within(canvasElement).getByLabelText('Open addon panel');
- await panelButton.click();
+ play: async ({ canvas }) => {
+ const panelButton = await canvas.findByLabelText('Open addon panel', {}, { timeout: 3000 });
+ await userEvent.click(panelButton);
},
};
@@ -174,8 +190,8 @@ export const PanelClosed: Story = {
// @ts-expect-error (non strict)
await PanelOpen.play(context);
await new Promise((resolve) => setTimeout(resolve, 500));
- const closeButton = await within(context.canvasElement).getByLabelText('Close addon panel');
- await closeButton.click();
+ const closeButton = await screen.findByLabelText('Close addon panel');
+ await userEvent.click(closeButton);
},
};
diff --git a/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx b/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx
index 8b5a9ca2c138..c1dde6c08125 100644
--- a/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx
+++ b/code/core/src/manager/components/mobile/navigation/MobileNavigation.tsx
@@ -1,11 +1,12 @@
-import type { ComponentProps, FC } from 'react';
import React from 'react';
+import type { ComponentProps, FC } from 'react';
-import { IconButton } from 'storybook/internal/components';
+import { Button } from 'storybook/internal/components';
import type { API_IndexHash, API_Refs } from 'storybook/internal/types';
import { BottomBarToggleIcon, MenuIcon } from '@storybook/icons';
+import { useId } from '@react-aria/utils';
import { useStorybookApi, useStorybookState } from 'storybook/manager-api';
import { styled } from 'storybook/theming';
@@ -74,45 +75,58 @@ export const MobileNavigation: FC {
- setMobilePanelOpen(false);
- };
+ const headingId = useId();
return (
-
+
{panel}
{!isMobilePanelOpen && (
-
-
+
+ Navigation controls
+
+ setMobileMenuOpen(!isMobileMenuOpen)}
- aria-label="Open navigation menu"
+ ariaLabel="Open navigation menu"
aria-expanded={isMobileMenuOpen}
aria-controls="storybook-mobile-menu"
>
{fullStoryName}
-
+
+
+ {fullStoryName}
+
{showPanel && (
- setMobilePanelOpen(true)}
- aria-label="Open addon panel"
+ ariaLabel="Open addon panel"
aria-expanded={isMobilePanelOpen}
aria-controls="storybook-mobile-addon-panel"
>
-
+
)}
-
+
)}
);
@@ -127,37 +141,35 @@ const Container = styled.div(({ theme }) => ({
borderTop: `1px solid ${theme.appBorderColor}`,
}));
-const Nav = styled.div({
+const MobileBottomBar = styled.section({
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
width: '100%',
height: 40,
padding: '0 6px',
-});
-const Button = styled.button(({ theme }) => ({
- all: 'unset',
- display: 'flex',
- alignItems: 'center',
- gap: 10,
- color: theme.barTextColor,
- fontSize: `${theme.typography.size.s2 - 1}px`,
- padding: '0 7px',
- fontWeight: theme.typography.weight.bold,
- WebkitLineClamp: 1,
-
- '> svg': {
- width: 14,
- height: 14,
- flexShrink: 0,
+ /* Because Popper.js's tooltip is creating extra div layers, we have to
+ * punch through them to configure the button to ellipsize. */
+ '& > *:first-child': {
+ /* 6px padding * 2 + 28px for the orientation button */
+ maxWidth: 'calc(100% - 40px)',
+ '& > button': {
+ maxWidth: '100%',
+ },
+ '& > button p': {
+ textOverflow: 'ellipsis',
+ },
},
+});
- '&:focus-visible': {
- outline: `2px solid ${theme.color.secondary}`,
- outlineOffset: 2,
+const BottomBarButton = styled(Button)({
+ WebkitLineClamp: 1,
+ flexShrink: 1,
+ p: {
+ textOverflow: 'ellipsis',
},
-}));
+});
const Text = styled.p({
display: '-webkit-box',
diff --git a/code/core/src/manager/components/notifications/NotificationItem.tsx b/code/core/src/manager/components/notifications/NotificationItem.tsx
index 286578dbd109..e87c07af46ab 100644
--- a/code/core/src/manager/components/notifications/NotificationItem.tsx
+++ b/code/core/src/manager/components/notifications/NotificationItem.tsx
@@ -1,7 +1,7 @@
import type { FC, SyntheticEvent } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
-import { IconButton } from 'storybook/internal/components';
+import { Button } from 'storybook/internal/components';
import { Link } from 'storybook/internal/router';
import { CloseAltIcon } from '@storybook/icons';
@@ -107,10 +107,10 @@ const NotificationTextWrapper = styled.div(({ theme }) => ({
width: '100%',
display: 'flex',
flexDirection: 'column',
- color: theme.base === 'dark' ? theme.color.mediumdark : theme.color.mediumlight,
+ color: theme.color.inverseText,
}));
-const Headline = styled.div<{ hasIcon: boolean }>(({ theme, hasIcon }) => ({
+const Headline = styled.div(({ theme }) => ({
height: '100%',
alignItems: 'center',
whiteSpace: 'balance',
@@ -136,15 +136,13 @@ const ItemContent: FC> = ({
<>
{!icon || {icon} }
-
- {headline}
-
+ {headline}
{subHeadline && {subHeadline} }
>
);
-const DismissButtonWrapper = styled(IconButton)(({ theme }) => ({
+const DismissButtonWrapper = styled(Button)(({ theme }) => ({
width: 28,
alignSelf: 'center',
marginTop: 0,
@@ -155,7 +153,9 @@ const DismissNotificationItem: FC<{
onDismiss: () => void;
}> = ({ onDismiss }) => (
{
e.preventDefault();
e.stopPropagation();
diff --git a/code/core/src/manager/components/panel/Panel.tsx b/code/core/src/manager/components/panel/Panel.tsx
index 5bc1993603ae..a41c2b38ae3d 100644
--- a/code/core/src/manager/components/panel/Panel.tsx
+++ b/code/core/src/manager/components/panel/Panel.tsx
@@ -1,12 +1,20 @@
-import React, { Component } from 'react';
+import type { ReactNode } from 'react';
+import React, { Component, useMemo } from 'react';
-import { EmptyTabContent, IconButton, Link, Tabs } from 'storybook/internal/components';
+import {
+ Button,
+ EmptyTabContent,
+ Link,
+ StatelessTab,
+ StatelessTabList,
+ StatelessTabPanel,
+ StatelessTabsView,
+} from 'storybook/internal/components';
import type { Addon_BaseType } from 'storybook/internal/types';
import { BottomBarIcon, CloseIcon, DocumentIcon, SidebarAltIcon } from '@storybook/icons';
import type { State } from 'storybook/manager-api';
-import { shortcutToHumanString } from 'storybook/manager-api';
import { styled } from 'storybook/theming';
import { useLayout } from '../layout/LayoutProvider';
@@ -17,29 +25,57 @@ export interface SafeTabProps {
children: Addon_BaseType['render'];
}
-class SafeTab extends Component {
- constructor(props: SafeTabProps) {
+interface ErrorBoundaryProps {
+ children: ReactNode;
+}
+
+class TabErrorBoundary extends Component {
+ constructor(props: ErrorBoundaryProps) {
super(props);
this.state = { hasError: false };
}
- componentDidCatch(error: Error, info: any) {
- this.setState({ hasError: true });
+ static getDerivedStateFromError() {
+ return { hasError: true };
+ }
- console.error(error, info);
+ componentDidCatch(error: Error, info: React.ErrorInfo) {
+ console.error('Error rendering addon panel');
+ console.error(error);
+ console.error(info.componentStack);
}
- // @ts-expect-error (we know this is broken)
render() {
const { hasError } = this.state;
- const { children } = this.props;
if (hasError) {
- return Something went wrong. ;
+ return (
+
+ );
}
+
+ const { children } = this.props;
return children;
}
}
+const Section = styled.section({
+ height: '100%',
+ display: 'flex',
+ flexDirection: 'column',
+});
+
+// Avoids crashes due to rules of hooks.
+const PreRenderAddons = ({ panels }: { panels: Record }) => {
+ return Object.entries(panels).map(([k, v]) => (
+
+ {v.render({ active: true })}
+
+ ));
+};
+
export const AddonPanel = React.memo<{
selectedPanel?: string;
actions: { onSelect: (id: string) => void } & Record;
@@ -47,84 +83,98 @@ export const AddonPanel = React.memo<{
shortcuts: State['shortcuts'];
panelPosition?: 'bottom' | 'right';
absolute?: boolean;
-}>(
- ({
- panels,
- shortcuts,
- actions,
- selectedPanel = null,
- panelPosition = 'right',
- absolute = true,
- }) => {
- const { isDesktop, setMobilePanelOpen } = useLayout();
-
- return (
-
- Integrate your tools with Storybook to connect workflows and unlock advanced
- features.
- >
- }
- footer={
-
- Explore integrations catalog
-
- }
- />
- }
- tools={
-
- {isDesktop ? (
- <>
-
- {panelPosition === 'bottom' ? : }
-
-
-
-
- >
- ) : (
- setMobilePanelOpen(false)} aria-label="Close addon panel">
-
-
- )}
-
- }
+}>(({ panels, shortcuts, actions, selectedPanel = null, panelPosition = 'right' }) => {
+ const { isDesktop, setMobilePanelOpen } = useLayout();
+
+ const emptyState = (
+ Integrate your tools with Storybook to connect workflows and unlock advanced features.>
+ }
+ footer={
+
+ Explore integrations catalog
+
+ }
+ />
+ );
+
+ const tools = useMemo(
+ () => (
+
+ {isDesktop ? (
+ <>
+
+ {panelPosition === 'bottom' ? : }
+
+
+
+
+ >
+ ) : (
+ setMobilePanelOpen(false)}
+ ariaLabel="Close addon panel"
+ >
+
+
+ )}
+
+ ),
+ [actions, isDesktop, panelPosition, setMobilePanelOpen, shortcuts]
+ );
+
+ return (
+
+
+ Addon panel
+
+ actions.onSelect(id)}
+ tools={tools}
>
- {Object.entries(panels).map(([k, v]) => (
- // @ts-expect-error (we know this is broken)
- : v.title}>
- {v.render}
-
- ))}
-
- );
- }
-);
+
+ {Object.entries(panels).map(([k, v]) => (
+
+ {typeof v.title === 'function' ? : v.title}
+
+ ))}
+
+ {Object.keys(panels).length ? : null}
+
+
+ );
+});
AddonPanel.displayName = 'AddonPanel';
-const Actions = styled.div({
+const ActionsWrapper = styled.div({
display: 'flex',
alignItems: 'center',
gap: 6,
diff --git a/code/core/src/manager/components/preview/FramesRenderer.tsx b/code/core/src/manager/components/preview/FramesRenderer.tsx
index c1dd0bdda83b..fd15d6e19a95 100644
--- a/code/core/src/manager/components/preview/FramesRenderer.tsx
+++ b/code/core/src/manager/components/preview/FramesRenderer.tsx
@@ -99,7 +99,7 @@ export const FramesRenderer: FC = ({
return null;
}
return (
-
+
Skip to sidebar
diff --git a/code/core/src/manager/components/preview/Preview.tsx b/code/core/src/manager/components/preview/Preview.tsx
index 48d089af47ea..dc21a9699dda 100644
--- a/code/core/src/manager/components/preview/Preview.tsx
+++ b/code/core/src/manager/components/preview/Preview.tsx
@@ -1,12 +1,14 @@
import type { FC } from 'react';
import React, { Fragment, useEffect, useRef, useState } from 'react';
-import { Loader } from 'storybook/internal/components';
+import { deprecate } from 'storybook/internal/client-logger';
+import { Loader, useTabsState } from 'storybook/internal/components';
import { PREVIEW_BUILDER_PROGRESS, SET_CURRENT_STORY } from 'storybook/internal/core-events';
import type { Addon_BaseType, Addon_WrapperType } from 'storybook/internal/types';
import { global } from '@storybook/global';
+import type { TabListState } from '@react-stately/tabs';
import { Helmet } from 'react-helmet-async';
import { type Combo, Consumer, addons, merge, types } from 'storybook/manager-api';
@@ -56,6 +58,30 @@ const Preview = React.memo(function Preview(props) {
tabId,
} = props;
+ // SB11: remove code
+ // NOTE: we interface with the old API without rewriting the UI because we know
+ // that addon tabs are pretty rare and we want to deprecate them. To make this UI
+ // accessible, we'd need to pass tabContent/CanvasWrap to the tabs consumed by
+ // the TabPanel. It's doable, but not worth the effort considering the feature's
+ // remaining lifespan.
+ const tabState = useTabsState({
+ selected: tabId ?? 'canvas',
+ onSelectionChange: (key) => {
+ api.applyQueryParams({ tab: key === 'canvas' ? undefined : key });
+ },
+ tabs: tabs.map((tab, index) => ({
+ id: tab.id ?? `tab-${index}`,
+ title: tab.title,
+ isDisabled: !!tab.disabled,
+ children: () => tab.render({ active: true }),
+ })),
+ });
+
+ if (tabs.length > 1) {
+ deprecate('Addon tabs are deprecated and will be removed in Storybook 11.');
+ }
+ // SB11: end remove code
+
const tabContent = tabs.find((tab) => tab.id === tabId)?.render;
const shouldScale = viewMode === 'story';
@@ -96,16 +122,17 @@ const Preview = React.memo(function Preview(props) {
}
tools={tools}
toolsExtra={toolsExtra}
- api={api}
/>
-
+
+
+ Main preview area
+
{tabContent && {tabContent({ active: true })} }
-
+
diff --git a/code/core/src/manager/components/preview/Toolbar.tsx b/code/core/src/manager/components/preview/Toolbar.tsx
index 028691c8a936..640ee5194e10 100644
--- a/code/core/src/manager/components/preview/Toolbar.tsx
+++ b/code/core/src/manager/components/preview/Toolbar.tsx
@@ -1,10 +1,11 @@
-import React, { Fragment, useId } from 'react';
+import React from 'react';
-import { IconButton, Separator, TabBar, TabButton } from 'storybook/internal/components';
+import { AbstractToolbar, Button, Separator, TabList } from 'storybook/internal/components';
import { type Addon_BaseType, Addon_TypesEnum } from 'storybook/internal/types';
import { CloseIcon, ExpandIcon } from '@storybook/icons';
+import type { TabListState } from '@react-stately/tabs';
import {
type API,
type Combo,
@@ -13,7 +14,6 @@ import {
type State,
addons,
merge,
- shortcutToHumanString,
types,
} from 'storybook/manager-api';
import { styled } from 'storybook/theming';
@@ -28,7 +28,7 @@ const fullScreenMapper = ({ api, state }: Combo) => {
return {
toggle: api.toggleFullscreen,
isFullscreen: api.getIsFullscreen(),
- shortcut: shortcutToHumanString(api.getShortcutKeys().fullScreen),
+ shortcut: api.getShortcutKeys().fullScreen,
hasPanel: Object.keys(api.getElements(Addon_TypesEnum.PANEL)).length > 0,
singleStory: state.singleStory,
};
@@ -51,14 +51,16 @@ export const fullScreenTool: Addon_BaseType = {
{({ toggle, isFullscreen, shortcut, hasPanel, singleStory }) =>
(!singleStory || (singleStory && hasPanel)) && (
- toggle()}
+ ariaLabel={isFullscreen ? 'Exit full screen' : 'Enter full screen'}
+ shortcut={shortcut}
>
{isFullscreen ? : }
-
+
)
}
@@ -66,54 +68,12 @@ export const fullScreenTool: Addon_BaseType = {
},
};
-const tabsMapper = ({ api, state }: Combo) => ({
- navigate: api.navigate,
- path: state.path,
- applyQueryParams: api.applyQueryParams,
-});
-
-export const createTabsTool = (tabs: Addon_BaseType[]): Addon_BaseType => ({
- title: 'title',
- id: 'title',
- type: types.TOOL,
- render: () => (
-
- {(rp) => (
-
-
- {tabs
- .filter(({ hidden }) => !hidden)
- .map((tab, index) => {
- const tabIdToApply = tab.id === 'canvas' ? undefined : tab.id;
- const isActive = rp.path.includes(`tab=${tab.id}`);
- return (
- {
- rp.applyQueryParams({ tab: tabIdToApply });
- }}
- key={tab.id || `tab-${index}`}
- >
- {tab.title as any}
-
- );
- })}
-
-
-
- )}
-
- ),
-});
-
export interface ToolData {
isShown: boolean;
tabs: Addon_BaseType[];
+ tabState: TabListState;
tools: Addon_BaseType[];
- tabId: string;
toolsExtra: Addon_BaseType[];
- api: API;
}
export const ToolbarComp = React.memo(function ToolbarComp({
@@ -121,62 +81,40 @@ export const ToolbarComp = React.memo(function ToolbarComp({
tools,
toolsExtra,
tabs,
- tabId,
- api,
+ tabState,
}) {
- const id = useId();
- return tabs || tools || toolsExtra ? (
-
-
+
-
-
- {tabs.length > 1 ? (
-
-
- {tabs.map((tab, index) => {
- return (
- {
- api.applyQueryParams({ tab: tab.id === 'canvas' ? undefined : tab.id });
- }}
- key={tab.id || `tab-${index}`}
- >
- {tab.title as any}
-
- );
- })}
-
-
-
- ) : null}
-
-
-
-
-
-
-
+
+ {tabs.length > 1 ? (
+ <>
+
+
+ >
+ ) : null}
+
+
+
+
+
) : null;
});
export const Tools = React.memo<{ list: Addon_BaseType[] }>(function Tools({ list }) {
return (
- <>
+
{list.filter(Boolean).map(({ render: Render, id, ...t }, index) => (
// @ts-expect-error (Converted from ts-ignore)
))}
- >
+
);
});
@@ -219,14 +157,15 @@ export function filterToolsSide(
return tools.filter(filter);
}
-const Toolbar = styled.section<{ shown: boolean }>(({ theme, shown }) => ({
+const StyledSection = styled.section(({ theme }) => ({
position: 'relative',
+ display: 'flex',
+ alignItems: 'center',
color: theme.barTextColor,
width: '100%',
flexShrink: 0,
overflowX: 'auto',
overflowY: 'hidden',
- marginTop: shown ? 0 : -40,
boxShadow: `${theme.appBorderColor} 0 -1px 0 0 inset`,
background: theme.barBg,
scrollbarColor: `${theme.barTextColor} ${theme.barBg}`,
@@ -234,25 +173,21 @@ const Toolbar = styled.section<{ shown: boolean }>(({ theme, shown }) => ({
zIndex: 4,
}));
-const ToolbarInner = styled.div({
- width: 'calc(100% - 20px)',
+const StyledToolbar = styled(AbstractToolbar)({
+ flex: 1,
display: 'flex',
justifyContent: 'space-between',
flexWrap: 'nowrap',
flexShrink: 0,
height: 40,
- marginLeft: 10,
- marginRight: 10,
+ marginInline: 10,
+ gap: 30,
});
-const ToolbarLeft = styled.div({
+const ToolGroup = styled.div({
display: 'flex',
whiteSpace: 'nowrap',
flexBasis: 'auto',
gap: 6,
alignItems: 'center',
});
-
-const ToolbarRight = styled(ToolbarLeft)({
- marginLeft: 30,
-});
diff --git a/code/core/src/manager/components/preview/tools/addons.tsx b/code/core/src/manager/components/preview/tools/addons.tsx
index 633f4705f31d..2a879f700f93 100644
--- a/code/core/src/manager/components/preview/tools/addons.tsx
+++ b/code/core/src/manager/components/preview/tools/addons.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { IconButton } from 'storybook/internal/components';
+import { Button } from 'storybook/internal/components';
import type { Addon_BaseType } from 'storybook/internal/types';
import { BottomBarIcon, SidebarAltIcon } from '@storybook/icons';
@@ -26,9 +26,15 @@ export const addonsTool: Addon_BaseType = {
!singleStory &&
!isVisible && (
<>
-
+
{panelPosition === 'bottom' ? : }
-
+
>
)
}
diff --git a/code/core/src/manager/components/preview/tools/menu.tsx b/code/core/src/manager/components/preview/tools/menu.tsx
index c1aa2f50f62f..0cb0531d5705 100644
--- a/code/core/src/manager/components/preview/tools/menu.tsx
+++ b/code/core/src/manager/components/preview/tools/menu.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { IconButton, Separator } from 'storybook/internal/components';
+import { Button, Separator } from 'storybook/internal/components';
import type { Addon_BaseType } from 'storybook/internal/types';
import { MenuIcon } from '@storybook/icons';
@@ -26,9 +26,15 @@ export const menuTool: Addon_BaseType = {
!singleStory &&
!isVisible && (
<>
-
+
-
+
>
)
diff --git a/code/core/src/manager/components/preview/tools/open-in-editor.tsx b/code/core/src/manager/components/preview/tools/open-in-editor.tsx
index 8515ab948ca7..fa01bfdca8cb 100644
--- a/code/core/src/manager/components/preview/tools/open-in-editor.tsx
+++ b/code/core/src/manager/components/preview/tools/open-in-editor.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { IconButton } from 'storybook/internal/components';
+import { Button } from 'storybook/internal/components';
import type { Addon_BaseType } from 'storybook/internal/types';
import { global } from '@storybook/global';
@@ -36,18 +36,19 @@ export const openInEditorTool: Addon_BaseType = {
return null;
}
return (
-
api.openInEditor({
file: importPath,
})
}
- title="Open in editor"
- aria-label="Open in editor"
+ ariaLabel="Open in editor"
+ padding="small"
+ variant="ghost"
>
-
+
);
}}
diff --git a/code/core/src/manager/components/preview/tools/remount.tsx b/code/core/src/manager/components/preview/tools/remount.tsx
index 0ebcc4044895..e042b8bb9fef 100644
--- a/code/core/src/manager/components/preview/tools/remount.tsx
+++ b/code/core/src/manager/components/preview/tools/remount.tsx
@@ -1,7 +1,7 @@
import type { ComponentProps } from 'react';
-import React, { useState } from 'react';
+import React, { useEffect, useState } from 'react';
-import { IconButton } from 'storybook/internal/components';
+import { Button } from 'storybook/internal/components';
import { FORCE_REMOUNT } from 'storybook/internal/core-events';
import type { Addon_BaseType } from 'storybook/internal/types';
@@ -15,8 +15,8 @@ interface AnimatedButtonProps {
animating?: boolean;
}
-const StyledAnimatedIconButton = styled(IconButton)<
- AnimatedButtonProps & Pick, 'disabled'>
+const StyledAnimatedButton = styled(Button)<
+ AnimatedButtonProps & Pick, 'disabled'>
>(({ theme, animating, disabled }) => ({
opacity: disabled ? 0.5 : 1,
svg: {
@@ -49,21 +49,25 @@ export const remountTool: Addon_BaseType = {
remount();
};
- api.on(FORCE_REMOUNT, () => {
- setIsAnimating(true);
- });
+ useEffect(() => {
+ const handler = () => setIsAnimating(true);
+ api.on(FORCE_REMOUNT, handler);
+ return () => api.off?.(FORCE_REMOUNT, handler);
+ }, [api]);
return (
- setIsAnimating(false)}
animating={isAnimating}
disabled={!storyId}
>
-
+
);
}}
diff --git a/code/core/src/manager/components/preview/tools/share.tsx b/code/core/src/manager/components/preview/tools/share.tsx
index 2549a76461fa..5517dec0b54c 100644
--- a/code/core/src/manager/components/preview/tools/share.tsx
+++ b/code/core/src/manager/components/preview/tools/share.tsx
@@ -1,9 +1,9 @@
import React, { useMemo, useState } from 'react';
import {
- IconButton,
+ Button,
+ PopoverProvider,
TooltipLinkList,
- WithTooltip,
getStoryHref,
} from 'storybook/internal/components';
import type { Addon_BaseType } from 'storybook/internal/types';
@@ -164,17 +164,18 @@ export const shareTool: Addon_BaseType = {
: window.location.href;
return storyId ? (
-
}
>
-
+
-
-
+
+
) : null;
}}
diff --git a/code/core/src/manager/components/preview/tools/zoom.tsx b/code/core/src/manager/components/preview/tools/zoom.tsx
index 742e4fdd9839..bc11e58a87fc 100644
--- a/code/core/src/manager/components/preview/tools/zoom.tsx
+++ b/code/core/src/manager/components/preview/tools/zoom.tsx
@@ -1,7 +1,7 @@
-import type { MouseEventHandler, PropsWithChildren, SyntheticEvent } from 'react';
+import type { EventHandler, PropsWithChildren, SyntheticEvent } from 'react';
import React, { Component, createContext, memo, useCallback } from 'react';
-import { IconButton, Separator } from 'storybook/internal/components';
+import { Button, Separator } from 'storybook/internal/components';
import type { Addon_BaseType } from 'storybook/internal/types';
import { ZoomIcon, ZoomOutIcon, ZoomResetIcon } from '@storybook/icons';
@@ -37,24 +37,27 @@ class ZoomProvider extends Component<
const { Consumer: ZoomConsumer } = Context;
const Zoom = memo<{
- zoomIn: MouseEventHandler;
- zoomOut: MouseEventHandler;
- reset: MouseEventHandler;
+ zoomIn: EventHandler;
+ zoomOut: EventHandler;
+ reset: EventHandler;
}>(function Zoom({ zoomIn, zoomOut, reset }) {
return (
<>
- {/* @ts-expect-error (non strict) */}
-
+
-
- {/* @ts-expect-error (non strict) */}
-
+
+
-
- {/* @ts-expect-error (non strict) */}
-
+
+
-
+
>
);
});
diff --git a/code/core/src/manager/components/preview/utils/components.ts b/code/core/src/manager/components/preview/utils/components.ts
index b5a355618312..1315aa1cd6bf 100644
--- a/code/core/src/manager/components/preview/utils/components.ts
+++ b/code/core/src/manager/components/preview/utils/components.ts
@@ -10,7 +10,7 @@ export const PreviewContainer = styled.main({
overflow: 'hidden',
});
-export const FrameWrap = styled.div({
+export const FrameWrap = styled.section({
overflow: 'auto',
width: '100%',
zIndex: 3,
diff --git a/code/core/src/manager/components/sidebar/Brand.tsx b/code/core/src/manager/components/sidebar/Brand.tsx
index d3146a267e58..1d5483f94656 100644
--- a/code/core/src/manager/components/sidebar/Brand.tsx
+++ b/code/core/src/manager/components/sidebar/Brand.tsx
@@ -18,7 +18,8 @@ export const Img = styled.img({
});
export const LogoLink = styled.a(({ theme }) => ({
- display: 'inline-block',
+ display: 'inline-flex',
+ alignItems: 'center',
height: '100%',
margin: '-3px -4px',
padding: '2px 3px',
@@ -26,9 +27,9 @@ export const LogoLink = styled.a(({ theme }) => ({
borderRadius: 3,
color: 'inherit',
textDecoration: 'none',
- '&:focus': {
- outline: 0,
- borderColor: theme.color.secondary,
+ '&:focus-visible': {
+ outline: `2px solid ${theme.color.secondary}`,
+ outlineOffset: 2,
},
}));
diff --git a/code/core/src/manager/components/sidebar/ContextMenu.tsx b/code/core/src/manager/components/sidebar/ContextMenu.tsx
index 5f1ad015f6ba..e015820d492f 100644
--- a/code/core/src/manager/components/sidebar/ContextMenu.tsx
+++ b/code/core/src/manager/components/sidebar/ContextMenu.tsx
@@ -1,7 +1,7 @@
import type { ComponentProps, FC, SyntheticEvent } from 'react';
import React, { useMemo, useState } from 'react';
-import { TooltipLinkList, WithTooltip } from 'storybook/internal/components';
+import { PopoverProvider, TooltipLinkList } from 'storybook/internal/components';
import {
type API_HashEntry,
type Addon_Collection,
@@ -26,15 +26,15 @@ const empty = {
node: null,
};
-const PositionedWithTooltip = styled(WithTooltip)({
- position: 'absolute',
- right: 0,
- zIndex: 1,
-});
-
const FloatingStatusButton = styled(StatusButton)({
background: 'var(--tree-node-background-hover)',
boxShadow: '0 0 5px 5px var(--tree-node-background-hover)',
+ position: 'absolute',
+ right: 0,
+ zIndex: 1,
+ '&:focus-visible': {
+ outlineOffset: -2,
+ },
});
export const useContextMenu = (context: API_HashEntry, links: Link[], api: API) => {
@@ -127,24 +127,26 @@ export const useContextMenu = (context: API_HashEntry, links: Link[], api: API)
return {
onMouseEnter: handlers.onMouseEnter,
node: shouldRender ? (
- {
- if (!visible) {
- handlers.onClose();
- } else {
- setIsOpen(true);
- }
- }}
- tooltip={ }
+ defaultVisible={false}
+ visible={isOpen}
+ onVisibleChange={setIsOpen}
+ popover={ }
+ hasChrome={true}
+ padding={0}
>
-
+
-
+
) : null,
};
}, [context, handlers, isOpen, shouldRender, links, topLinks]);
diff --git a/code/core/src/manager/components/sidebar/FileList.tsx b/code/core/src/manager/components/sidebar/FileList.tsx
index a2c179439801..9dfb6221c206 100644
--- a/code/core/src/manager/components/sidebar/FileList.tsx
+++ b/code/core/src/manager/components/sidebar/FileList.tsx
@@ -16,7 +16,7 @@ export const FileListWrapper = styled('div')(({ theme }) => ({
},
}));
-export const FileList = styled('div')(({ theme }) => ({
+export const FileList = styled('div')({
height: '280px',
overflow: 'auto',
msOverflowStyle: 'none',
@@ -25,7 +25,7 @@ export const FileList = styled('div')(({ theme }) => ({
'::-webkit-scrollbar': {
display: 'none',
},
-}));
+});
export const FileListLi = styled('li')(({ theme }) => ({
':focus-visible': {
@@ -42,11 +42,11 @@ export const FileListLi = styled('li')(({ theme }) => ({
},
}));
-export const FileListItem = styled('div')(({ theme }) => ({
+export const FileListItem = styled('div')({
display: 'flex',
flexDirection: 'column',
position: 'relative',
-}));
+});
export const FileListItemContentWrapper = styled.div<{
selected: boolean;
@@ -74,7 +74,7 @@ export const FileListItemContentWrapper = styled.div<{
cursor: 'not-allowed',
div: {
- color: `${theme.color.mediumdark} !important`,
+ color: `${theme.textMutedColor} !important`,
},
}),
@@ -127,7 +127,7 @@ export const FileListItemLabel = styled('div')<{ error: boolean }>(({ theme, err
}));
export const FileListItemPath = styled('div')(({ theme }) => ({
- color: theme.color.mediumdark,
+ color: theme.textMutedColor,
fontSize: '14px',
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
@@ -135,14 +135,13 @@ export const FileListItemPath = styled('div')(({ theme }) => ({
maxWidth: '100%',
}));
-export const FileListExport = styled('ul')(({ theme }) => ({
+export const FileListExport = styled('ul')({
margin: 0,
padding: 0,
-}));
+});
export const FileListItemExport = styled('li')<{ error: boolean }>(({ theme, error }) => ({
- padding: '8px 16px 8px 16px',
- marginLeft: '30px',
+ padding: '8px 16px 8px 58px',
display: 'flex',
gap: '8px',
alignItems: 'center',
@@ -177,27 +176,31 @@ export const FileListItemExport = styled('li')<{ error: boolean }>(({ theme, err
},
}));
-export const FileListItemExportName = styled('div')(({ theme }) => ({
+export const FileListItemExportName = styled('div')({
display: 'flex',
alignItems: 'center',
gap: '8px',
width: 'calc(100% - 20px)',
-}));
+});
export const FileListItemExportNameContent = styled('span')(({ theme }) => ({
whiteSpace: 'nowrap',
textOverflow: 'ellipsis',
overflow: 'hidden',
- maxWidth: 'calc(100% - 160px)',
display: 'inline-block',
+ color: theme.base === 'dark' ? theme.color.lightest : theme.color.darkest,
}));
+export const FileListItemExportNameContentWithExport = styled(FileListItemExportNameContent)({
+ maxWidth: 'calc(100% - 120px)',
+});
+
export const DefaultExport = styled('span')(({ theme }) => ({
display: 'inline-block',
padding: `1px ${theme.appBorderRadius}px`,
borderRadius: '2px',
fontSize: '10px',
- color: theme.base === 'dark' ? theme.color.lightest : '#727272',
+ color: theme.color.defaultText,
backgroundColor: theme.base === 'dark' ? 'rgba(255, 255, 255, 0.1)' : '#F2F4F5',
}));
@@ -211,5 +214,5 @@ export const NoResults = styled('div')(({ theme }) => ({
export const NoResultsDescription = styled('p')(({ theme }) => ({
margin: 0,
- color: theme.base === 'dark' ? theme.color.defaultText : theme.color.mediumdark,
+ color: theme.textMutedColor,
}));
diff --git a/code/core/src/manager/components/sidebar/FileSearchList.stories.tsx b/code/core/src/manager/components/sidebar/FileSearchList.stories.tsx
index e7c983aadef6..c74655ebb69c 100644
--- a/code/core/src/manager/components/sidebar/FileSearchList.stories.tsx
+++ b/code/core/src/manager/components/sidebar/FileSearchList.stories.tsx
@@ -1,6 +1,6 @@
import type { Meta, StoryObj } from '@storybook/react-vite';
-import { expect, findByText, fireEvent, fn } from 'storybook/test';
+import { expect, fireEvent, fn, within } from 'storybook/test';
import { FileSearchList } from './FileSearchList';
@@ -39,12 +39,11 @@ export const Empty: Story = {
export const WithResults: Story = {
play: async ({ canvasElement, args }) => {
- // use react testing library
- // select first item in the list and click on it
- const firstItem = await findByText(canvasElement, 'module-multiple-exports.js');
+ const canvas = within(canvasElement);
+ const firstItem = await canvas.findByText('module-multiple-exports.js', {}, { timeout: 3000 });
fireEvent.click(firstItem);
- const exportedElement1 = await findByText(canvasElement, 'module-multiple-exports');
+ const exportedElement1 = await canvas.findByText('module-multiple-exports');
fireEvent.click(exportedElement1);
expect(args.onNewStory).toHaveBeenCalledWith(
@@ -56,7 +55,7 @@ export const WithResults: Story = {
})
);
- const exportedElement2 = await findByText(canvasElement, 'namedExport');
+ const exportedElement2 = await canvas.findByText('namedExport');
fireEvent.click(exportedElement2);
expect(args.onNewStory).toHaveBeenCalledWith(
@@ -68,7 +67,7 @@ export const WithResults: Story = {
})
);
- const singleExport = await findByText(canvasElement, 'module-single-export.js');
+ const singleExport = await canvas.findByText('module-single-export.js');
fireEvent.click(singleExport);
expect(args.onNewStory).toHaveBeenCalledWith(
@@ -82,12 +81,12 @@ export const WithResults: Story = {
expect(args.onNewStory).toHaveBeenCalledTimes(3);
- const noExportsModule1 = await findByText(canvasElement, 'no-exports-module.js');
+ const noExportsModule1 = await canvas.findByText('no-exports-module.js');
fireEvent.click(noExportsModule1);
expect(args.onNewStory).toHaveBeenCalledTimes(3);
- const noExportsModule2 = await findByText(canvasElement, 'no-exports-module-1.js');
+ const noExportsModule2 = await canvas.findByText('no-exports-module-1.js');
fireEvent.click(noExportsModule2);
expect(args.onNewStory).toHaveBeenCalledTimes(3);
diff --git a/code/core/src/manager/components/sidebar/FileSearchList.tsx b/code/core/src/manager/components/sidebar/FileSearchList.tsx
index 15593113ad2f..7a47e6c9a02b 100644
--- a/code/core/src/manager/components/sidebar/FileSearchList.tsx
+++ b/code/core/src/manager/components/sidebar/FileSearchList.tsx
@@ -1,12 +1,17 @@
import React, { memo, useCallback, useMemo, useState } from 'react';
-import { TooltipNote, WithTooltip } from 'storybook/internal/components';
+import { TooltipNote, TooltipProvider } from 'storybook/internal/components';
import type {
CreateNewStoryRequestPayload,
FileComponentSearchResponsePayload,
} from 'storybook/internal/core-events';
-import { ChevronDownIcon, ChevronRightIcon, ComponentIcon } from '@storybook/icons';
+import {
+ BookmarkHollowIcon,
+ ChevronSmallDownIcon,
+ ChevronSmallRightIcon,
+ ComponentIcon,
+} from '@storybook/icons';
import type { VirtualItem } from '@tanstack/react-virtual';
import { useVirtualizer } from '@tanstack/react-virtual';
@@ -24,6 +29,7 @@ import {
FileListItemExport,
FileListItemExportName,
FileListItemExportNameContent,
+ FileListItemExportNameContentWithExport,
FileListItemLabel,
FileListItemPath,
FileListLi,
@@ -40,16 +46,14 @@ export interface NewStoryPayload extends CreateNewStoryRequestPayload {
selectedItemId: string | number;
}
-const ChevronRightIconStyled = styled(ChevronRightIcon)(({ theme }) => ({
- display: 'none',
- alignSelf: 'center',
- color: theme.color.mediumdark,
+const TreeExpandIconStyled = styled(ChevronSmallRightIcon)(({ theme }) => ({
+ color: theme.textMutedColor,
+ marginTop: 2,
}));
-const ChevronDownIconStyled = styled(ChevronDownIcon)(({ theme }) => ({
- display: 'none',
- alignSelf: 'center',
- color: theme.color.mediumdark,
+const TreeCollapseIconStyled = styled(ChevronSmallDownIcon)(({ theme }) => ({
+ color: theme.textMutedColor,
+ marginTop: 2,
}));
interface FileSearchListProps {
@@ -170,9 +174,17 @@ export const FileSearchList = memo(function FileSearchList({
);
const ListItem = useCallback(
- ({ virtualItem, selected, searchResult }: FileItemContentProps) => {
+ ({
+ virtualItem,
+ selected,
+ searchResult,
+ noExports,
+ }: FileItemContentProps & { noExports: boolean }) => {
const itemError = errorItemId === searchResult.filepath;
const itemSelected = selected === virtualItem.index;
+ const tooltip = noExports
+ ? "We can't evaluate exports for this file. Automatic story creation is disabled."
+ : undefined;
return (
- : undefined}
+ placement="top-start"
+ delayHide={100}
+ delayShow={200}
>
-
-
-
-
-
- {searchResult.filepath.split('/').at(-1)}
-
- {searchResult.filepath}
-
- {itemSelected ? : }
-
+
+ {itemSelected ? (
+
+ ) : (
+
+ )}
+
+
+
+
+
+ {searchResult.filepath.split('/').at(-1)}
+
+ {searchResult.filepath}
+
+
+
{/* @ts-expect-error (non strict) */}
{searchResult?.exportedComponents?.length > 1 && itemSelected && (
-
+
{component.default ? (
<>
-
+
{searchResult.filepath.split('/').at(-1)?.split('.')?.at(0)}
-
+
Default export
>
) : (
- component.name
+
+ {component.name}
+
)}
-
);
})}
@@ -336,38 +357,14 @@ export const FileSearchList = memo(function FileSearchList({
}}
tabIndex={0}
>
- {noExports ? (
-
- }
- >
-
-
- ) : (
-
- )}
+
);
})}
diff --git a/code/core/src/manager/components/sidebar/FileSearchModal.stories.tsx b/code/core/src/manager/components/sidebar/FileSearchModal.stories.tsx
index 1265865992ba..47b0e75a291a 100644
--- a/code/core/src/manager/components/sidebar/FileSearchModal.stories.tsx
+++ b/code/core/src/manager/components/sidebar/FileSearchModal.stories.tsx
@@ -1,8 +1,10 @@
-import React, { useState } from 'react';
+import React from 'react';
+
+import { ModalDecorator } from 'storybook/internal/components';
import type { Meta, StoryObj } from '@storybook/react-vite';
-import { expect, findByText, fireEvent, fn } from 'storybook/test';
+import { expect, fireEvent, fn, within } from 'storybook/test';
import { WithResults } from './FileSearchList.stories';
import { FileSearchModal } from './FileSearchModal';
@@ -20,26 +22,10 @@ const meta = {
parameters: {
layout: 'fullscreen',
},
- // This decorator is used to show the modal in the side by side view
- decorators: [
- (Story, context) => {
- const [container, setContainer] = useState(undefined);
-
- return (
- setContainer(element ?? undefined)}
- style={{
- width: '100%',
- height: '100%',
- minHeight: '600px',
- transform: 'translateZ(0)',
- }}
- >
-
-
- );
- },
- ],
+ decorators: [ModalDecorator],
+ globals: {
+ sb_theme: 'dark',
+ },
} satisfies Meta;
export default meta;
@@ -95,9 +81,13 @@ export const WithSearchResults: Story = {
searchResults: WithResults.args.searchResults,
},
play: async ({ canvasElement, args }) => {
- const parent = canvasElement.parentNode as HTMLElement;
+ const parent = within(canvasElement.parentNode as HTMLElement);
- const moduleSingleExport = await findByText(parent, 'module-single-export.js');
+ const moduleSingleExport = await parent.findByText(
+ 'module-single-export.js',
+ {},
+ { timeout: 3000 }
+ );
await fireEvent.click(moduleSingleExport);
await expect(args.onCreateNewStory).toHaveBeenCalledWith({
diff --git a/code/core/src/manager/components/sidebar/FileSearchModal.tsx b/code/core/src/manager/components/sidebar/FileSearchModal.tsx
index 8b84b2f41202..12bdf945b15e 100644
--- a/code/core/src/manager/components/sidebar/FileSearchModal.tsx
+++ b/code/core/src/manager/components/sidebar/FileSearchModal.tsx
@@ -30,7 +30,7 @@ const ModalChild = styled.div<{ height?: number }>(({ theme, height }) => ({
const ModalContent = styled(Modal.Content)(({ theme }) => ({
margin: 0,
- color: theme.base === 'dark' ? theme.color.lighter : theme.color.mediumdark,
+ color: theme.color.defaultText,
}));
const ModalInput = styled(Form.Input)(({ theme }) => ({
@@ -126,7 +126,6 @@ interface FileSearchModalProps {
searchResults: SearchResult[] | null;
onCreateNewStory: (payload: NewStoryPayload) => void;
setError: (error: Error) => void;
- container?: HTMLElement;
}
export const FileSearchModal = ({
@@ -139,7 +138,6 @@ export const FileSearchModal = ({
searchResults,
onCreateNewStory,
setError,
- container,
}: FileSearchModalProps) => {
const [modalContentRef, modalContentDimensions] = useMeasure();
// @ts-expect-error (non strict)
@@ -159,17 +157,11 @@ export const FileSearchModal = ({
return (
{
- onOpenChange(false);
- }}
- onInteractOutside={() => {
- onOpenChange(false);
- }}
- container={container}
>
{/* @ts-expect-error (non strict) */}
diff --git a/code/core/src/manager/components/sidebar/FilterToggle.stories.ts b/code/core/src/manager/components/sidebar/FilterToggle.stories.ts
deleted file mode 100644
index f7528c87afee..000000000000
--- a/code/core/src/manager/components/sidebar/FilterToggle.stories.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { fn } from 'storybook/test';
-
-import { FilterToggle } from './FilterToggle';
-
-export default {
- component: FilterToggle,
- title: 'Sidebar/FilterToggle',
- args: {
- active: false,
- onClick: fn(),
- },
-};
-
-export const Errors = {
- args: {
- count: 2,
- label: 'Error',
- status: 'critical',
- },
-};
-
-export const ErrorsActive = {
- args: {
- ...Errors.args,
- active: true,
- },
-};
-
-export const Warning = {
- args: {
- count: 12,
- label: 'Warning',
- status: 'warning',
- },
-};
-
-export const WarningActive = {
- args: {
- ...Warning.args,
- active: true,
- },
-};
diff --git a/code/core/src/manager/components/sidebar/FilterToggle.tsx b/code/core/src/manager/components/sidebar/FilterToggle.tsx
deleted file mode 100644
index c26e047f774f..000000000000
--- a/code/core/src/manager/components/sidebar/FilterToggle.tsx
+++ /dev/null
@@ -1,61 +0,0 @@
-import React, { type ComponentProps } from 'react';
-
-import { Badge as BaseBadge, IconButton } from 'storybook/internal/components';
-
-import { css, styled } from 'storybook/theming';
-
-const Badge = styled(BaseBadge)(({ theme }) => ({
- padding: '4px 8px',
- fontSize: theme.typography.size.s1,
-}));
-
-const Button = styled(IconButton)(
- ({ theme }) => ({
- fontSize: theme.typography.size.s2,
- '&:hover [data-badge][data-status=warning], [data-badge=true][data-status=warning]': {
- background: '#E3F3FF',
- borderColor: 'rgba(2, 113, 182, 0.1)',
- color: '#0271B6',
- },
- '&:hover [data-badge][data-status=critical], [data-badge=true][data-status=critical]': {
- background: theme.background.negative,
- boxShadow: `inset 0 0 0 1px rgba(182, 2, 2, 0.1)`,
- color: theme.color.negativeText,
- },
- }),
- ({ active, theme }) =>
- !active &&
- css({
- '&:hover': {
- color: theme.base === 'light' ? theme.color.defaultText : theme.color.light,
- },
- })
-);
-
-const Label = styled.span(({ theme }) => ({
- color: theme.base === 'light' ? theme.color.defaultText : theme.color.light,
-}));
-
-interface FilterToggleProps {
- active: boolean;
- count: number;
- label: string;
- status: ComponentProps['status'];
-}
-
-export const FilterToggle = ({
- active,
- count,
- label,
- status,
- ...props
-}: FilterToggleProps & Omit, 'status'>) => {
- return (
-
-
- {count}
-
- {`${label}${count === 1 ? '' : 's'}`}
-
- );
-};
diff --git a/code/core/src/manager/components/sidebar/Heading.tsx b/code/core/src/manager/components/sidebar/Heading.tsx
index b70c5682a054..423a2378b865 100644
--- a/code/core/src/manager/components/sidebar/Heading.tsx
+++ b/code/core/src/manager/components/sidebar/Heading.tsx
@@ -53,7 +53,6 @@ const SkipToCanvasLink = styled(Button)(({ theme }) => ({
display: 'block',
position: 'absolute',
fontSize: theme.typography.size.s1,
- zIndex: 3,
border: 0,
width: 1,
height: 1,
@@ -73,6 +72,7 @@ const SkipToCanvasLink = styled(Button)(({ theme }) => ({
clip: 'unset',
overflow: 'unset',
opacity: 1,
+ zIndex: 3,
},
},
}));
@@ -88,7 +88,7 @@ export const Heading: FC> =
return (
{skipLinkHref && (
-
+
Skip to canvas
diff --git a/code/core/src/manager/components/sidebar/Menu.stories.tsx b/code/core/src/manager/components/sidebar/Menu.stories.tsx
index 600e3afd8366..7dcd68ddfec7 100644
--- a/code/core/src/manager/components/sidebar/Menu.stories.tsx
+++ b/code/core/src/manager/components/sidebar/Menu.stories.tsx
@@ -7,7 +7,7 @@ import { LinkIcon } from '@storybook/icons';
import type { Meta, StoryObj } from '@storybook/react-vite';
import type { State } from 'storybook/manager-api';
-import { expect, screen, userEvent, within } from 'storybook/test';
+import { expect, screen, userEvent, waitFor, within } from 'storybook/test';
import { styled } from 'storybook/theming';
import { useMenu } from '../../container/Menu';
@@ -53,7 +53,7 @@ const DoubleThemeRenderingHack = styled.div({
});
export const Expanded: Story = {
- globals: { sb_theme: 'light' },
+ globals: { sb_theme: 'light', viewport: 'desktop' },
render: () => {
const menu = useMenu(
{ whatsNewData: undefined } as State,
@@ -77,15 +77,23 @@ export const Expanded: Story = {
);
},
- play: async ({ canvasElement }) => {
+ play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement);
- await new Promise((res) => {
- setTimeout(res, 500);
+ await step('Wait 3 seconds for story to load', async () => {
+ await new Promise((res) => {
+ setTimeout(res, 3000);
+ });
+ });
+
+ await step('Expand menu', async () => {
+ const menuButton = await canvas.findByRole('switch');
+ await userEvent.click(menuButton);
+ });
+
+ await step('Check menu is open', async () => {
+ const aboutStorybookBtn = await screen.findByText(/About your Storybook/);
+ await expect(aboutStorybookBtn).toBeInTheDocument();
});
- const menuButton = await canvas.findByRole('button');
- await userEvent.click(menuButton);
- const aboutStorybookBtn = await screen.findByText(/About your Storybook/);
- await expect(aboutStorybookBtn).toBeInTheDocument();
},
decorators: [
(StoryFn) => (
@@ -137,12 +145,15 @@ export const ExpandedWithShortcuts: Story = {
},
play: async (context) => {
const canvas = within(context.canvasElement);
+ // This story can have significant loading time.
await new Promise((res) => {
- setTimeout(res, 500);
+ setTimeout(res, 2000);
});
- // @ts-expect-error (non strict)
- await Expanded.play(context);
- const releaseNotes = await canvas.queryByText(/What's new/);
+ const menuButton = await waitFor(() => canvas.findByRole('switch'));
+ await userEvent.click(menuButton);
+ const aboutStorybookBtn = await screen.findByText(/About your Storybook/);
+ await expect(aboutStorybookBtn).toBeInTheDocument();
+ const releaseNotes = canvas.queryByText(/What's new/);
await expect(releaseNotes).not.toBeInTheDocument();
},
};
diff --git a/code/core/src/manager/components/sidebar/Menu.tsx b/code/core/src/manager/components/sidebar/Menu.tsx
index f83361a35cfd..4d03a6a6a48c 100644
--- a/code/core/src/manager/components/sidebar/Menu.tsx
+++ b/code/core/src/manager/components/sidebar/Menu.tsx
@@ -1,59 +1,82 @@
import type { ComponentProps, FC } from 'react';
import React, { useState } from 'react';
-import { IconButton, TooltipLinkList, WithTooltip } from 'storybook/internal/components';
-import type { Button } from 'storybook/internal/components';
+import {
+ Button,
+ PopoverProvider,
+ ToggleButton,
+ TooltipLinkList,
+} from 'storybook/internal/components';
import { CloseIcon, CogIcon } from '@storybook/icons';
import { transparentize } from 'polished';
-import { styled } from 'storybook/theming';
+import { type Theme, css, styled } from 'storybook/theming';
import type { useMenu } from '../../container/Menu';
import { useLayout } from '../layout/LayoutProvider';
export type MenuList = ReturnType;
-export const SidebarIconButton = styled(IconButton)<
+const buttonStyleAdditions = ({
+ highlighted,
+ isMobile,
+ theme,
+}: {
+ highlighted: boolean;
+ isMobile: boolean;
+ theme: Theme;
+}) => css`
+ position: relative;
+ overflow: visible;
+ margin-top: 0;
+ z-index: 1;
+ ${isMobile &&
+ `
+ width: 36px;
+ height: 36px;
+ `}
+ ${highlighted &&
+ `
+ &:before,
+ &:after {
+ content: '';
+ position: absolute;
+ top: 6px;
+ right: 6px;
+ width: 5px;
+ height: 5px;
+ z-index: 2;
+ border-radius: 50%;
+ background: ${theme.background.app};
+ border: 1px solid ${theme.background.app};
+ box-shadow: 0 0 0 2px ${theme.background.app};
+ }
+ &:after {
+ background: ${theme.color.positive};
+ border: 1px solid rgba(0, 0, 0, 0.1);
+ box-shadow: 0 0 0 2px ${theme.background.app};
+ }
+ &:hover:after,
+ &:focus-visible:after {
+ box-shadow: 0 0 0 2px ${transparentize(0.88, theme.color.secondary)};
+ }
+ `}
+`;
+
+export const SidebarButton = styled(Button)<
ComponentProps & {
highlighted: boolean;
isMobile: boolean;
}
->(({ highlighted, theme, isMobile }) => ({
- position: 'relative',
- overflow: 'visible',
- marginTop: 0,
- zIndex: 1,
- ...(isMobile && {
- width: 36,
- height: 36,
- }),
-
- ...(highlighted && {
- '&:before, &:after': {
- content: '""',
- position: 'absolute',
- top: 6,
- right: 6,
- width: 5,
- height: 5,
- zIndex: 2,
- borderRadius: '50%',
- background: theme.background.app,
- border: `1px solid ${theme.background.app}`,
- boxShadow: `0 0 0 2px ${theme.background.app}`,
- },
- '&:after': {
- background: theme.color.positive,
- border: `1px solid rgba(0, 0, 0, 0.1)`,
- boxShadow: `0 0 0 2px ${theme.background.app}`,
- },
+>(buttonStyleAdditions);
- '&:hover:after, &:focus-visible:after': {
- boxShadow: `0 0 0 2px ${transparentize(0.88, theme.color.secondary)}`,
- },
- }),
-}));
+export const SidebarToggleButton = styled(ToggleButton)<
+ ComponentProps & {
+ highlighted: boolean;
+ isMobile: boolean;
+ }
+>(buttonStyleAdditions);
const MenuButtonGroup = styled.div({
display: 'flex',
@@ -80,48 +103,49 @@ export const SidebarMenu: FC = ({ menu, isHighlighted, onClick
if (isMobile) {
return (
-
-
-
+ setMobileMenuOpen(false)}
isMobile={true}
>
-
+
);
}
return (
- }
+ }
onVisibleChange={setIsTooltipVisible}
>
-
-
-
+
+
);
};
diff --git a/code/core/src/manager/components/sidebar/RefBlocks.tsx b/code/core/src/manager/components/sidebar/RefBlocks.tsx
index fd2c54a2f8b8..4dc9c9344f21 100644
--- a/code/core/src/manager/components/sidebar/RefBlocks.tsx
+++ b/code/core/src/manager/components/sidebar/RefBlocks.tsx
@@ -2,13 +2,20 @@ import type { FC } from 'react';
import React, { Fragment, useCallback, useState } from 'react';
import { logger } from 'storybook/internal/client-logger';
-import { Button, ErrorFormatter, Link, Spaced, WithTooltip } from 'storybook/internal/components';
+import {
+ Button,
+ ErrorFormatter,
+ Link,
+ PopoverProvider,
+ Spaced,
+} from 'storybook/internal/components';
import { global } from '@storybook/global';
import { ChevronDownIcon, LockIcon, SyncIcon } from '@storybook/icons';
import { styled } from 'storybook/theming';
+import { useLayout } from '../layout/LayoutProvider';
import { Contained, Loader } from './Loader';
import { NoResults } from './NoResults';
@@ -35,14 +42,22 @@ const Text = styled.div(({ theme }) => ({
},
}));
-const ErrorDisplay = styled.pre(
+const ErrorDisplay = styled.pre<{ isMobile: boolean }>(
{
- width: 420,
boxSizing: 'border-box',
borderRadius: 8,
overflow: 'auto',
whiteSpace: 'pre',
},
+ ({ isMobile }) =>
+ isMobile
+ ? {
+ maxWidth: 'calc(100vw - 40px)',
+ }
+ : {
+ minWidth: 420,
+ maxWidth: 640,
+ },
({ theme }) => ({
color: theme.color.dark,
})
@@ -81,7 +96,7 @@ export const AuthBlock: FC<{ loginUrl: string; id: string }> = ({ loginUrl, id }
this Storybook.
-
+
Refresh now
@@ -104,30 +119,36 @@ export const AuthBlock: FC<{ loginUrl: string; id: string }> = ({ loginUrl, id }
);
};
-export const ErrorBlock: FC<{ error: Error }> = ({ error }) => (
-
-
-
- Oh no! Something went wrong loading this Storybook.
-
-
-
-
- }
- >
-
- View error
+export const ErrorBlock: FC<{ error: Error }> = ({ error }) => {
+ const { isMobile } = useLayout();
+ return (
+
+
+
+ Oh no! Something went wrong loading this Storybook.
+
+
+
+
+ }
+ >
+
+ View error
+
+ {' '}
+
+ View docs
- {' '}
-
- View docs
-
-
-
-
-);
+
+
+
+ );
+};
const FlexSpaced = styled(Spaced)({
display: 'flex',
diff --git a/code/core/src/manager/components/sidebar/RefIndicator.tsx b/code/core/src/manager/components/sidebar/RefIndicator.tsx
index c7259181b29d..6292b475622d 100644
--- a/code/core/src/manager/components/sidebar/RefIndicator.tsx
+++ b/code/core/src/manager/components/sidebar/RefIndicator.tsx
@@ -1,14 +1,11 @@
-import type { FC, MouseEventHandler } from 'react';
+import type { FC, MouseEventHandler, ReactNode } from 'react';
import React, { forwardRef, useCallback, useMemo } from 'react';
-import type { TooltipLinkListLink } from 'storybook/internal/components';
-import { Spaced, TooltipLinkList, WithTooltip } from 'storybook/internal/components';
+import { Button, PopoverProvider, Select, Spaced } from 'storybook/internal/components';
import { global } from '@storybook/global';
import {
AlertIcon,
- CheckIcon,
- ChevronDownIcon,
DocumentIcon,
GlobeIcon,
LightningIcon,
@@ -17,12 +14,12 @@ import {
TimeIcon,
} from '@storybook/icons';
-import { transparentize } from 'polished';
import { useStorybookApi } from 'storybook/manager-api';
import { styled, useTheme } from 'storybook/theming';
import type { NormalLink } from '../../../components/components/tooltip/TooltipLinkList';
import type { getStateType } from '../../utils/tree';
+import { useLayout } from '../layout/LayoutProvider';
import type { RefType } from './types';
const { document, window: globalWindow } = global;
@@ -47,34 +44,12 @@ const IndicatorPlacement = styled.aside(({ theme }) => ({
},
}));
-const IndicatorClickTarget = styled.button(({ theme }) => ({
- height: 20,
- width: 20,
- padding: 0,
- margin: 0,
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- background: 'transparent',
- outline: 'none',
- border: '1px solid transparent',
- borderRadius: '100%',
- cursor: 'pointer',
- color:
- theme.base === 'light'
- ? transparentize(0.3, theme.color.defaultText)
- : transparentize(0.6, theme.color.defaultText),
-
- '&:hover': {
- color: theme.barSelectedColor,
- },
- '&:focus': {
- color: theme.barSelectedColor,
- borderColor: theme.color.secondary,
- },
+const IndicatorClickTarget = styled(Button)(({ theme }) => ({
+ color: theme.textMutedColor,
svg: {
- height: 10,
- width: 10,
+ height: 14,
+ width: 14,
+ padding: 2,
transition: 'all 150ms ease-out',
color: 'inherit',
},
@@ -84,7 +59,7 @@ const MessageTitle = styled.span(({ theme }) => ({
fontWeight: theme.typography.weight.bold,
}));
-const Message = styled.a(({ theme }) => ({
+const StyledMessage = styled.a(({ theme }) => ({
textDecoration: 'none',
lineHeight: '16px',
padding: 15,
@@ -92,21 +67,22 @@ const Message = styled.a(({ theme }) => ({
flexDirection: 'row',
alignItems: 'flex-start',
color: theme.color.defaultText,
+
'&:not(:last-child)': {
borderBottom: `1px solid ${theme.appBorderColor}`,
},
'&:hover': {
background: theme.background.hoverable,
- color: theme.color.darker,
- },
- '&:link': {
- color: theme.color.darker,
+ color: theme.color.defaultText,
},
- '&:active': {
- color: theme.color.darker,
+ '&:link, &:active, &:focus': {
+ color: theme.color.defaultText,
},
- '&:focus': {
- color: theme.color.darker,
+ '&:focus-visible': {
+ background: theme.background.hoverable,
+ borderRadius: 8,
+ boxShadow: `inset 0 0 0 2px ${theme.color.secondary}`,
+ outline: 'none',
},
'& > *': {
flex: 1,
@@ -120,52 +96,45 @@ const Message = styled.a(({ theme }) => ({
},
}));
-export const MessageWrapper = styled.div({
- width: 280,
- boxSizing: 'border-box',
- borderRadius: 8,
- overflow: 'hidden',
-});
+const Message: FC<{
+ blank?: boolean;
+ children: ReactNode;
+ href?: string;
+ onClick?: MouseEventHandler;
+}> = ({ href, blank = true, children, onClick }) => {
+ return (
+
+ {children}
+
+ );
+};
-const Version = styled.div(({ theme }) => ({
- display: 'flex',
- alignItems: 'center',
+export const MessageWrapper = styled.div<{
+ isMobile: boolean;
+}>(
+ ({ isMobile }) => ({
+ width: isMobile ? 'calc(100vw - 20px)' : 280,
+ boxSizing: 'border-box',
+ borderRadius: 8,
+ overflow: 'hidden',
+ }),
+ ({ theme }) => ({
+ color: theme.color.dark,
+ })
+);
+
+const SubtleSelect = styled(Select)(({ theme }) => ({
+ background: 'transparent',
+ color: theme.color.defaultText,
fontSize: theme.typography.size.s1,
fontWeight: theme.typography.weight.regular,
- color:
- theme.base === 'light'
- ? transparentize(0.3, theme.color.defaultText)
- : transparentize(0.6, theme.color.defaultText),
-
- '& > * + *': {
- marginLeft: 4,
- },
-
- svg: {
- height: 10,
- width: 10,
- },
}));
-const CurrentVersion: FC = ({ url, versions }) => {
- const currentVersionId = useMemo(() => {
- // @ts-expect-error (non strict)
- const c = Object.entries(versions).find(([k, v]) => v === url);
- return c && c[0] ? c[0] : 'current';
- }, [url, versions]);
-
- return (
-
- {currentVersionId}
-
-
- );
-};
-
export const RefIndicator = React.memo(
forwardRef }>(
({ state, ...ref }, forwardedRef) => {
const api = useStorybookApi();
+ const { isMobile } = useLayout();
const list = useMemo(() => Object.values(ref.index || {}), [ref.index]);
const componentCount = useMemo(
() => list.filter((v) => v.type === 'component').length,
@@ -176,14 +145,20 @@ export const RefIndicator = React.memo(
[list]
);
+ const currentVersion = useMemo(() => {
+ if (ref.versions) {
+ return Object.entries(ref.versions).find(([, v]) => v === ref.url)?.[0];
+ }
+ return undefined;
+ }, [ref.versions, ref.url]);
+
return (
-
+ (
+
{state === 'loading' && }
{(state === 'error' || state === 'empty') && (
@@ -202,38 +177,42 @@ export const RefIndicator = React.memo(
{state !== 'loading' && }
- }
+ )}
>
-
+
-
+
{ref.versions && Object.keys(ref.versions).length ? (
- (
- ({
- icon: href === ref.url ? : undefined,
- id,
- title: id,
- href,
- onClick: (event, item) => {
- event.preventDefault();
- // @ts-expect-error (non strict)
- api.changeRefVersion(ref.id, item.href);
- tooltip.onHide();
- },
- }))}
- />
- )}
- >
-
-
+ <>
+ {
+ const href = ref.versions?.[item];
+ if (href) {
+ api.changeRefVersion(ref.id, href);
+ }
+ }}
+ options={Object.entries(ref.versions).map(([id, href]) => ({
+ value: id,
+ title: id,
+ href,
+ }))}
+ >
+ version
+
+ >
) : null}
);
@@ -249,7 +228,7 @@ const ReadyMessage: FC<{
const theme = useTheme();
return (
-
+
View external Storybook
@@ -267,7 +246,7 @@ const SourceCodeMessage: FC<{
const theme = useTheme();
return (
-
+
View source code
@@ -278,23 +257,30 @@ const SourceCodeMessage: FC<{
const LoginRequiredMessage: FC
= ({ loginUrl, id }) => {
const theme = useTheme();
- const open = useCallback((e) => {
- e.preventDefault();
- const childWindow = globalWindow.open(loginUrl, `storybook_auth_${id}`, 'resizable,scrollbars');
-
- // poll for window to close
- const timer = setInterval(() => {
- if (!childWindow) {
- clearInterval(timer);
- } else if (childWindow.closed) {
- clearInterval(timer);
- document.location.reload();
- }
- }, 1000);
- }, []);
+ const open = useCallback(
+ (e) => {
+ e.preventDefault();
+ const childWindow = globalWindow.open(
+ loginUrl,
+ `storybook_auth_${id}`,
+ 'resizable,scrollbars'
+ );
+
+ // poll for window to close
+ const timer = setInterval(() => {
+ if (!childWindow) {
+ clearInterval(timer);
+ } else if (childWindow.closed) {
+ clearInterval(timer);
+ document.location.reload();
+ }
+ }, 1000);
+ },
+ [id, loginUrl]
+ );
return (
-
+
Log in required
@@ -308,10 +294,7 @@ const ReadDocsMessage: FC = () => {
const theme = useTheme();
return (
-
+
Read Composition docs
@@ -325,7 +308,7 @@ const ErrorOccurredMessage: FC<{ url: string }> = ({ url }) => {
const theme = useTheme();
return (
-
+
Something went wrong
@@ -339,7 +322,7 @@ const LoadingMessage: FC<{ url: string }> = ({ url }) => {
const theme = useTheme();
return (
-
+
Please wait
@@ -354,10 +337,7 @@ const PerformanceDegradedMessage: FC = () => {
const theme = useTheme();
return (
-
+
Reduce lag
diff --git a/code/core/src/manager/components/sidebar/Refs.stories.tsx b/code/core/src/manager/components/sidebar/Refs.stories.tsx
index 19598ee80aa5..f2f47012289b 100644
--- a/code/core/src/manager/components/sidebar/Refs.stories.tsx
+++ b/code/core/src/manager/components/sidebar/Refs.stories.tsx
@@ -1,7 +1,9 @@
import React from 'react';
+import type { StoryAnnotations } from 'storybook/internal/csf';
+
import { ManagerContext } from 'storybook/manager-api';
-import { fn } from 'storybook/test';
+import { fn, userEvent, within } from 'storybook/test';
import { standardData as standardHeaderData } from './Heading.stories';
import { IconSymbols } from './IconSymbols';
@@ -268,6 +270,52 @@ export const Errored = () => (
setHighlighted={() => {}}
/>
);
+export const ErroredMobile = () => (
+
[ {}}
+ />
+);
+ErroredMobile.globals = { sb_theme: 'stacked', viewport: { value: 'mobile1' } };
+export const ErroredWithErrorOpen: StoryAnnotations = {
+ render: () => Errored(),
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const button = await canvas.findByText('View error');
+ await userEvent.click(button);
+ },
+};
+export const ErroredMobileWithErrorOpen: StoryAnnotations = {
+ render: () => ErroredMobile(),
+ globals: { sb_theme: 'stacked', viewport: { value: 'mobile1' } },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const button = await canvas.findByText('View error');
+ await userEvent.click(button);
+ },
+};
+export const ErroredWithIndicatorOpen: StoryAnnotations = {
+ render: () => Errored(),
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const button = await canvas.findByRole('button', { name: 'Extra actions' });
+ await userEvent.click(button);
+ },
+};
+export const ErroredMobileWithIndicatorOpen: StoryAnnotations = {
+ render: () => ErroredMobile(),
+ globals: { sb_theme: 'stacked', viewport: { value: 'mobile1' } },
+ play: async ({ canvasElement }) => {
+ const canvas = within(canvasElement);
+ const button = await canvas.findByRole('button', { name: 'Extra actions' });
+ await userEvent.click(button);
+ },
+};
export const Auth = () => (
][ ({
paddingBottom: 12,
borderTop: `1px solid ${theme.appBorderColor}`,
- color:
- theme.base === 'light' ? theme.color.defaultText : transparentize(0.2, theme.color.defaultText),
+ color: theme.color.defaultText,
}));
const RefTitle = styled.div({
diff --git a/code/core/src/manager/components/sidebar/Search.tsx b/code/core/src/manager/components/sidebar/Search.tsx
index a6113815b4e6..ede9c9376755 100644
--- a/code/core/src/manager/components/sidebar/Search.tsx
+++ b/code/core/src/manager/components/sidebar/Search.tsx
@@ -1,6 +1,6 @@
import React, { type ReactNode, useCallback, useRef, useState } from 'react';
-import { IconButton } from 'storybook/internal/components';
+import { Button } from 'storybook/internal/components';
import { global } from '@storybook/global';
import { CloseIcon, SearchIcon } from '@storybook/icons';
@@ -74,8 +74,9 @@ const SearchField = styled.div<{ isMobile: boolean }>(({ theme, isMobile }) => (
borderRadius: theme.appBorderRadius + 2,
'&:has(input:focus), &:has(input:active)': {
- boxShadow: `${theme.color.secondary} 0 0 0 1px inset`,
background: theme.background.app,
+ outline: `2px solid ${theme.color.secondary}`,
+ outlineOffset: 2,
},
}));
@@ -409,10 +410,15 @@ export const Search = React.memo](function Search({
)}
- {isOpen && (
- clearSelection()}>
+ {input && (
+ clearSelection()}
+ >
-
+
)}
{searchFieldContent}
diff --git a/code/core/src/manager/components/sidebar/SearchResults.tsx b/code/core/src/manager/components/sidebar/SearchResults.tsx
index 0fdee2722640..f88398606d69 100644
--- a/code/core/src/manager/components/sidebar/SearchResults.tsx
+++ b/code/core/src/manager/components/sidebar/SearchResults.tsx
@@ -1,7 +1,7 @@
import type { FC, MouseEventHandler, PropsWithChildren, ReactNode } from 'react';
import React, { useCallback, useEffect } from 'react';
-import { Button, IconButton } from 'storybook/internal/components';
+import { Button } from 'storybook/internal/components';
import { PRELOAD_ENTRIES } from 'storybook/internal/core-events';
import { global } from '@storybook/global';
@@ -119,7 +119,7 @@ const Highlight: FC> = React.memo(function
return {result} ;
});
-const Title = styled.div(({ theme }) => ({
+const Title = styled.div({
display: 'grid',
justifyContent: 'start',
gridAutoColumns: 'auto',
@@ -131,7 +131,7 @@ const Title = styled.div(({ theme }) => ({
overflow: 'hidden',
textOverflow: 'ellipsis',
},
-}));
+});
const Path = styled.div(({ theme }) => ({
display: 'grid',
@@ -289,12 +289,15 @@ export const SearchResults: FC<{
{results.length > 0 && !query && (
Recently opened
-
-
+
)}
{results.length === 0 && query && (
diff --git a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx
index 0644d84d5cad..274c67add214 100644
--- a/code/core/src/manager/components/sidebar/Sidebar.stories.tsx
+++ b/code/core/src/manager/components/sidebar/Sidebar.stories.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import type { StatusesByStoryIdAndTypeId } from 'storybook/internal/types';
+import type { DecoratorFunction, StatusesByStoryIdAndTypeId } from 'storybook/internal/types';
import type { Meta, StoryObj } from '@storybook/react-vite';
@@ -90,9 +90,15 @@ const meta = {
isDevelopment: true,
},
decorators: [
- (storyFn) => (
+ (storyFn, { globals, title }) => (
-
+
{storyFn()}
@@ -109,6 +115,21 @@ export default meta;
type Story = StoryObj;
+const mobileLayoutDecorator: DecoratorFunction = (storyFn, { globals, title }) => (
+
+
+
+ {storyFn()}
+
+
+);
+
const refs: Record = {
optimized: {
id: 'optimized',
@@ -149,6 +170,11 @@ export const SimpleInProduction: Story = {
},
};
+export const Mobile: Story = {
+ decorators: [mobileLayoutDecorator],
+ globals: { sb_theme: 'light', viewport: { value: 'mobile1' } },
+};
+
export const Loading: Story = {
args: {
previewInitialized: false,
@@ -156,12 +182,24 @@ export const Loading: Story = {
},
};
+export const LoadingMobile: Story = {
+ args: Loading.args,
+ decorators: [mobileLayoutDecorator],
+ globals: { sb_theme: 'light', viewport: { value: 'mobile1' } },
+};
+
export const Empty: Story = {
args: {
index: {},
},
};
+export const EmptyMobile: Story = {
+ args: Empty.args,
+ decorators: [mobileLayoutDecorator],
+ globals: { sb_theme: 'light', viewport: { value: 'mobile1' } },
+};
+
export const EmptyIndex: Story = {
args: {
index: {},
@@ -220,6 +258,12 @@ export const WithRefsNarrow: Story = {
},
};
+export const WithRefsMobile: Story = {
+ args: WithRefs.args,
+ decorators: [mobileLayoutDecorator],
+ globals: { sb_theme: 'light', viewport: { value: 'mobile1' } },
+};
+
export const LoadingWithRefs: Story = {
args: {
...Loading.args,
@@ -234,6 +278,12 @@ export const LoadingWithRefError: Story = {
},
};
+export const LoadingWithRefErrorMobile: Story = {
+ args: LoadingWithRefError.args,
+ decorators: [mobileLayoutDecorator],
+ globals: { sb_theme: 'light', viewport: { value: 'mobile1' } },
+};
+
export const WithRefEmpty: Story = {
args: {
...Empty.args,
diff --git a/code/core/src/manager/components/sidebar/Sidebar.tsx b/code/core/src/manager/components/sidebar/Sidebar.tsx
index 3306ae21c817..777e12b75328 100644
--- a/code/core/src/manager/components/sidebar/Sidebar.tsx
+++ b/code/core/src/manager/components/sidebar/Sidebar.tsx
@@ -1,12 +1,6 @@
import React, { useMemo, useState } from 'react';
-import {
- IconButton,
- ScrollArea,
- Spaced,
- TooltipNote,
- WithTooltip,
-} from 'storybook/internal/components';
+import { Button, ScrollArea, Spaced } from 'storybook/internal/components';
import type { API_LoadedRefData, StoryIndex, TagsOptions } from 'storybook/internal/types';
import type { StatusesByStoryIdAndTypeId } from 'storybook/internal/types';
@@ -57,12 +51,8 @@ const Top = styled(Spaced)({
flex: 1,
});
-const TooltipNoteWrapper = styled(TooltipNote)({
- margin: 0,
-});
-
-const CreateNewStoryButton = styled(IconButton)<{ isMobile: boolean }>(({ theme, isMobile }) => ({
- color: theme.color.mediumdark,
+const CreateNewStoryButton = styled(Button)<{ isMobile: boolean }>(({ theme, isMobile }) => ({
+ color: theme.textMutedColor,
width: isMobile ? 36 : 32,
height: isMobile ? 36 : 32,
borderRadius: theme.appBorderRadius + 2,
@@ -165,7 +155,7 @@ export const Sidebar = React.memo(function Sidebar({
return (
-
+
- }
+ {
+ setIsFileSearchModalOpen(true);
+ }}
+ ariaLabel="Create a new story"
+ variant="outline"
+ padding="small"
>
- {
- setIsFileSearchModalOpen(true);
- }}
- variant="outline"
- >
-
-
-
+
+
{
- const screen = await within(canvasElement);
+ const screen = within(canvasElement);
- const toggleButton = await screen.getByLabelText(/Expand/);
+ const toggleButton = await screen.findByLabelText(/Expand/, {}, { timeout: 3000 });
await fireEvent.click(toggleButton);
const content = await screen.findByText('CUSTOM CONTENT WITH DYNAMIC HEIGHT');
- const collapse = await screen.getByTestId('collapse');
+ const collapse = screen.getByTestId('collapse');
await expect(content).toBeVisible();
diff --git a/code/core/src/manager/components/sidebar/StatusButton.tsx b/code/core/src/manager/components/sidebar/StatusButton.tsx
index 9d10b02d902a..e2b1079cc632 100644
--- a/code/core/src/manager/components/sidebar/StatusButton.tsx
+++ b/code/core/src/manager/components/sidebar/StatusButton.tsx
@@ -1,4 +1,7 @@
-import { IconButton } from 'storybook/internal/components';
+import type { ComponentProps } from 'react';
+import React, { forwardRef } from 'react';
+
+import { Button } from 'storybook/internal/components';
import type { StatusValue } from 'storybook/internal/types';
import type { Theme } from '@emotion/react';
@@ -26,7 +29,14 @@ export const StatusLabel = styled.div<{ status: StatusValue }>(withStatusColor,
margin: 3,
});
-export const StatusButton = styled(IconButton)<{
+export interface StatusButtonProps extends ComponentProps {
+ height?: number;
+ width?: number;
+ status: StatusValue;
+ selectedItem?: boolean;
+}
+
+const StyledButton = styled(Button)<{
height?: number;
width?: number;
status: StatusValue;
@@ -50,11 +60,13 @@ export const StatusButton = styled(IconButton)<{
},
'[data-selected="true"] &': {
- background: theme.color.secondary,
- boxShadow: `0 0 5px 5px ${theme.color.secondary}`,
+ background:
+ theme.base === 'dark' ? darken(0.18, theme.color.secondary) : theme.color.secondary,
+ boxShadow: `0 0 5px 5px ${theme.base === 'dark' ? darken(0.18, theme.color.secondary) : theme.color.secondary}`,
'&:hover': {
- background: lighten(0.1, theme.color.secondary),
+ background:
+ theme.base === 'dark' ? darken(0.1, theme.color.secondary) : theme.color.secondary,
},
},
@@ -75,3 +87,8 @@ export const StatusButton = styled(IconButton)<{
},
}
);
+
+export const StatusButton = forwardRef((props, ref) => {
+ return ;
+});
+StatusButton.displayName = 'StatusButton';
diff --git a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx
index 8fa7ba7139ee..a2fc136196b2 100644
--- a/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx
+++ b/code/core/src/manager/components/sidebar/TagsFilter.stories.tsx
@@ -53,8 +53,8 @@ export const ClosedWithSelection: Story = {
export const Clear = {
...Closed,
play: async ({ canvasElement }) => {
- const button = await findByRole(canvasElement, 'button');
- await button.click();
+ const button = await findByRole(canvasElement, 'button', {}, { timeout: 3000 });
+ button.click();
},
} satisfies Story;
diff --git a/code/core/src/manager/components/sidebar/TagsFilter.tsx b/code/core/src/manager/components/sidebar/TagsFilter.tsx
index cfd094f6a704..6f7b49bfd0ba 100644
--- a/code/core/src/manager/components/sidebar/TagsFilter.tsx
+++ b/code/core/src/manager/components/sidebar/TagsFilter.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect, useMemo, useState } from 'react';
-import { Badge, IconButton, WithTooltip } from 'storybook/internal/components';
+import { Badge, Button, PopoverProvider } from 'storybook/internal/components';
import type {
API_PreparedIndexEntry,
StoryIndex,
@@ -27,6 +27,17 @@ const BUILT_IN_TAGS = new Set([
'test-fn',
]);
+// Temporary to prevent regressions until TagFilterPanel can be refactored.
+const StyledIconButton = styled(Button)<{ active: boolean }>(({ active, theme }) => ({
+ '&:focus-visible': {
+ outlineOffset: 4,
+ },
+ ...(active && {
+ background: theme.background.hoverable,
+ color: theme.color.secondary,
+ }),
+}));
+
// Immutable set operations
const add = (set: Set, id: string) => {
const copy = new Set(set);
@@ -59,8 +70,8 @@ const TagSelected = styled(Badge)(({ theme }) => ({
lineHeight: 'px',
boxShadow: `${theme.barSelectedColor} 0 0 0 1px inset`,
fontSize: theme.typography.size.s1 - 1,
- background: theme.color.secondary,
- color: theme.color.lightest,
+ background: theme.barSelectedColor,
+ color: theme.color.inverseText,
}));
export interface TagsFilterProps {
@@ -223,13 +234,12 @@ export const TagsFilter = ({ api, indexJson, isDevelopment, tagPresets }: TagsFi
}
return (
- (
+ offset={8}
+ padding={0}
+ popover={() => (
0 || defaultExcluded.size > 0}
/>
)}
- closeOnOutsideClick
>
-
-
-
-
+
+
{includedFilters.size + excludedFilters.size > 0 && }
-
-
+
+
);
};
diff --git a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx
index 1fe947674eb7..f3197f4ed85b 100644
--- a/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx
+++ b/code/core/src/manager/components/sidebar/TagsFilterPanel.tsx
@@ -3,11 +3,10 @@ import React, { useRef } from 'react';
import {
Button,
Form,
- IconButton,
ListItem,
TooltipLinkList,
TooltipNote,
- WithTooltip,
+ TooltipProvider,
} from 'storybook/internal/components';
import type { API_PreparedIndexEntry } from 'storybook/internal/types';
@@ -51,17 +50,17 @@ const Actions = styled.div(({ theme }) => ({
const TagRow = styled.div({
display: 'flex',
- '& button': {
+ '& button:last-of-type': {
width: 64,
maxWidth: 64,
marginLeft: 4,
paddingLeft: 0,
paddingRight: 0,
fontWeight: 'normal',
- transition: 'all 150ms',
+ transition: 'max-width 150ms',
},
- '&:not(:hover)': {
- '& button': {
+ '&:not(:hover):not(:focus-within)': {
+ '& button:last-of-type': {
marginLeft: 0,
maxWidth: 0,
opacity: 0,
@@ -148,15 +147,10 @@ export const TagsFilterPanel = ({
id: `filter-${type}-${id}`,
content: (
- }
- trigger="hover"
- >
+ }>
onToggle(!isChecked)}
icon={
<>
{isExcluded ? : isIncluded ? null : icon}
@@ -164,6 +158,8 @@ export const TagsFilterPanel = ({
checked={isChecked}
onChange={() => onToggle(!isChecked)}
data-tag={title}
+ aria-hidden={true}
+ tabIndex={-1}
/>
>
}
@@ -176,22 +172,15 @@ export const TagsFilterPanel = ({
}
right={isExcluded ? {count} : {count} }
/>
-
- }
- trigger="hover"
+
+ onToggle(true, !isExcluded)}
+ ariaLabel={invertButtonLabel}
>
- onToggle(true, !isExcluded)}
- aria-label={invertButtonLabel}
- >
- {isExcluded ? 'Include' : 'Exclude'}
-
-
+ {isExcluded ? 'Include' : 'Exclude'}
+
),
};
@@ -218,51 +207,51 @@ export const TagsFilterPanel = ({
]);
}
- const filtersLabel =
- includedFilters.size === 0 && excludedFilters.size === 0 ? 'Select all' : 'Clear filters';
+ const isNothingSelectedYet = includedFilters.size === 0 && excludedFilters.size === 0;
+ const filtersLabel = isNothingSelectedYet ? 'Select all' : 'Clear filters';
return (
{Object.keys(filtersById).length > 0 && (
- {includedFilters.size === 0 && excludedFilters.size === 0 ? (
- setAllFilters(true)}
>
{filtersLabel}
-
+
) : (
- setAllFilters(false)}
>
{filtersLabel}
-
+
)}
{hasDefaultSelection && (
- }
- trigger="hover"
+
-
-
-
-
+
+
)}
)}
diff --git a/code/core/src/manager/components/sidebar/TestingModule.tsx b/code/core/src/manager/components/sidebar/TestingModule.tsx
index d63a68feaacd..fef81281684f 100644
--- a/code/core/src/manager/components/sidebar/TestingModule.tsx
+++ b/code/core/src/manager/components/sidebar/TestingModule.tsx
@@ -1,8 +1,7 @@
import React, { type SyntheticEvent, useCallback, useEffect, useRef, useState } from 'react';
import { once } from 'storybook/internal/client-logger';
-import { Button, IconButton, TooltipNote } from 'storybook/internal/components';
-import { WithTooltip } from 'storybook/internal/components';
+import { Button, ToggleButton } from 'storybook/internal/components';
import type {
Addon_Collection,
Addon_TestProviderType,
@@ -40,8 +39,7 @@ const Outline = styled.div<{
padding: 1,
overflow: 'hidden',
backgroundColor: `var(--sb-sidebar-bottom-card-background, ${theme.background.content})`,
- borderRadius:
- `var(--sb-sidebar-bottom-card-border-radius, ${theme.appBorderRadius + 1}px)` as any,
+ borderRadius: `var(--sb-sidebar-bottom-card-border-radius, ${theme.appBorderRadius + 1}px)`,
boxShadow: `inset 0 0 0 1px ${crashed && !running ? theme.color.negative : updated ? theme.color.positive : theme.appBorderColor}, var(--sb-sidebar-bottom-card-box-shadow, 0 1px 2px 0 rgba(0, 0, 0, 0.05), 0px -5px 20px 10px ${theme.background.app})`,
transition: 'box-shadow 1s',
@@ -131,10 +129,10 @@ const RunButton = styled(Button)({
},
});
-const StatusButton = styled(Button)<{ status: 'negative' | 'warning' }>(
+const StatusButton = styled(ToggleButton)<{ pressed: boolean; status: 'negative' | 'warning' }>(
{ minWidth: 28 },
- ({ active, status, theme }) =>
- !active &&
+ ({ pressed, status, theme }) =>
+ !pressed &&
(theme.base === 'light'
? {
background: {
@@ -292,134 +290,104 @@ export const TestingModule = ({
toggleCollapsed(e) } : {})}>
{hasTestProviders && (
- }
- trigger="hover"
+ {
+ e.stopPropagation();
+ onRunAll();
+ }}
+ disabled={isRunning}
>
- {
- e.stopPropagation();
- onRunAll();
- }}
- disabled={isRunning}
- >
-
- {isRunning ? 'Running...' : 'Run tests'}
-
-
+
+ {isRunning ? 'Running...' : 'Run tests'}
+
)}
{hasTestProviders && (
-
- }
- trigger="hover"
+ toggleCollapsed(e)}
+ id="testing-module-collapse-toggle"
+ ariaLabel={isCollapsed ? 'Expand testing module' : 'Collapse testing module'}
>
- toggleCollapsed(e)}
- id="testing-module-collapse-toggle"
- aria-label={isCollapsed ? 'Expand testing module' : 'Collapse testing module'}
- >
-
-
-
+
+
)}
{errorCount > 0 && (
- }
- trigger="hover"
+ {
+ e.stopPropagation();
+ setErrorsActive(!errorsActive);
+ }}
+ ariaLabel={`Filter main navigation to show ${errorCount} tests with errors`}
+ tooltip={
+ errorsActive
+ ? 'Clear test error filter'
+ : `Filter sidebar to show ${errorCount} tests with errors`
+ }
>
- {
- e.stopPropagation();
- setErrorsActive(!errorsActive);
- }}
- aria-label="Toggle errors"
- >
- {errorCount < 1000 ? errorCount : '999+'}
-
-
+ {errorCount < 1000 ? errorCount : '999+'}
+
)}
{warningCount > 0 && (
- }
- trigger="hover"
+ {
+ e.stopPropagation();
+ setWarningsActive(!warningsActive);
+ }}
+ ariaLabel={`Filter main navigation to show ${warningCount} tests with warnings`}
+ tooltip={
+ warningsActive
+ ? 'Clear test warning filter'
+ : `Filter sidebar to show ${warningCount} tests with warnings`
+ }
>
- {
- e.stopPropagation();
- setWarningsActive(!warningsActive);
- }}
- aria-label="Toggle warnings"
- >
- {warningCount < 1000 ? warningCount : '999+'}
-
-
+ {warningCount < 1000 ? warningCount : '999+'}
+
)}
{hasStatuses && (
-
+ {
+ e.stopPropagation();
+ clearStatuses();
+ }}
+ disabled={isRunning}
+ ariaLabel={
+ isRunning ? "Can't clear statuses while tests are running" : 'Clear all statuses'
}
- trigger="hover"
>
- {
- e.stopPropagation();
- clearStatuses();
- }}
- disabled={isRunning}
- aria-label={
- isRunning
- ? "Can't clear statuses while tests are running"
- : 'Clear all statuses'
- }
- >
-
-
-
+
+
)}
diff --git a/code/core/src/manager/components/sidebar/Tree.stories.tsx b/code/core/src/manager/components/sidebar/Tree.stories.tsx
index af4284ee3bcf..4dd919042eaa 100644
--- a/code/core/src/manager/components/sidebar/Tree.stories.tsx
+++ b/code/core/src/manager/components/sidebar/Tree.stories.tsx
@@ -10,7 +10,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import { action } from 'storybook/actions';
import { type ComponentEntry, type IndexHash, ManagerContext } from 'storybook/manager-api';
-import { expect, fn, userEvent, within } from 'storybook/test';
+import { expect, fn, screen, userEvent, within } from 'storybook/test';
import { DEFAULT_REF_ID } from './Sidebar';
import { Tree } from './Tree';
@@ -300,19 +300,16 @@ export const WithContextContent: Story = {
viewport: { value: 'desktop' },
},
play: async ({ canvasElement }) => {
- const screen = await within(canvasElement);
+ const canvas = within(canvasElement);
- const link = await screen.findByText('TooltipBuildList');
+ const link = await canvas.findByText('TooltipBuildList');
await userEvent.hover(link);
- const contextButton = await screen.findAllByTestId('context-menu');
+ const contextButton = await canvas.findAllByTestId('context-menu');
await userEvent.click(contextButton[0]);
- const body = await within(document.body);
-
- const tooltip = await body.findByTestId('tooltip');
-
- await expect(tooltip).toBeVisible();
- expect(tooltip).toHaveTextContent('TEST_PROVIDER_CONTEXT_CONTENT');
+ const popover = screen.getByRole('dialog');
+ await expect(popover).toBeVisible();
+ expect(popover).toHaveTextContent('TEST_PROVIDER_CONTEXT_CONTENT');
},
};
diff --git a/code/core/src/manager/components/sidebar/Tree.tsx b/code/core/src/manager/components/sidebar/Tree.tsx
index 96281fd811d7..997e58455d31 100644
--- a/code/core/src/manager/components/sidebar/Tree.tsx
+++ b/code/core/src/manager/components/sidebar/Tree.tsx
@@ -1,7 +1,7 @@
import type { ComponentProps, FC, MutableRefObject } from 'react';
import React, { useCallback, useMemo, useRef } from 'react';
-import { Button, IconButton, ListItem } from 'storybook/internal/components';
+import { Button, ListItem } from 'storybook/internal/components';
import { PRELOAD_ENTRIES } from 'storybook/internal/core-events';
import type { StatusValue } from 'storybook/internal/types';
import {
@@ -21,7 +21,7 @@ import {
} from '@storybook/icons';
import { internal_fullStatusStore as fullStatusStore } from '#manager-stores';
-import { darken, lighten } from 'polished';
+import { darken } from 'polished';
import { useStorybookApi } from 'storybook/manager-api';
import type {
API,
@@ -60,22 +60,14 @@ const Container = styled.div<{ hasOrphans: boolean }>((props) => ({
marginBottom: 20,
}));
-const CollapseButton = styled.button({
- all: 'unset',
- display: 'flex',
- padding: '0px 8px',
- borderRadius: 4,
- transition: 'color 150ms, box-shadow 150ms',
- gap: 6,
- alignItems: 'center',
- cursor: 'pointer',
- height: 28,
-
- '&:hover, &:focus': {
- outline: 'none',
- background: 'var(--tree-node-background-hover)',
- },
-});
+const CollapseButton = styled(Button)(({ theme }) => ({
+ fontSize: `${theme.typography.size.s1 - 1}px`,
+ fontWeight: theme.typography.weight.bold,
+ letterSpacing: '0.16em',
+ textTransform: 'uppercase',
+ color: theme.textMutedColor,
+ padding: '0 8px',
+}));
export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({
position: 'relative',
@@ -94,10 +86,7 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({
},
'&:hover, &:focus': {
- '--tree-node-background-hover':
- theme.base === 'dark'
- ? darken(0.35, theme.color.secondary)
- : lighten(0.45, theme.color.secondary),
+ '--tree-node-background-hover': theme.background.hoverable,
background: 'var(--tree-node-background-hover)',
outline: 'none',
},
@@ -120,12 +109,12 @@ export const LeafNodeStyleWrapper = styled.div(({ theme }) => ({
'&[data-selected="true"]': {
color: theme.color.lightest,
- background: theme.color.secondary,
+ background: theme.base === 'dark' ? darken(0.18, theme.color.secondary) : theme.color.secondary,
fontWeight: theme.typography.weight.bold,
'&&:hover, &&:focus': {
- '--tree-node-background-hover': theme.color.secondary,
- background: 'var(--tree-node-background-hover)',
+ background:
+ theme.base === 'dark' ? darken(0.18, theme.color.secondary) : theme.color.secondary,
},
svg: { color: theme.color.lightest },
},
@@ -309,7 +298,8 @@ const Node = React.memo(function Node(props) {
data-nodetype="root"
>
{
event.preventDefault();
@@ -321,9 +311,11 @@ const Node = React.memo(function Node(props) {
{item.renderLabel?.(item, api) || item.name}
{isExpanded && (
- {
@@ -333,7 +325,7 @@ const Node = React.memo(function Node(props) {
}}
>
{isFullyExpanded ? : }
-
+
)}
);
@@ -343,7 +335,8 @@ const Node = React.memo(function Node(props) {
const [itemIcon, itemColor] = statusMapping[itemStatus];
const itemStatusButton = itemIcon ? (
(function Node(props) {
item.name}
{isSelected && (
-
+
Skip to canvas
)}
{contextMenu.node}
{showBranchStatus ? (
-
+
@@ -466,7 +465,7 @@ const Node = React.memo(function Node(props) {
item.name}
{isSelected && (
-
+
Skip to canvas
)}
diff --git a/code/core/src/manager/components/sidebar/TreeNode.tsx b/code/core/src/manager/components/sidebar/TreeNode.tsx
index f86c0f00fd00..ac9465eb4a72 100644
--- a/code/core/src/manager/components/sidebar/TreeNode.tsx
+++ b/code/core/src/manager/components/sidebar/TreeNode.tsx
@@ -80,13 +80,6 @@ export const RootNode = styled.div(({ theme }) => ({
justifyContent: 'space-between',
marginTop: 16,
marginBottom: 4,
- fontSize: `${theme.typography.size.s1 - 1}px`,
- fontWeight: theme.typography.weight.bold,
- lineHeight: '16px',
- minHeight: 28,
- letterSpacing: '0.16em',
- textTransform: 'uppercase',
- color: theme.textMutedColor,
}));
const Wrapper = styled.div({
diff --git a/code/core/src/manager/components/sidebar/components/CollapseIcon.tsx b/code/core/src/manager/components/sidebar/components/CollapseIcon.tsx
index 0c541f623b4f..171d12fdc343 100644
--- a/code/core/src/manager/components/sidebar/components/CollapseIcon.tsx
+++ b/code/core/src/manager/components/sidebar/components/CollapseIcon.tsx
@@ -3,7 +3,7 @@ import React from 'react';
import { styled } from 'storybook/theming';
-export const CollapseIconWrapper = styled.div<{ isExpanded: boolean }>(({ isExpanded }) => ({
+export const CollapseIconWrapper = styled.div<{ isExpanded: boolean }>(({ theme, isExpanded }) => ({
width: 8,
height: 8,
display: 'flex',
@@ -11,6 +11,7 @@ export const CollapseIconWrapper = styled.div<{ isExpanded: boolean }>(({ isExpa
alignItems: 'center',
transform: isExpanded ? 'rotateZ(90deg)' : 'none',
transition: 'transform .1s ease-out',
+ color: theme.textMutedColor,
}));
export const CollapseIcon: FC> = (props) => (
diff --git a/code/core/src/manager/components/sidebar/useDynamicFavicon.stories.tsx b/code/core/src/manager/components/sidebar/useDynamicFavicon.stories.tsx
index 66b60153bf21..5780d6bf624f 100644
--- a/code/core/src/manager/components/sidebar/useDynamicFavicon.stories.tsx
+++ b/code/core/src/manager/components/sidebar/useDynamicFavicon.stories.tsx
@@ -44,12 +44,12 @@ export const Statuses = {
},
render: ({ size }: { size: number }) => (
),
};
@@ -64,11 +64,11 @@ export const Sizes = {
},
render: ({ status }: { status?: Parameters[1] }) => (
),
};
diff --git a/code/core/src/manager/container/Menu.stories.tsx b/code/core/src/manager/container/Menu.stories.tsx
index 2a97012f0ad5..def579e9fc0f 100644
--- a/code/core/src/manager/container/Menu.stories.tsx
+++ b/code/core/src/manager/container/Menu.stories.tsx
@@ -1,6 +1,6 @@
import React from 'react';
-import { TooltipLinkList, WithTooltip } from 'storybook/internal/components';
+import { Button, PopoverProvider, TooltipLinkList } from 'storybook/internal/components';
import type { Meta, StoryObj } from '@storybook/react-vite';
@@ -19,9 +19,9 @@ export default {
height: '300px',
}}
>
-
- Tooltip
-
+
+ Click me
+
),
],
diff --git a/code/core/src/manager/globals/exports.ts b/code/core/src/manager/globals/exports.ts
index c7de468b7254..5a193d06cd24 100644
--- a/code/core/src/manager/globals/exports.ts
+++ b/code/core/src/manager/globals/exports.ts
@@ -341,6 +341,7 @@ export default {
'mockChannel',
'optionOrAltSymbol',
'shortcutMatchesShortcut',
+ 'shortcutToAriaKeyshortcuts',
'shortcutToHumanString',
'types',
'useAddonState',
@@ -481,6 +482,7 @@ export default {
'storybook/internal/client-logger': ['deprecate', 'logger', 'once', 'pretty'],
'storybook/internal/components': [
'A',
+ 'AbstractToolbar',
'ActionBar',
'AddonPanel',
'Badge',
@@ -510,40 +512,57 @@ export default {
'ListItem',
'Loader',
'Modal',
+ 'ModalDecorator',
'OL',
'P',
'Placeholder',
+ 'Popover',
+ 'PopoverProvider',
'Pre',
'ProgressSpinner',
'ResetWrapper',
'ScrollArea',
+ 'Select',
'Separator',
'Spaced',
'Span',
+ 'StatelessTab',
+ 'StatelessTabList',
+ 'StatelessTabPanel',
+ 'StatelessTabsView',
'StorybookIcon',
'StorybookLogo',
'SyntaxHighlighter',
'TT',
'TabBar',
'TabButton',
+ 'TabList',
+ 'TabPanel',
'TabWrapper',
'Table',
'Tabs',
'TabsState',
+ 'TabsView',
+ 'ToggleButton',
+ 'Toolbar',
+ 'Tooltip',
'TooltipLinkList',
'TooltipMessage',
'TooltipNote',
+ 'TooltipProvider',
'UL',
'WithTooltip',
'WithTooltipPure',
'Zoom',
'codeCommon',
'components',
+ 'convertToReactAriaPlacement',
'createCopyToClipboardFunction',
'getStoryHref',
'interleaveSeparators',
'nameSpaceClassNames',
'resetComponents',
+ 'useTabsState',
'withReset',
],
'storybook/internal/core-events': [
diff --git a/code/core/src/manager/settings/About.tsx b/code/core/src/manager/settings/About.tsx
index 7985a8d4f146..63e8fa526552 100644
--- a/code/core/src/manager/settings/About.tsx
+++ b/code/core/src/manager/settings/About.tsx
@@ -66,13 +66,13 @@ const AboutScreen: FC<{ onNavigateToWhatsNew?: () => void }> = ({ onNavigateToWh
-
+
GitHub
-
+
Documentation
diff --git a/code/core/src/manager/settings/index.tsx b/code/core/src/manager/settings/index.tsx
index 375a4eae5c3a..c91bce32a778 100644
--- a/code/core/src/manager/settings/index.tsx
+++ b/code/core/src/manager/settings/index.tsx
@@ -1,7 +1,7 @@
-import type { FC, SyntheticEvent } from 'react';
-import React, { Fragment } from 'react';
+import type { FC, ReactNode, SyntheticEvent } from 'react';
+import React, { useMemo } from 'react';
-import { IconButton, ScrollArea, TabBar, TabButton } from 'storybook/internal/components';
+import { Button, ScrollArea, TabsView } from 'storybook/internal/components';
import { Location, Route } from 'storybook/internal/router';
import type { Addon_PageType } from 'storybook/internal/types';
@@ -18,50 +18,17 @@ import { WhatsNewPage } from './whats_new_page';
const { document } = global;
-const Header = styled.div(({ theme }) => ({
- display: 'flex',
- justifyContent: 'space-between',
- alignItems: 'center',
- height: 40,
- boxShadow: `${theme.appBorderColor} 0 -1px 0 0 inset`,
- background: theme.barBg,
- paddingRight: 8,
+const Content = styled(ScrollArea)(({ theme }) => ({
+ background: theme.background.content,
}));
-const TabBarButton = React.memo(function TabBarButton({
- changeTab,
- id,
- title,
-}: {
- changeTab: (tab: string) => void;
- id: string;
- title: string;
-}) {
+const RouteWrapper: FC<{ children: ReactNode; path: string }> = ({ children, path }) => {
return (
-
- {({ path }) => {
- const active = path.includes(`settings/${id}`);
- return (
- changeTab(id)}
- role="tab"
- >
- {title}
-
- );
- }}
-
+
+ {children}
+
);
-});
-
-const Content = styled(ScrollArea)(({ theme }) => ({
- background: theme.background.content,
-}));
+};
const Pages: FC<{
onClose: () => void;
@@ -83,38 +50,70 @@ const Pages: FC<{
return () => document.removeEventListener('keydown', handleEscape);
}, [enableShortcuts, onClose]);
- return (
-
-
-
-
- {enableWhatsNew && (
-
- )}
-
-
- {
- e.preventDefault();
- return onClose();
- }}
- title="Close settings page"
- >
-
-
-
-
-
-
-
-
-
-
-
+ const tabs = useMemo(() => {
+ const tabsToInclude = [
+ {
+ id: 'about',
+ title: 'About',
+ children: (
+
+
+
+ ),
+ },
+ ];
+
+ if (enableWhatsNew) {
+ tabsToInclude.push({
+ id: 'whats-new',
+ title: "What's new?",
+ children: (
+
+
+
+ ),
+ });
+ }
+
+ tabsToInclude.push({
+ id: 'shortcuts',
+ title: 'Keyboard shortcuts',
+ children: (
+
-
-
-
+
+ ),
+ });
+
+ return tabsToInclude;
+ }, [enableWhatsNew]);
+
+ return (
+
+ {({ path }) => {
+ const selected = tabs.find((tab) => path.includes(`settings/${tab.id}`))?.id;
+ return (
+ {
+ e.preventDefault();
+ return onClose();
+ }}
+ ariaLabel="Close settings page"
+ >
+
+
+ }
+ selected={selected}
+ onSelectionChange={changeTab}
+ />
+ );
+ }}
+
);
};
diff --git a/code/core/src/manager/settings/shortcuts.tsx b/code/core/src/manager/settings/shortcuts.tsx
index 03c644e18be5..33a29e239ba7 100644
--- a/code/core/src/manager/settings/shortcuts.tsx
+++ b/code/core/src/manager/settings/shortcuts.tsx
@@ -118,7 +118,7 @@ const shortcutLabels = {
togglePanel: 'Toggle addons',
panelPosition: 'Toggle addons orientation',
toggleNav: 'Toggle sidebar',
- toolbar: 'Toggle canvas toolbar',
+ toolbar: 'Toggle toolbar',
search: 'Focus search',
focusNav: 'Focus sidebar',
focusIframe: 'Focus canvas',
@@ -131,7 +131,7 @@ const shortcutLabels = {
aboutPage: 'Go to about page',
collapseAll: 'Collapse all items on sidebar',
expandAll: 'Expand all items on sidebar',
- remount: 'Remount component',
+ remount: 'Reload story',
openInEditor: 'Open story in editor',
copyStoryLink: 'Copy story link to clipboard',
// TODO: bring this back once we want to add shortcuts for this
@@ -332,6 +332,7 @@ class ShortcutsScreen extends Component ({
const Container = styled.div(({ theme }) => ({
position: 'absolute',
width: '100%',
- bottom: '40px',
+ bottom: '0px',
background: theme.background.bar,
fontSize: `13px`,
borderTop: '1px solid',
@@ -68,17 +68,17 @@ export const WhatsNewFooter = ({
Share this with your team.
-
+
{copyText}
{isNotificationsEnabled ? (
-
+
Hide notifications
) : (
-
+
Show notifications
@@ -98,7 +98,7 @@ const Iframe = styled.iframe<{ isLoaded: boolean }>(
margin: 0,
padding: 0,
width: '100%',
- height: 'calc(100% - 80px)',
+ height: 'calc(100% - 40px)',
background: 'white',
},
({ isLoaded }) => ({ visibility: isLoaded ? 'visible' : 'hidden' })
diff --git a/code/core/src/manager/utils/status.tsx b/code/core/src/manager/utils/status.tsx
index e53d5c67bfa4..9d545d7a9701 100644
--- a/code/core/src/manager/utils/status.tsx
+++ b/code/core/src/manager/utils/status.tsx
@@ -19,10 +19,9 @@ const SmallIcons = styled(CircleIcon)({
},
});
-const LoadingIcons = styled(SmallIcons)(({ theme: { animation, color, base } }) => ({
+const LoadingIcons = styled(SmallIcons)(({ theme: { animation } }) => ({
// specificity hack
animation: `${animation.glow} 1.5s ease-in-out infinite`,
- color: base === 'light' ? color.mediumdark : color.darker,
}));
export const statusPriority: StatusValue[] = [
diff --git a/code/core/src/measure/Tool.tsx b/code/core/src/measure/Tool.tsx
index e259cf5334e0..8f20e29c6183 100644
--- a/code/core/src/measure/Tool.tsx
+++ b/code/core/src/measure/Tool.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useEffect } from 'react';
-import { IconButton } from 'storybook/internal/components';
+import { ToggleButton } from 'storybook/internal/components';
import { RulerIcon } from '@storybook/icons';
@@ -32,13 +32,17 @@ export const Tool = () => {
}, [toggleMeasure, api]);
return (
-
-
+
);
};
diff --git a/code/core/src/outline/OutlineSelector.tsx b/code/core/src/outline/OutlineSelector.tsx
index f31250614982..0001e255c769 100644
--- a/code/core/src/outline/OutlineSelector.tsx
+++ b/code/core/src/outline/OutlineSelector.tsx
@@ -1,6 +1,6 @@
import React, { memo, useCallback, useEffect } from 'react';
-import { IconButton } from 'storybook/internal/components';
+import { ToggleButton } from 'storybook/internal/components';
import { OutlineIcon } from '@storybook/icons';
@@ -19,7 +19,7 @@ export const OutlineSelector = memo(function OutlineSelector() {
updateGlobals({
[PARAM_KEY]: !isActive,
}),
- [isActive]
+ [isActive, updateGlobals]
);
useEffect(() => {
@@ -33,13 +33,17 @@ export const OutlineSelector = memo(function OutlineSelector() {
}, [toggleOutline, api]);
return (
-
-
+
);
});
diff --git a/code/core/src/preview/preview-navigator.ts b/code/core/src/preview/preview-navigator.ts
index c45271be98af..51fbed0260f6 100644
--- a/code/core/src/preview/preview-navigator.ts
+++ b/code/core/src/preview/preview-navigator.ts
@@ -185,7 +185,7 @@ export const setupPreviewNavigator = async (index: StoryIndex, currentEntryId: s
}
.sb-navigator-story-link.active {
font-weight: bold;
- color: rgb(2, 156, 253);
+ color: hsl(212 100 46);
}
`;
document.head.appendChild(style);
diff --git a/code/core/src/theming/base.ts b/code/core/src/theming/base.ts
index a322d46470a4..07276bed1783 100644
--- a/code/core/src/theming/base.ts
+++ b/code/core/src/theming/base.ts
@@ -1,9 +1,7 @@
-import { transparentize } from 'polished';
-
export const color = {
// Official color palette
primary: '#FF4785', // coral
- secondary: '#029CFD', // ocean
+ secondary: '#006DEB', // ocean
tertiary: '#FAFBFC',
ancillary: '#22a699',
@@ -17,30 +15,30 @@ export const color = {
// Monochrome
lightest: '#FFFFFF',
- lighter: '#F7FAFC',
- light: '#EEF3F6',
- mediumlight: '#ECF4F9',
- medium: '#D9E8F2',
- mediumdark: '#73828C',
- dark: '#5C6870',
- darker: '#454E54',
- darkest: '#2E3438',
+ lighter: '#F6F9FC',
+ light: '#EEF2F6',
+ mediumlight: '#ECF2F9',
+ medium: '#D9E5F2',
+ mediumdark: '#737F8C',
+ dark: '#5C6570',
+ darker: '#454C54',
+ darkest: '#2E3338',
// For borders
- border: 'hsla(203, 50%, 30%, 0.15)',
+ border: 'hsla(212, 50%, 30%, 0.15)',
// Status
positive: '#66BF3C',
- negative: '#FF4400',
warning: '#E69D00',
+ negative: '#FF4400',
critical: '#FFFFFF',
// Text
- defaultText: '#2E3438',
+ defaultText: '#2E3338',
inverseText: '#FFFFFF',
- positiveText: '#448028',
- negativeText: '#D43900',
- warningText: '#A15C20',
+ positiveText: '#427C27',
+ warningText: '#955B1E',
+ negativeText: '#C23400',
};
export const background = {
@@ -49,13 +47,13 @@ export const background = {
content: color.lightest,
preview: color.lightest,
gridCellSize: 10,
- hoverable: transparentize(0.9, color.secondary), // hover state for items in a list
+ hoverable: '#DBECFF',
// Notification, error, and warning backgrounds
- positive: '#E1FFD4',
- negative: '#FEDED2',
- warning: '#FFF5CF',
- critical: '#FF4400',
+ positive: '#F1FFEB',
+ warning: '#FFF9EB',
+ negative: '#FFF0EB',
+ critical: '#D13800',
};
export const typography = {
diff --git a/code/core/src/theming/convert.ts b/code/core/src/theming/convert.ts
index 1336c029493f..8cc9210ce052 100644
--- a/code/core/src/theming/convert.ts
+++ b/code/core/src/theming/convert.ts
@@ -12,16 +12,16 @@ const lightSyntaxColors = {
red1: '#A31515',
red2: '#9a050f',
red3: '#800000',
- red4: '#ff0000',
+ red4: '#eb0000',
gray1: '#393A34',
- cyan1: '#36acaa',
- cyan2: '#2B91AF',
+ cyan1: '#008380',
+ cyan2: '#007ca0',
blue1: '#0000ff',
blue2: '#00009f',
};
const darkSyntaxColors = {
- green1: '#7C7C7C',
+ green1: '#95999D',
red1: '#92C379',
red2: '#9a050f',
red3: '#A8FF60',
@@ -82,6 +82,7 @@ export const convert = (inherit: ThemeVars = themes[getPreferredColorScheme()]):
colorSecondary,
appBg,
appContentBg,
+ appHoverBg,
appPreviewBg,
appBorderColor,
appBorderRadius,
@@ -120,7 +121,7 @@ export const convert = (inherit: ThemeVars = themes[getPreferredColorScheme()]):
content: appContentBg,
preview: appPreviewBg,
gridCellSize: gridCellSize || background.gridCellSize,
- hoverable: background.hoverable,
+ hoverable: appHoverBg,
positive: background.positive,
negative: background.negative,
warning: background.warning,
diff --git a/code/core/src/theming/global.ts b/code/core/src/theming/global.ts
index 77bd7bc590b2..3a28c75f86d1 100644
--- a/code/core/src/theming/global.ts
+++ b/code/core/src/theming/global.ts
@@ -116,7 +116,9 @@ export const createGlobal = memoize(1)(({
padding: 0,
margin: -1,
overflow: 'hidden',
+ whiteSpace: 'nowrap',
clip: 'rect(0, 0, 0, 0)',
+ clipPath: 'inset(50%)',
border: 0,
},
@@ -128,5 +130,9 @@ export const createGlobal = memoize(1)(({
'.sb-hidden-until-focus:focus': {
opacity: 1,
},
+
+ '.react-aria-Popover:focus-visible': {
+ outline: 'none',
+ },
};
});
diff --git a/code/core/src/theming/tests/convert.test.js b/code/core/src/theming/tests/convert.test.js
index 779bbd0349d3..6cfa924323a7 100644
--- a/code/core/src/theming/tests/convert.test.js
+++ b/code/core/src/theming/tests/convert.test.js
@@ -17,11 +17,47 @@ describe('convert', () => {
expect(result.base).toEqual('dark');
expect(result).toMatchObject({
color: expect.objectContaining({
+ ancillary: '#22a699',
+ border: 'hsla(212, 50%, 30%, 0.15)',
+ critical: '#FFFFFF',
+ dark: '#5C6570',
+ darker: '#454C54',
+ darkest: '#2E3338',
+ defaultText: '#C9CCCF',
+ gold: '#FFAE00',
+ green: '#66BF3C',
+ inverseText: '#1B1C1D',
+ light: '#EEF2F6',
+ lighter: '#F6F9FC',
+ lightest: '#FFFFFF',
+ medium: '#D9E5F2',
+ mediumdark: '#737F8C',
+ mediumlight: '#ECF2F9',
+ negative: '#FF4400',
+ negativeText: '#C23400',
+ orange: '#FC521F',
+ positive: '#66BF3C',
+ positiveText: '#427C27',
primary: '#FF4785',
- secondary: '#029CFD',
+ purple: '#6F2CAC',
+ seafoam: '#37D5D3',
+ secondary: '#479DFF',
+ tertiary: '#FAFBFC',
+ ultraviolet: '#2A0481',
+ warning: '#E69D00',
+ warningText: '#955B1E',
}),
background: expect.objectContaining({
- app: '#222425',
+ app: '#1B1C1D',
+ bar: '#222325',
+ content: '#222325',
+ critical: '#D13800',
+ gridCellSize: 10,
+ hoverable: '#233952',
+ negative: '#FFF0EB',
+ positive: '#F1FFEB',
+ preview: '#FFFFFF',
+ warning: '#FFF9EB',
}),
});
});
@@ -31,11 +67,47 @@ describe('convert', () => {
expect(result.base).toEqual('light');
expect(result).toMatchObject({
color: expect.objectContaining({
+ ancillary: '#22a699',
+ border: 'hsla(212, 50%, 30%, 0.15)',
+ critical: '#FFFFFF',
+ dark: '#5C6570',
+ darker: '#454C54',
+ darkest: '#2E3338',
+ defaultText: '#2E3338',
+ gold: '#FFAE00',
+ green: '#66BF3C',
+ inverseText: '#FFFFFF',
+ light: '#EEF2F6',
+ lighter: '#F6F9FC',
+ lightest: '#FFFFFF',
+ medium: '#D9E5F2',
+ mediumdark: '#737F8C',
+ mediumlight: '#ECF2F9',
+ negative: '#FF4400',
+ negativeText: '#C23400',
+ orange: '#FC521F',
+ positive: '#66BF3C',
+ positiveText: '#427C27',
primary: '#FF4785',
- secondary: '#029CFD',
+ purple: '#6F2CAC',
+ seafoam: '#37D5D3',
+ secondary: '#006DEB',
+ tertiary: '#FAFBFC',
+ ultraviolet: '#2A0481',
+ warning: '#E69D00',
+ warningText: '#955B1E',
}),
background: expect.objectContaining({
app: '#F6F9FC',
+ bar: '#FFFFFF',
+ content: '#FFFFFF',
+ critical: '#D13800',
+ gridCellSize: 10,
+ hoverable: '#DBECFF',
+ negative: '#FFF0EB',
+ positive: '#F1FFEB',
+ preview: '#FFFFFF',
+ warning: '#FFF9EB',
}),
});
});
diff --git a/code/core/src/theming/themes/dark.ts b/code/core/src/theming/themes/dark.ts
index 173e735ef366..7748f69dd30b 100644
--- a/code/core/src/theming/themes/dark.ts
+++ b/code/core/src/theming/themes/dark.ts
@@ -6,11 +6,12 @@ const theme: ThemeVars = {
// Storybook-specific color palette
colorPrimary: '#FF4785', // coral
- colorSecondary: '#029CFD', // ocean
+ colorSecondary: '#479DFF',
// UI
- appBg: '#222425',
- appContentBg: '#1B1C1D',
+ appBg: '#1B1C1D',
+ appContentBg: '#222325',
+ appHoverBg: '#233952',
appPreviewBg: color.lightest,
appBorderColor: 'rgba(255,255,255,.1)',
appBorderRadius: 4,
@@ -20,24 +21,24 @@ const theme: ThemeVars = {
fontCode: typography.fonts.mono,
// Text colors
- textColor: '#C9CDCF',
- textInverseColor: '#222425',
- textMutedColor: '#798186',
+ textColor: '#C9CCCF',
+ textInverseColor: '#1B1C1D',
+ textMutedColor: '#95999D',
// Toolbar default and active colors
- barTextColor: color.mediumdark,
- barHoverColor: color.secondary,
- barSelectedColor: color.secondary,
- barBg: '#292C2E',
+ barTextColor: '#95999D',
+ barHoverColor: '#70B3FF',
+ barSelectedColor: '#479DFF',
+ barBg: '#222325',
// Form colors
- buttonBg: '#222425',
- buttonBorder: 'rgba(255,255,255,.1)',
- booleanBg: '#222425',
- booleanSelectedBg: '#2E3438',
+ buttonBg: '#1B1C1D',
+ buttonBorder: 'hsl(0 0 100 / 0.1)',
+ booleanBg: '#1B1C1D',
+ booleanSelectedBg: '#292B2E',
inputBg: '#1B1C1D',
- inputBorder: 'rgba(255,255,255,.1)',
- inputTextColor: color.lightest,
+ inputBorder: 'hsl(0 0 100 / 0.1)',
+ inputTextColor: '#C9CCCF',
inputBorderRadius: 4,
};
diff --git a/code/core/src/theming/themes/light.ts b/code/core/src/theming/themes/light.ts
index a5bf4def7e9c..d896cb9deb51 100644
--- a/code/core/src/theming/themes/light.ts
+++ b/code/core/src/theming/themes/light.ts
@@ -5,12 +5,13 @@ const theme: ThemeVars = {
base: 'light',
// Storybook-specific color palette
- colorPrimary: '#FF4785', // coral
- colorSecondary: '#029CFD', // ocean
+ colorPrimary: color.primary,
+ colorSecondary: color.secondary,
// UI
appBg: background.app,
appContentBg: color.lightest,
+ appHoverBg: '#DBECFF',
appPreviewBg: color.lightest,
appBorderColor: color.border,
appBorderRadius: 4,
@@ -25,9 +26,9 @@ const theme: ThemeVars = {
textMutedColor: color.dark,
// Toolbar default and active colors
- barTextColor: color.mediumdark,
- barHoverColor: color.secondary,
- barSelectedColor: color.secondary,
+ barTextColor: color.dark,
+ barHoverColor: '#005CC7',
+ barSelectedColor: '#0063D6',
barBg: color.lightest,
// Form colors
diff --git a/code/core/src/theming/types.ts b/code/core/src/theming/types.ts
index 1f386bfebd8a..f009df790df8 100644
--- a/code/core/src/theming/types.ts
+++ b/code/core/src/theming/types.ts
@@ -16,6 +16,7 @@ export interface ThemeVarsColors {
// UI
appBg: string;
appContentBg: string;
+ appHoverBg: string;
appPreviewBg: string;
appBorderColor: string;
appBorderRadius: number;
diff --git a/code/core/src/toolbar/components/ToolbarManager.tsx b/code/core/src/toolbar/components/ToolbarManager.tsx
index a379b9398566..c391c3b4f172 100644
--- a/code/core/src/toolbar/components/ToolbarManager.tsx
+++ b/code/core/src/toolbar/components/ToolbarManager.tsx
@@ -6,7 +6,7 @@ import { useGlobalTypes } from 'storybook/manager-api';
import type { ToolbarArgType } from '../types';
import { normalizeArgType } from '../utils/normalize-toolbar-arg-type';
-import { ToolbarMenuList } from './ToolbarMenuList';
+import { ToolbarMenuSelect } from './ToolbarMenuSelect';
/** A smart component for handling manager-preview interactions. */
export const ToolbarManager: FC = () => {
@@ -23,7 +23,7 @@ export const ToolbarManager: FC = () => {
{globalIds.map((id) => {
const normalizedArgType = normalizeArgType(id, globalTypes[id] as ToolbarArgType);
- return ;
+ return ;
})}
>
);
diff --git a/code/core/src/toolbar/components/ToolbarMenuButton.tsx b/code/core/src/toolbar/components/ToolbarMenuButton.tsx
deleted file mode 100644
index 5c55e63c0f16..000000000000
--- a/code/core/src/toolbar/components/ToolbarMenuButton.tsx
+++ /dev/null
@@ -1,39 +0,0 @@
-import React, { type FC } from 'react';
-
-import { IconButton } from 'storybook/internal/components';
-
-import { Icons, type IconsProps } from '../../components/components/icon/icon';
-
-interface ToolbarMenuButtonProps {
- active: boolean;
- disabled?: boolean;
- title: string;
- icon?: IconsProps['icon'];
- description: string;
- onClick?: () => void;
-}
-
-// We can't remove the Icons component just yet because there's no way for now to import icons
-// in the preview directly. Before having a better solution, we are going to keep the Icons component
-// for now and remove the deprecated warning.
-
-export const ToolbarMenuButton: FC = ({
- active,
- disabled,
- title,
- icon,
- description,
- onClick,
-}) => {
- return (
- {} : onClick}
- >
- {icon && }
- {title ? `\xa0${title}` : null}
-
- );
-};
diff --git a/code/core/src/toolbar/components/ToolbarMenuList.tsx b/code/core/src/toolbar/components/ToolbarMenuList.tsx
deleted file mode 100644
index 2958044bfde9..000000000000
--- a/code/core/src/toolbar/components/ToolbarMenuList.tsx
+++ /dev/null
@@ -1,97 +0,0 @@
-import type { FC } from 'react';
-import React, { useCallback, useState } from 'react';
-
-import { TooltipLinkList, WithTooltip } from 'storybook/internal/components';
-
-import { useGlobals } from 'storybook/manager-api';
-
-import type { WithKeyboardCycleProps } from '../hoc/withKeyboardCycle';
-import { withKeyboardCycle } from '../hoc/withKeyboardCycle';
-import type { ToolbarMenuProps } from '../types';
-import { getSelectedIcon, getSelectedTitle } from '../utils/get-selected';
-import { ToolbarMenuButton } from './ToolbarMenuButton';
-import { ToolbarMenuListItem } from './ToolbarMenuListItem';
-
-type ToolbarMenuListProps = ToolbarMenuProps & WithKeyboardCycleProps;
-
-export const ToolbarMenuList: FC = withKeyboardCycle(
- ({
- id,
- name,
- description,
- toolbar: { icon: _icon, items, title: _title, preventDynamicIcon, dynamicTitle },
- }) => {
- const [globals, updateGlobals, storyGlobals] = useGlobals();
- const [isTooltipVisible, setIsTooltipVisible] = useState(false);
-
- const currentValue = globals[id];
- const hasGlobalValue = !!currentValue;
- const isOverridden = id in storyGlobals;
- let icon = _icon;
- let title = _title;
-
- if (!preventDynamicIcon) {
- icon = getSelectedIcon({ currentValue, items }) || icon;
- }
-
- if (dynamicTitle) {
- title = getSelectedTitle({ currentValue, items }) || title;
- }
-
- if (!title && !icon) {
- console.warn(`Toolbar '${name}' has no title or icon`);
- }
-
- const handleItemClick = useCallback(
- (value: string | undefined) => {
- updateGlobals({ [id]: value });
- },
- [id, updateGlobals]
- );
-
- return (
- {
- const links = items
- // Special case handling for various "type" variants
- .filter(({ type }) => {
- let shouldReturn = true;
-
- if (type === 'reset' && !currentValue) {
- shouldReturn = false;
- }
-
- return shouldReturn;
- })
- .map((item) => {
- const listItem = ToolbarMenuListItem({
- ...item,
- currentValue,
- disabled: isOverridden,
- onClick: () => {
- handleItemClick(item.value);
- onHide();
- },
- });
-
- return listItem;
- });
- return ;
- }}
- closeOnOutsideClick
- onVisibleChange={setIsTooltipVisible}
- >
- {
-
- }
-
- );
- }
-);
diff --git a/code/core/src/toolbar/components/ToolbarMenuListItem.tsx b/code/core/src/toolbar/components/ToolbarMenuListItem.tsx
deleted file mode 100644
index 3ebb317aba48..000000000000
--- a/code/core/src/toolbar/components/ToolbarMenuListItem.tsx
+++ /dev/null
@@ -1,42 +0,0 @@
-import React from 'react';
-
-import type { TooltipLinkListLink } from 'storybook/internal/components';
-
-import { Icons } from '../../components/components/icon/icon';
-import type { ToolbarItem } from '../types';
-
-export type ToolbarMenuListItemProps = {
- currentValue: string;
- onClick: () => void;
- disabled?: boolean;
-} & ToolbarItem;
-
-export const ToolbarMenuListItem = ({
- right,
- title,
- value,
- icon,
- hideIcon,
- onClick,
- disabled,
- currentValue,
-}: ToolbarMenuListItemProps) => {
- const Icon = icon && (
-
- );
-
- const Item: TooltipLinkListLink = {
- id: value ?? '_reset',
- active: currentValue === value,
- right,
- title,
- disabled,
- onClick,
- };
-
- if (icon && !hideIcon) {
- Item.icon = Icon;
- }
-
- return Item;
-};
diff --git a/code/core/src/toolbar/components/ToolbarMenuSelect.tsx b/code/core/src/toolbar/components/ToolbarMenuSelect.tsx
new file mode 100644
index 000000000000..43052b602dcc
--- /dev/null
+++ b/code/core/src/toolbar/components/ToolbarMenuSelect.tsx
@@ -0,0 +1,110 @@
+import type { FC } from 'react';
+import React from 'react';
+
+import { Select } from 'storybook/internal/components';
+
+import { useGlobals } from 'storybook/manager-api';
+import { styled } from 'storybook/theming';
+
+import { Icons } from '../../components/components/icon/icon';
+import type { WithKeyboardCycleProps } from '../hoc/withKeyboardCycle';
+import { withKeyboardCycle } from '../hoc/withKeyboardCycle';
+import type { ToolbarItem, ToolbarMenuProps } from '../types';
+import { getSelectedIcon, getSelectedTitle } from '../utils/get-selected';
+
+// We can't remove the Icons component just yet because there's no way for now to import icons
+// in the preview directly. Before having a better solution, we are going to keep the Icons component
+// for now and remove the deprecated warning.
+
+const ToolbarMenuItemContainer = styled('div')({
+ width: '100%',
+ display: 'flex',
+ alignItems: 'center',
+ gap: 8,
+});
+const ToolbarMenuItemMiddle = styled('div')({
+ flex: 1,
+});
+
+type ToolbarMenuSelectProps = ToolbarMenuProps & WithKeyboardCycleProps;
+
+export const ToolbarMenuSelect: FC = withKeyboardCycle(
+ ({
+ id,
+ name,
+ description,
+ toolbar: { icon: _icon, items, title: _title, preventDynamicIcon, dynamicTitle },
+ }) => {
+ const [globals, updateGlobals, storyGlobals] = useGlobals();
+
+ const currentValue = globals[id];
+ const isOverridden = id in storyGlobals;
+ let icon = _icon;
+ let title = _title;
+
+ if (!preventDynamicIcon) {
+ icon = getSelectedIcon({ currentValue, items }) || icon;
+ }
+
+ if (dynamicTitle) {
+ title = getSelectedTitle({ currentValue, items }) || title;
+ }
+
+ if (!title && !icon) {
+ console.warn(`Toolbar '${name}' has no title or icon`);
+ }
+
+ const resetItem = items.find((item) => item.type === 'reset');
+ const resetLabel = resetItem?.title;
+ const options = items
+ .filter((item) => item.type === 'item')
+ .filter((item): item is ToolbarItem & { value: string } => item.value !== undefined)
+ .map((item) => {
+ const itemTitle = item.title ?? item.value;
+ const iconComponent =
+ !item.hideIcon && item.icon ? (
+
+ ) : undefined;
+
+ if (item.right) {
+ return {
+ title: itemTitle,
+ value: item.value,
+ children: (
+
+ {iconComponent}
+ {item.title ?? item.value}
+ {item.right}
+
+ ),
+ };
+ } else {
+ return {
+ title: itemTitle,
+ value: item.value,
+ icon: iconComponent,
+ };
+ }
+ });
+
+ // FIXME: for SB 10 we would want description to become an aria-description, and to add an
+ // ariaLabel prop to tools with an automigration switching current description to ariaLabel
+ const ariaLabel = description || title || name || id;
+
+ return (
+ updateGlobals({ [id]: '_reset' })}
+ onSelect={(selected) => updateGlobals({ [id]: selected })}
+ icon={icon && }
+ >
+ {title}
+
+ );
+ }
+);
diff --git a/code/core/src/toolbar/index.ts b/code/core/src/toolbar/index.ts
index df9adcaca421..a645849c9ad0 100644
--- a/code/core/src/toolbar/index.ts
+++ b/code/core/src/toolbar/index.ts
@@ -1,6 +1,3 @@
export * from './types';
export * from './constants';
export * from './components/ToolbarManager';
-export * from './components/ToolbarMenuButton';
-export * from './components/ToolbarMenuList';
-export * from './components/ToolbarMenuListItem';
diff --git a/code/core/src/viewport/components/Tool.tsx b/code/core/src/viewport/components/Tool.tsx
index 7f59b63f8934..c821c948d663 100644
--- a/code/core/src/viewport/components/Tool.tsx
+++ b/code/core/src/viewport/components/Tool.tsx
@@ -1,44 +1,33 @@
-import React, { type FC, Fragment, useCallback, useEffect, useState } from 'react';
+import React, { type FC, Fragment, useCallback, useEffect, useMemo } from 'react';
-import { IconButton, TooltipLinkList, WithTooltip } from 'storybook/internal/components';
+import { Button, Select } from 'storybook/internal/components';
-import { GrowIcon, RefreshIcon, TransferIcon } from '@storybook/icons';
+import { GrowIcon, TransferIcon } from '@storybook/icons';
import { type API, useGlobals, useParameter } from 'storybook/manager-api';
-import { Global } from 'storybook/theming';
+import { Global, styled } from 'storybook/theming';
import { PARAM_KEY } from '../constants';
import { MINIMAL_VIEWPORTS } from '../defaults';
import { responsiveViewport } from '../responsiveViewport';
import { registerShortcuts } from '../shortcuts';
import type { GlobalStateUpdate, Viewport, ViewportMap, ViewportParameters } from '../types';
-import {
- ActiveViewportLabel,
- ActiveViewportSize,
- IconButtonLabel,
- IconButtonWithLabel,
- iconsMap,
-} from '../utils';
+import { ActiveViewportLabel, ActiveViewportSize, iconsMap } from '../utils';
interface PureProps {
item: Viewport;
updateGlobals: ReturnType['1'];
- setIsTooltipVisible: React.Dispatch>;
viewportMap: ViewportMap;
viewportName: keyof ViewportMap;
isLocked: boolean;
- isActive: boolean;
isRotated: boolean | undefined;
width: string;
height: string;
}
-type Link = Parameters['0']['links'][0];
-
export const ViewportTool: FC<{ api: API }> = ({ api }) => {
const config = useParameter(PARAM_KEY);
const [globals, updateGlobals, storyGlobals] = useGlobals();
- const [isTooltipVisible, setIsTooltipVisible] = useState(false);
const { options = MINIMAL_VIEWPORTS, disable } = config || {};
const data = globals?.[PARAM_KEY] || {};
@@ -46,7 +35,6 @@ export const ViewportTool: FC<{ api: API }> = ({ api }) => {
const isRotated = typeof data === 'string' ? false : !!data.isRotated;
const item = (options as ViewportMap)[viewportName] || responsiveViewport;
- const isActive = isTooltipVisible || item !== responsiveViewport;
const isLocked = PARAM_KEY in storyGlobals;
const length = Object.keys(options).length;
@@ -81,9 +69,7 @@ export const ViewportTool: FC<{ api: API }> = ({ api }) => {
viewportMap: options,
viewportName,
isRotated,
- setIsTooltipVisible,
isLocked,
- isActive,
width,
height,
}}
@@ -91,78 +77,59 @@ export const ViewportTool: FC<{ api: API }> = ({ api }) => {
);
};
+// These ensure that we both present a logical DOM order based on whether
+// or not viewport dimensions are locked, and display them with the '/' or
+// rotate button in the middle.
+const FirstDimension = styled(ActiveViewportLabel)({
+ order: 1,
+});
+const DimensionSeparator = styled.div({
+ order: 2,
+});
+const LastDimension = styled(ActiveViewportLabel)({
+ order: 3,
+});
+
const Pure = React.memo(function PureTool(props: PureProps) {
- const {
- item,
- viewportMap,
- viewportName,
- isRotated,
- updateGlobals,
- setIsTooltipVisible,
- isLocked,
- isActive,
- width,
- height,
- } = props;
+ const { item, viewportMap, viewportName, isRotated, updateGlobals, isLocked, width, height } =
+ props;
const update = useCallback(
(input: GlobalStateUpdate | undefined) => updateGlobals({ [PARAM_KEY]: input }),
[updateGlobals]
);
+ const options = useMemo(
+ () =>
+ Object.entries(viewportMap).map(([k, value]) => ({
+ value: k,
+ title: value.name,
+ icon: iconsMap[value.type!],
+ })),
+ [viewportMap]
+ );
+
return (
- (
- 0 && item !== responsiveViewport
- ? [
- {
- id: 'reset',
- title: 'Reset viewport',
- icon: ,
- onClick: () => {
- update(undefined);
- onHide();
- },
- },
- ]
- : []),
- ...Object.entries(viewportMap).map (([k, value]) => ({
- id: k,
- title: value.name,
- icon: iconsMap[value.type!],
- active: k === viewportName,
- onClick: () => {
- update({ value: k, isRotated: false });
- onHide();
- },
- })),
- ].flat()}
- />
- )}
- closeOnOutsideClick
- onVisibleChange={setIsTooltipVisible}
+ update({ value: undefined, isRotated: false })}
+ key="viewport"
+ disabled={isLocked}
+ ariaLabel={isLocked ? 'Viewport size set by story parameters' : 'Viewport size'}
+ ariaDescription="Select a viewport size among predefined options for the preview area, or reset to the default size."
+ tooltip={isLocked ? 'Viewport size set by story parameters' : 'Resize viewport'}
+ defaultOptions={viewportName}
+ options={options}
+ onSelect={(selected) => update({ value: selected, isRotated: false })}
+ icon={ }
>
- {
- update({ value: undefined, isRotated: false });
- }}
- >
-
- {item !== responsiveViewport ? (
-
- {item.name} {isRotated ? `(L)` : `(P)`}
-
- ) : null}
-
-
+ {item !== responsiveViewport ? (
+ <>
+ {item.name} {isRotated ? `(L)` : `(P)`}
+ >
+ ) : null}
+
-
+
+ Viewport width:
{width.replace('px', '')}
-
- {!isLocked ? (
- {
- update({ value: viewportName, isRotated: !isRotated });
- }}
- >
-
-
- ) : (
- '/'
- )}
-
+
+ {isLocked && / }
+
+ Viewport height:
{height.replace('px', '')}
-
+
+ {!isLocked && (
+
+ {
+ update({ value: viewportName, isRotated: !isRotated });
+ }}
+ >
+
+
+
+ )}
) : null}
diff --git a/code/core/src/viewport/utils.tsx b/code/core/src/viewport/utils.tsx
index e3f66838aef5..934727dae7b0 100644
--- a/code/core/src/viewport/utils.tsx
+++ b/code/core/src/viewport/utils.tsx
@@ -1,7 +1,5 @@
import React, { Fragment } from 'react';
-import { IconButton } from 'storybook/internal/components';
-
import { BrowserIcon, MobileIcon, TabletIcon } from '@storybook/icons';
import { styled } from 'storybook/theming';
@@ -27,16 +25,6 @@ export const ActiveViewportLabel = styled.div(({ theme }) => ({
background: 'transparent',
}));
-export const IconButtonWithLabel = styled(IconButton)(() => ({
- display: 'inline-flex',
- alignItems: 'center',
-}));
-
-export const IconButtonLabel = styled.div(({ theme }) => ({
- fontSize: theme.typography.size.s2 - 1,
- marginLeft: 10,
-}));
-
export const iconsMap: Record, React.ReactNode> = {
desktop: ,
mobile: ,
diff --git a/code/e2e-tests/addon-a11y.spec.ts b/code/e2e-tests/addon-a11y.spec.ts
index ab737572e0e9..7d36c44f7b6f 100644
--- a/code/e2e-tests/addon-a11y.spec.ts
+++ b/code/e2e-tests/addon-a11y.spec.ts
@@ -17,7 +17,9 @@ test.describe('addon-a11y', () => {
await sbPage.viewAddonPanel('Accessibility');
const panel = sbPage.panelContent();
- await panel.getByRole('button', { name: 'Show highlights' }).click();
+ await panel
+ .getByRole('button', { name: 'Highlight elements with accessibility test results' })
+ .click();
const highlightElement = sbPage
.previewIframe()
@@ -28,7 +30,7 @@ test.describe('addon-a11y', () => {
'color(srgb 1 0.266667 0 / 0.4)'
);
- await page.getByRole('button', { name: 'Hide highlights' }).click();
+ await page.getByRole('button', { name: 'Hide accessibility test result highlights' }).click();
await expect(highlightElement).toBeHidden();
});
@@ -66,7 +68,10 @@ test.describe('addon-a11y', () => {
// navigate to that url
await page.goto(clipboard);
await new SbPage(page, expect).waitUntilLoaded();
- await expect(page.getByRole('tab', { name: 'Passes' })).toHaveAttribute('data-active', 'true');
+ await expect(page.getByRole('tab', { name: 'Passes' })).toHaveAttribute(
+ 'aria-selected',
+ 'true'
+ );
await expect(page.getByRole('button', { name: 'Hidden body' })).toHaveAttribute(
'aria-expanded',
'true'
diff --git a/code/e2e-tests/addon-actions.spec.ts b/code/e2e-tests/addon-actions.spec.ts
index b67ceb27447d..33890f9eeaa8 100644
--- a/code/e2e-tests/addon-actions.spec.ts
+++ b/code/e2e-tests/addon-actions.spec.ts
@@ -28,8 +28,8 @@ test.describe('addon-actions', () => {
await expect(button).toBeVisible();
await button.click();
- const logItem = page.locator('#storybook-panel-root #panel-tab-content', {
- hasText: 'click',
+ const logItem = sbPage.panelContent().locator('span', {
+ hasText: 'onClick:',
});
await expect(logItem).toBeVisible();
});
@@ -52,9 +52,12 @@ test.describe('addon-actions', () => {
await expect(button).toBeVisible();
await button.click();
- const logItem = page.locator('#storybook-panel-root #panel-tab-content', {
- hasText: 'console.log',
+ const logItem = sbPage.panelContent().locator('span', {
+ hasText: 'console.log:',
});
- await expect(logItem).toBeVisible();
+ // Avoid getting failed due to other console.log calls by frameworks
+ await expect(logItem.getByText('first')).toBeVisible();
+ await expect(logItem.getByText('second')).toBeVisible();
+ await expect(logItem.getByText('third')).toBeVisible();
});
});
diff --git a/code/e2e-tests/addon-backgrounds.spec.ts b/code/e2e-tests/addon-backgrounds.spec.ts
index 70cb2424ef19..7eb3024463e1 100644
--- a/code/e2e-tests/addon-backgrounds.spec.ts
+++ b/code/e2e-tests/addon-backgrounds.spec.ts
@@ -12,14 +12,14 @@ test.describe('addon-backgrounds', () => {
await new SbPage(page, expect).waitUntilLoaded();
});
- const backgroundToolbarSelector = '[title="Change the background of the preview"]';
- const gridToolbarSelector = '[title="Apply a grid to the preview"]';
+ const backgroundToolbarSelector = '[aria-label="Preview background"]';
+ const gridToolbarSelector = '[aria-label="Grid visibility"]';
test('should have a dark background', async ({ page }) => {
const sbPage = new SbPage(page, expect);
await sbPage.navigateToStory('example/button', 'primary');
- await sbPage.selectToolbar(backgroundToolbarSelector, '#list-item-dark');
+ await sbPage.selectToolbar(backgroundToolbarSelector, 'text=/dark/');
await expect(sbPage.getCanvasBodyElement()).toHaveCSS('background-color', 'rgb(51, 51, 51)');
});
diff --git a/code/e2e-tests/addon-controls.spec.ts b/code/e2e-tests/addon-controls.spec.ts
index 333a53c20096..6ce2b16625fe 100644
--- a/code/e2e-tests/addon-controls.spec.ts
+++ b/code/e2e-tests/addon-controls.spec.ts
@@ -57,7 +57,7 @@ test.describe('addon-controls', () => {
// cy.getStoryElement().find('button').should('have.css', 'font-size', '16px');
// Reset controls: assert that the component is back to original state
- const reset = sbPage.panelContent().locator('button[title="Reset controls"]');
+ const reset = sbPage.panelContent().locator('[aria-label="Reset controls"]');
await reset.click();
const button = sbPage.previewRoot().locator('button');
await expect(button).toHaveCSS('font-size', '14px');
@@ -82,6 +82,8 @@ test.describe('addon-controls', () => {
const sbPage = new SbPage(page, expect);
await sbPage.waitUntilLoaded();
+ await sbPage.closeAnyPendingModal();
+
await sbPage.viewAddonPanel('Controls');
await sbPage.panelContent().locator('#control-select').selectOption('double space');
@@ -94,6 +96,7 @@ test.describe('addon-controls', () => {
const sbPage = new SbPage(page, expect);
await sbPage.waitUntilLoaded();
+ await sbPage.closeAnyPendingModal();
await sbPage.viewAddonPanel('Controls');
await sbPage.panelContent().locator('#control-multiSelect').selectOption('double space');
diff --git a/code/e2e-tests/addon-onboarding.spec.ts b/code/e2e-tests/addon-onboarding.spec.ts
index 06840cfc3858..aac9498b7f48 100644
--- a/code/e2e-tests/addon-onboarding.spec.ts
+++ b/code/e2e-tests/addon-onboarding.spec.ts
@@ -35,14 +35,14 @@ test.describe('addon-onboarding', () => {
// so we just create a random id to make it easier to run tests
const id = Math.random().toString(36).substring(7);
await page.getByPlaceholder('Story export name').fill('Test-' + id);
- await page.getByRole('button', { name: 'Create' }).click();
+ await page.getByRole('button', { exact: true, name: 'Create' }).click();
await expect(page.getByText('You just added your first')).toBeVisible();
await page.getByLabel('Last').click();
await page.getByRole('checkbox', { name: 'Application UI' }).check();
await page.getByRole('checkbox', { name: 'Functional testing' }).check();
- await page.getByRole('combobox').selectOption('Web Search');
+ await page.locator('#referrer').selectOption('Web Search');
await page.getByRole('button', { name: 'Submit' }).click();
await expect(
diff --git a/code/e2e-tests/addon-toolbars.spec.ts b/code/e2e-tests/addon-toolbars.spec.ts
index 8a0e135eb927..e47ee783a744 100644
--- a/code/e2e-tests/addon-toolbars.spec.ts
+++ b/code/e2e-tests/addon-toolbars.spec.ts
@@ -16,7 +16,7 @@ test.describe('addon-toolbars', () => {
// Click on viewport button and select spanish
await sbPage.navigateToStory('core/toolbars/globals', 'basic');
- await sbPage.selectToolbar('[title="Internationalization locale"]', '#list-item-es');
+ await sbPage.selectToolbar('[aria-label^="Internationalization locale"]', 'text=/Español/');
// Check that spanish is selected
await expect(sbPage.previewRoot()).toContainText('Hola');
@@ -31,7 +31,7 @@ test.describe('addon-toolbars', () => {
await sbPage.navigateToStory('core/toolbars/globals', 'override-locale');
await expect(sbPage.previewRoot()).toContainText('안녕하세요');
- const button = sbPage.page.getByTitle('Internationalization locale');
+ const button = sbPage.page.getByLabel('Internationalization locale');
await expect(button).toBeDisabled();
});
});
diff --git a/code/e2e-tests/addon-viewport.spec.ts b/code/e2e-tests/addon-viewport.spec.ts
index c0ad4d1074c9..ff8450c3905d 100644
--- a/code/e2e-tests/addon-viewport.spec.ts
+++ b/code/e2e-tests/addon-viewport.spec.ts
@@ -16,7 +16,7 @@ test.describe('addon-viewport', () => {
// Click on viewport button and select small mobile
await sbPage.navigateToStory('example/button', 'primary');
- await sbPage.selectToolbar('[title="Change the size of the preview"]', '#list-item-mobile1');
+ await sbPage.selectToolbar('[aria-label="Viewport size"]', 'text=/Small mobile/');
// Check that Button story is still displayed
await expect(sbPage.previewRoot()).toContainText('Button');
@@ -32,7 +32,7 @@ test.describe('addon-viewport', () => {
const originalDimensions = await sbPage.getCanvasBodyElement().boundingBox();
expect(originalDimensions?.width).toBeDefined();
- await sbPage.selectToolbar('[title="Change the size of the preview"]', '#list-item-mobile1');
+ await sbPage.selectToolbar('[aria-label="Viewport size"]', 'text=/Small mobile/');
// Measure the adjusted dimensions of previewRoot after clicking the mobile item.
const adjustedDimensions = await sbPage.getCanvasBodyElement().boundingBox();
@@ -52,7 +52,7 @@ test.describe('addon-viewport', () => {
const originalDimensions = await sbPage.getCanvasBodyElement().boundingBox();
expect(originalDimensions?.width).toBeDefined();
- const toolbar = page.getByTitle('Change the size of the preview');
+ const toolbar = page.getByLabel('Viewport size');
await expect(toolbar).toBeDisabled();
});
diff --git a/code/e2e-tests/component-tests.spec.ts b/code/e2e-tests/component-tests.spec.ts
index 0af1a3f08b09..c60fd0438abe 100644
--- a/code/e2e-tests/component-tests.spec.ts
+++ b/code/e2e-tests/component-tests.spec.ts
@@ -31,7 +31,7 @@ test.describe('interactions', () => {
const welcome = sbPage.previewRoot().locator('.welcome');
await expect(welcome).toContainText('Welcome, Jane Doe!', { timeout: 50000 });
- const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel');
+ const interactionsTab = page.getByRole('tab', { name: 'Interactions' });
await expect(interactionsTab).toContainText(/(\d)/);
await expect(interactionsTab).toBeVisible();
@@ -64,12 +64,12 @@ test.describe('interactions', () => {
const formInput = sbPage.previewRoot().locator('#interaction-test-form input');
await expect(formInput).toHaveValue('final value', { timeout: 50000 });
- const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel');
+ const interactionsTab = page.getByRole('tab', { name: 'Interactions' });
await expect(interactionsTab.getByText('3')).toBeVisible();
await expect(interactionsTab).toBeVisible();
const panel = sbPage.panelContent();
- const runStatusBadge = panel.locator('[aria-label="Story status"]');
+ const runStatusBadge = panel.locator('[aria-label^="Story status:"]');
await expect(runStatusBadge).toContainText(/Pass/);
await expect(panel).toContainText(/"initial value"/);
await expect(panel).toContainText(/clear/);
@@ -110,7 +110,7 @@ test.describe('interactions', () => {
await expect(interactionsTab.getByText('3')).toBeVisible();
// Test remount state (from toolbar) - Interactions have rerun, count is correct and values are as expected
- const remountComponentButton = page.locator('[title="Remount component"]');
+ const remountComponentButton = page.locator('[aria-label="Reload story"]');
await remountComponentButton.click();
await interactionsRow.first().isVisible();
@@ -173,7 +173,7 @@ test.describe('test function', () => {
const welcome = sbPage.previewRoot().locator('button');
await expect(welcome).toContainText('Arg from story', { timeout: 50000 });
- const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel');
+ const interactionsTab = page.getByRole('tab', { name: 'Interactions' });
await expect(interactionsTab).toContainText(/(\d)/);
await expect(interactionsTab).toBeVisible();
diff --git a/code/e2e-tests/framework-nextjs.spec.ts b/code/e2e-tests/framework-nextjs.spec.ts
index 9e426e26709b..a8723a708c11 100644
--- a/code/e2e-tests/framework-nextjs.spec.ts
+++ b/code/e2e-tests/framework-nextjs.spec.ts
@@ -63,7 +63,7 @@ test.describe('Next.js', () => {
await button.click();
await sbPage.viewAddonPanel('Actions');
- const logItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const logItem = page.locator('#storybook-panel-root [role="tabpanel"]', {
hasText: `useRouter().${action}`,
});
await expect(logItem).toBeVisible();
@@ -95,7 +95,7 @@ test.describe('Next.js', () => {
await button.click();
await sbPage.viewAddonPanel('Actions');
- const logItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const logItem = page.locator('#storybook-panel-root [role="tabpanel"]', {
hasText: `useRouter().${action}`,
});
await expect(logItem).toBeVisible();
diff --git a/code/e2e-tests/framework-svelte.spec.ts b/code/e2e-tests/framework-svelte.spec.ts
index 7dae52e33eb2..00993129b27e 100644
--- a/code/e2e-tests/framework-svelte.spec.ts
+++ b/code/e2e-tests/framework-svelte.spec.ts
@@ -38,12 +38,12 @@ test.describe('Svelte', () => {
await link.click();
await sbPage.viewAddonPanel('Actions');
- const basicLogItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const basicLogItem = page.locator('#storybook-panel-root [role="tabpanel"]', {
hasText: `/basic-href`,
});
await expect(basicLogItem).toBeVisible();
- const complexLogItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const complexLogItem = page.locator('#storybook-panel-root [role="tabpanel"]', {
hasText: `/deep/nested`,
});
await expect(complexLogItem).toBeVisible();
@@ -62,7 +62,7 @@ test.describe('Svelte', () => {
const goto = root.locator('button', { hasText: 'goto' });
await goto.click();
- const gotoLogItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const gotoLogItem = page.locator('#storybook-panel-root [role="tabpanel"]', {
hasText: `/storybook-goto`,
});
await expect(gotoLogItem).toBeVisible();
@@ -70,7 +70,7 @@ test.describe('Svelte', () => {
const invalidate = root.getByRole('button', { name: 'invalidate', exact: true });
await invalidate.click();
- const invalidateLogItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const invalidateLogItem = page.locator('#storybook-panel-root [role="tabpanel"]', {
hasText: `/storybook-invalidate`,
});
await expect(invalidateLogItem).toBeVisible();
@@ -78,7 +78,7 @@ test.describe('Svelte', () => {
const invalidateAll = root.getByRole('button', { name: 'invalidateAll' });
await invalidateAll.click();
- const invalidateAllLogItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const invalidateAllLogItem = page.locator('#storybook-panel-root [role="tabpanel"]', {
hasText: `"invalidateAll"`,
});
await expect(invalidateAllLogItem).toBeVisible();
@@ -86,7 +86,7 @@ test.describe('Svelte', () => {
const replaceState = root.getByRole('button', { name: 'replaceState' });
await replaceState.click();
- const replaceStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const replaceStateLogItem = page.locator('#storybook-panel-root [role="tabpanel"]', {
hasText: `/storybook-replace-state`,
});
await expect(replaceStateLogItem).toBeVisible();
@@ -94,7 +94,7 @@ test.describe('Svelte', () => {
const pushState = root.getByRole('button', { name: 'pushState' });
await pushState.click();
- const pushStateLogItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const pushStateLogItem = page.locator('#storybook-panel-root [role="tabpanel"]', {
hasText: `/storybook-push-state`,
});
await expect(pushStateLogItem).toBeVisible();
diff --git a/code/e2e-tests/manager.spec.ts b/code/e2e-tests/manager.spec.ts
index c0b1e3d15a41..d523416f915b 100644
--- a/code/e2e-tests/manager.spec.ts
+++ b/code/e2e-tests/manager.spec.ts
@@ -21,17 +21,17 @@ test.describe('Manager UI', () => {
await page.locator('[aria-label="Settings"]').click();
// should only hide if pressing Escape, and not other keyboard inputs
- await expect(page.getByTestId('tooltip')).toBeVisible();
+ await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('A');
- await expect(page.getByTestId('tooltip')).toBeVisible();
+ await expect(page.getByRole('dialog')).toBeVisible();
await page.keyboard.press('Escape');
- await expect(page.getByTestId('tooltip')).toBeHidden();
+ await expect(page.getByRole('dialog')).toBeHidden();
// should also hide if clicking anywhere outside the tooltip
await page.locator('[aria-label="Settings"]').click();
- await expect(page.getByTestId('tooltip')).toBeVisible();
+ await expect(page.getByRole('dialog')).toBeVisible();
await page.click('body');
- await expect(page.getByTestId('tooltip')).toBeHidden();
+ await expect(page.getByRole('dialog')).toBeHidden();
});
test('Sidebar toggling', async ({ page }) => {
@@ -63,9 +63,10 @@ test.describe('Manager UI', () => {
// Context menu should contain open in editor for component node
await page.locator('[data-item-id="example-button"]').hover();
await page
- .locator('[data-item-id="example-button"] div[data-testid="context-menu"] button')
+ .locator('[data-item-id="example-button"]')
+ .getByRole('button', { name: 'Open context menu' })
.click();
- const sidebarContextMenu = page.getByTestId('tooltip');
+ const sidebarContextMenu = page.getByRole('dialog');
await expect(
sidebarContextMenu.getByRole('button', { name: /open in editor/i })
).toBeVisible();
@@ -74,23 +75,25 @@ test.describe('Manager UI', () => {
// Context menu should contain open in editor for docs node
await page.locator('[data-item-id="example-button--docs"]').hover();
await page
- .locator('[data-item-id="example-button--docs"] div[data-testid="context-menu"] button')
+ .locator('[data-item-id="example-button--docs"]')
+ .getByRole('button', { name: 'Open context menu' })
.click();
await expect(
- page.getByTestId('tooltip').getByRole('button', { name: /open in editor/i })
+ page.getByRole('dialog').getByRole('button', { name: /open in editor/i })
).toBeVisible();
await page.click('body');
// Context menu should contain open in editor and copy story name for story node
await page.locator('[data-item-id="example-button--primary"]').hover();
await page
- .locator('[data-item-id="example-button--primary"] div[data-testid="context-menu"] button')
+ .locator('[data-item-id="example-button--primary"]')
+ .getByRole('button', { name: 'Open context menu' })
.click();
await expect(
- page.getByTestId('tooltip').getByRole('button', { name: /open in editor/i })
+ page.getByRole('dialog').getByRole('button', { name: /open in editor/i })
).toBeVisible();
await page
- .getByTestId('tooltip')
+ .getByRole('dialog')
.getByRole('button', { name: /copy story name/i })
.click();
@@ -128,31 +131,31 @@ test.describe('Manager UI', () => {
test('Toolbar toggling', async ({ page }) => {
const sbPage = new SbPage(page, expect);
- const expectToolbarVisibility = async (visible: boolean) => {
- await expect(async () => {
- const toolbar = sbPage.page.locator(`[data-test-id="sb-preview-toolbar"]`);
- const marginTop = await toolbar.evaluate(
- (element) => window.getComputedStyle(element).marginTop
- );
- expect(marginTop).toBe(visible ? '0px' : '-40px');
- }).toPass({ intervals: [400] });
+ const expectToolbarToBeVisible = async () => {
+ const toolbar = page.getByTestId('sb-preview-toolbar').getByRole('toolbar');
+ await expect(toolbar).toBeVisible();
};
- await expectToolbarVisibility(true);
+ const expectToolbarToNotExist = async () => {
+ const toolbar = page.getByTestId('sb-preview-toolbar').getByRole('toolbar');
+ await expect(toolbar).toBeHidden();
+ };
+
+ await expectToolbarToBeVisible();
// toggle with keyboard shortcut
await sbPage.page.locator('html').press('Alt+t');
- await expectToolbarVisibility(false);
+ await expectToolbarToNotExist();
await sbPage.page.locator('html').press('Alt+t');
- await expectToolbarVisibility(true);
+ await expectToolbarToBeVisible();
// toggle with menu item
await sbPage.page.locator('[aria-label="Settings"]').click();
await sbPage.page.locator('#list-item-T').click();
- await expectToolbarVisibility(false);
+ await expectToolbarToNotExist();
await sbPage.page.locator('[aria-label="Settings"]').click();
await sbPage.page.locator('#list-item-T').click();
- await expectToolbarVisibility(true);
+ await expectToolbarToBeVisible();
});
test.describe('Panel', () => {
@@ -190,8 +193,8 @@ test.describe('Manager UI', () => {
await sbPage.page.locator('#list-item-A').click();
await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden();
- // toggle with "show addons" button
- await sbPage.page.locator('[aria-label="Show addons"]').click();
+ // toggle with "Show addon panel" button
+ await sbPage.page.locator('[aria-label="Show addon panel"]').click();
await expect(sbPage.page.locator('#storybook-panel-root')).toBeVisible();
});
@@ -247,19 +250,19 @@ test.describe('Manager UI', () => {
await expect(sbPage.page.locator('#storybook-panel-root')).toBeVisible();
await expect(sbPage.page.locator('.sidebar-container')).toBeVisible();
- await sbPage.page.locator('[aria-label="Go full screen"]').click();
+ await sbPage.page.locator('[aria-label="Enter full screen"]').click();
await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden();
await expect(sbPage.page.locator('.sidebar-container')).toBeHidden();
// go fullscreen when sidebar is shown but panel is hidden
await sbPage.page.locator('[aria-label="Show sidebar"]').click();
- await sbPage.page.locator('[aria-label="Go full screen"]').click();
+ await sbPage.page.locator('[aria-label="Enter full screen"]').click();
await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden();
await expect(sbPage.page.locator('.sidebar-container')).toBeHidden();
// go fullscreen when panel is shown but sidebar is hidden
- await sbPage.page.locator('[aria-label="Show addons"]').click();
- await sbPage.page.locator('[aria-label="Go full screen"]').click();
+ await sbPage.page.locator('[aria-label="Show addon panel"]').click();
+ await sbPage.page.locator('[aria-label="Enter full screen"]').click();
await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden();
await expect(sbPage.page.locator('.sidebar-container')).toBeHidden();
});
@@ -273,7 +276,7 @@ test.describe('Manager UI', () => {
await expect(sbPage.page.locator('#storybook-panel-root')).toBeHidden();
- await sbPage.page.locator('[title="Close settings page"]').click();
+ await sbPage.page.locator('[aria-label="Close settings page"]').click();
expect(sbPage.page.url()).not.toContain('/settings/about');
});
});
@@ -326,20 +329,20 @@ test.describe('Manager UI', () => {
// panel is closed
await expect(mobileNavigationHeading).toHaveText('Example/Button/Secondary');
- await expect(sbPage.page.locator('#tabbutton-addon-controls')).toBeHidden();
+ await expect(sbPage.page.getByRole('tab', { name: 'Controls' })).toBeHidden();
// open panel
await sbPage.page.locator('[aria-label="Open addon panel"]').click();
// panel is open
- await expect(sbPage.page.locator('#tabbutton-addon-controls')).toBeVisible();
+ await expect(sbPage.page.getByRole('tab', { name: 'Controls' })).toBeVisible();
// close panel
- await sbPage.page.locator('[aria-label="Close addon panel"]').click();
+ await sbPage.page.getByRole('button', { name: 'Close addon panel' }).click();
// panel is closed
await expect(mobileNavigationHeading).toHaveText('Example/Button/Secondary');
- await expect(sbPage.page.locator('#tabbutton-addon-controls')).toBeHidden();
+ await expect(sbPage.page.getByRole('tab', { name: 'Controls' })).toBeHidden();
});
});
});
diff --git a/code/e2e-tests/module-mocking.spec.ts b/code/e2e-tests/module-mocking.spec.ts
index a6d8d39bd437..6173744726fb 100644
--- a/code/e2e-tests/module-mocking.spec.ts
+++ b/code/e2e-tests/module-mocking.spec.ts
@@ -18,8 +18,8 @@ test.describe('module-mocking', () => {
await sbPage.navigateToStory('core/order-of-hooks', 'order-of-hooks');
await sbPage.viewAddonPanel('Actions');
- const logItem = page.locator('#storybook-panel-root #panel-tab-content');
- await expect(logItem).toBeVisible();
+ const panel = sbPage.panelContent();
+ await expect(panel).toBeVisible();
const expectedTexts = [
'1 - [from loaders]',
@@ -35,10 +35,10 @@ test.describe('module-mocking', () => {
// Collect all logs in the panel but only check the order of the logs
// we care about, disregarding any other logs that could appear in between
- const logItemsCount = await logItem.locator('li').count();
+ const logItemsCount = await panel.locator('li').count();
const actualTexts = [];
for (let i = 0; i < logItemsCount; i++) {
- actualTexts.push(await logItem.locator(`li >> nth=${i}`).innerText());
+ actualTexts.push(await panel.locator(`li >> nth=${i}`).innerText());
}
let lastMatchIndex = -1;
@@ -60,7 +60,7 @@ test.describe('module-mocking', () => {
await sbPage.navigateToStory('core/module-mocking', 'basic');
await sbPage.viewAddonPanel('Actions');
- const logItem = page.locator('#storybook-panel-root #panel-tab-content', {
+ const logItem = sbPage.panelContent().filter({
hasText: 'foo: []',
});
await expect(logItem).toBeVisible();
diff --git a/code/e2e-tests/preview-api.spec.ts b/code/e2e-tests/preview-api.spec.ts
index 3e3b9947e1b5..5022f3e003b4 100644
--- a/code/e2e-tests/preview-api.spec.ts
+++ b/code/e2e-tests/preview-api.spec.ts
@@ -27,10 +27,10 @@ test.describe('preview-api', () => {
// wait for the play function to complete
await sbPage.viewAddonPanel('Interactions');
- const interactionsTab = page.locator('#tabbutton-storybook-interactions-panel');
+ const interactionsTab = page.getByRole('tab', { name: 'Interactions' });
await expect(interactionsTab).toBeVisible();
const panel = sbPage.panelContent();
- const runStatusBadge = panel.locator('[aria-label="Story status"]');
+ const runStatusBadge = panel.locator('[aria-label^="Story status:"]');
await expect(runStatusBadge).toContainText(/Pass/);
// click outside, to remove focus from the input of the story, then press S to toggle sidebar
@@ -82,9 +82,9 @@ test.describe('preview-api', () => {
await expect(root.getByText('Loaded. Click me')).toBeVisible();
- await sbPage.page.getByRole('button', { name: 'Remount component' }).click();
+ await sbPage.page.getByRole('button', { name: 'Reload story' }).click();
await wait(200);
- await sbPage.page.getByRole('button', { name: 'Remount component' }).click();
+ await sbPage.page.getByRole('button', { name: 'Reload story' }).click();
// the loading spinner indicates the iframe is being fully reloaded
await expect(sbPage.previewIframe().locator('.sb-preparing-story > .sb-loader')).toBeVisible();
diff --git a/code/e2e-tests/tags.spec.ts b/code/e2e-tests/tags.spec.ts
index 3f59b9874218..6132b0169ce8 100644
--- a/code/e2e-tests/tags.spec.ts
+++ b/code/e2e-tests/tags.spec.ts
@@ -136,13 +136,13 @@ test.describe('tags', () => {
await expect(page.locator('#storybook-explorer-menu')).toBeVisible();
// Open Tag filters tooltip
- await page.locator('[title="Tag filters"]').click();
- const tooltip = page.locator('[data-testid="tooltip"]');
- await expect(tooltip).toBeVisible();
+ await page.locator('[aria-label="Tag filters"]').click();
+ const tagFilterPopover = page.getByRole('dialog', { name: 'Tag filters' });
+ await expect(tagFilterPopover).toBeVisible();
// No checkbox selected by default and "Select all tags" is shown
- await expect(tooltip.locator('#select-all')).toBeVisible();
- await expect(tooltip.locator('input[type="checkbox"]:checked')).toHaveCount(0);
+ await expect(tagFilterPopover.locator('#select-all')).toBeVisible();
+ await expect(tagFilterPopover.locator('input[type="checkbox"]:checked')).toHaveCount(0);
// Select the dev-only tag
await page.getByText('dev-only', { exact: true }).click();
@@ -152,11 +152,11 @@ test.describe('tags', () => {
await expect(stories).toHaveCount(1);
// Clear selection
- await expect(tooltip.locator('#deselect-all')).toBeVisible();
- await tooltip.locator('#deselect-all').click();
+ await expect(tagFilterPopover.locator('#deselect-all')).toBeVisible();
+ await tagFilterPopover.locator('#deselect-all').click();
// Checkboxes are not selected anymore
- await expect(tooltip.locator('input[type="checkbox"]:checked')).toHaveCount(0);
+ await expect(tagFilterPopover.locator('input[type="checkbox"]:checked')).toHaveCount(0);
});
});
});
diff --git a/code/e2e-tests/util.ts b/code/e2e-tests/util.ts
index 3f602132d0cf..e3d91c949ef1 100644
--- a/code/e2e-tests/util.ts
+++ b/code/e2e-tests/util.ts
@@ -147,6 +147,16 @@ export class SbPage {
await this.waitForStoryLoaded();
}
+ /**
+ * We have stories with modals set to auto-open (e.g. startOpen color control). This helper closes
+ * them to free scroll and keyboard focus traps.
+ */
+ async closeAnyPendingModal() {
+ const popover = this.page.locator('[role="dialog"]');
+ await this.page.keyboard.press('Escape');
+ await popover.waitFor({ state: 'hidden' });
+ }
+
previewIframe() {
return this.page.frameLocator('#storybook-preview-iframe');
}
@@ -157,11 +167,11 @@ export class SbPage {
}
panelContent() {
- return this.page.locator('#storybook-panel-root #panel-tab-content > div:not([hidden])');
+ return this.page.locator('#storybook-panel-root').getByRole('tabpanel');
}
async viewAddonPanel(name: string) {
- const tabs = this.page.locator('[role=tablist] button[role=tab]');
+ const tabs = this.page.locator('[role=tablist] div[role=tab]');
const tab = tabs.locator(`text=/^${name}/`);
await tab.click();
}
@@ -181,8 +191,9 @@ export class SbPage {
}
async openTagsFilter() {
- const tagFiltersButton = this.page.locator('[title="Tag filters"]');
- const tooltip = this.page.locator('[data-testid="tooltip"]');
+ const tagFiltersButton = this.page.locator('[aria-label="Tag filters"]');
+ // FIXME: we might want to strengthen this locator with an aria-label or testid on the dialog.
+ const tooltip = this.page.locator('[role="dialog"]');
const isTooltipVisible = await tooltip.isVisible();
if (!isTooltipVisible) {
diff --git a/code/frameworks/angular/package.json b/code/frameworks/angular/package.json
index 00ea08fcd498..086d378e7f61 100644
--- a/code/frameworks/angular/package.json
+++ b/code/frameworks/angular/package.json
@@ -122,5 +122,5 @@
"access": "public"
},
"builders": "builders.json",
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/ember/package.json b/code/frameworks/ember/package.json
index 1245cd0dcab4..8020963601a7 100644
--- a/code/frameworks/ember/package.json
+++ b/code/frameworks/ember/package.json
@@ -71,5 +71,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/html-vite/package.json b/code/frameworks/html-vite/package.json
index 607b1a95ee11..714f323ff9d4 100644
--- a/code/frameworks/html-vite/package.json
+++ b/code/frameworks/html-vite/package.json
@@ -63,5 +63,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/nextjs-vite/package.json b/code/frameworks/nextjs-vite/package.json
index 8952df4753f4..6182cd67509f 100644
--- a/code/frameworks/nextjs-vite/package.json
+++ b/code/frameworks/nextjs-vite/package.json
@@ -108,5 +108,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/nextjs/package.json b/code/frameworks/nextjs/package.json
index 7556682d5bd4..b14e1a96f0ba 100644
--- a/code/frameworks/nextjs/package.json
+++ b/code/frameworks/nextjs/package.json
@@ -144,5 +144,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/preact-vite/package.json b/code/frameworks/preact-vite/package.json
index 83925a96d9b5..1812fe74c8f3 100644
--- a/code/frameworks/preact-vite/package.json
+++ b/code/frameworks/preact-vite/package.json
@@ -66,5 +66,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/react-native-web-vite/package.json b/code/frameworks/react-native-web-vite/package.json
index 6b7a393aad05..f51d8691dca1 100644
--- a/code/frameworks/react-native-web-vite/package.json
+++ b/code/frameworks/react-native-web-vite/package.json
@@ -74,5 +74,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/react-vite/package.json b/code/frameworks/react-vite/package.json
index b778d09ad5da..8dbae3f9387c 100644
--- a/code/frameworks/react-vite/package.json
+++ b/code/frameworks/react-vite/package.json
@@ -77,5 +77,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/react-webpack5/package.json b/code/frameworks/react-webpack5/package.json
index afd8e559bb55..a547c241f634 100644
--- a/code/frameworks/react-webpack5/package.json
+++ b/code/frameworks/react-webpack5/package.json
@@ -71,5 +71,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/server-webpack5/package.json b/code/frameworks/server-webpack5/package.json
index de3291089a24..a3535b7793d5 100644
--- a/code/frameworks/server-webpack5/package.json
+++ b/code/frameworks/server-webpack5/package.json
@@ -62,5 +62,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/svelte-vite/package.json b/code/frameworks/svelte-vite/package.json
index 8f300c77dc87..62b21dbb9771 100644
--- a/code/frameworks/svelte-vite/package.json
+++ b/code/frameworks/svelte-vite/package.json
@@ -76,5 +76,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/sveltekit/package.json b/code/frameworks/sveltekit/package.json
index 6d7f243559b1..cfa34879f89c 100644
--- a/code/frameworks/sveltekit/package.json
+++ b/code/frameworks/sveltekit/package.json
@@ -76,5 +76,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/vue3-vite/package.json b/code/frameworks/vue3-vite/package.json
index 63f3d9c9b05e..75fb058f1c24 100644
--- a/code/frameworks/vue3-vite/package.json
+++ b/code/frameworks/vue3-vite/package.json
@@ -70,5 +70,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/frameworks/web-components-vite/package.json b/code/frameworks/web-components-vite/package.json
index e5b39ba00e32..f82ec3acb0ee 100644
--- a/code/frameworks/web-components-vite/package.json
+++ b/code/frameworks/web-components-vite/package.json
@@ -65,5 +65,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/lib/cli-sb/package.json b/code/lib/cli-sb/package.json
index a7a440482d05..b407e715c1a0 100644
--- a/code/lib/cli-sb/package.json
+++ b/code/lib/cli-sb/package.json
@@ -31,5 +31,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/lib/cli-storybook/package.json b/code/lib/cli-storybook/package.json
index 777a6d00b933..d167c281617c 100644
--- a/code/lib/cli-storybook/package.json
+++ b/code/lib/cli-storybook/package.json
@@ -72,5 +72,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/lib/codemod/package.json b/code/lib/codemod/package.json
index f2fff7557f0f..a0fdc9e5d8bb 100644
--- a/code/lib/codemod/package.json
+++ b/code/lib/codemod/package.json
@@ -62,5 +62,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/lib/core-webpack/package.json b/code/lib/core-webpack/package.json
index 8050d39bbca3..0b65bcfa4c35 100644
--- a/code/lib/core-webpack/package.json
+++ b/code/lib/core-webpack/package.json
@@ -53,5 +53,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/lib/create-storybook/package.json b/code/lib/create-storybook/package.json
index 1cafe4327b34..c985e29ed3ae 100644
--- a/code/lib/create-storybook/package.json
+++ b/code/lib/create-storybook/package.json
@@ -64,5 +64,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/lib/csf-plugin/package.json b/code/lib/csf-plugin/package.json
index 4b35d85d4b9c..108ade1795fd 100644
--- a/code/lib/csf-plugin/package.json
+++ b/code/lib/csf-plugin/package.json
@@ -70,5 +70,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/lib/eslint-plugin/package.json b/code/lib/eslint-plugin/package.json
index 3930563f222f..9b30c92da765 100644
--- a/code/lib/eslint-plugin/package.json
+++ b/code/lib/eslint-plugin/package.json
@@ -68,5 +68,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/lib/eslint-plugin/scripts/update-lib-flat-configs.ts b/code/lib/eslint-plugin/scripts/update-lib-flat-configs.ts
index e36b5193e2b3..f011c12cc18e 100644
--- a/code/lib/eslint-plugin/scripts/update-lib-flat-configs.ts
+++ b/code/lib/eslint-plugin/scripts/update-lib-flat-configs.ts
@@ -75,7 +75,7 @@ const FLAT_CONFIG_DIR = path.resolve(import.meta.dirname, '../src/configs/flat')
export async function update() {
// setup config directory
- await fs.mkdir(FLAT_CONFIG_DIR);
+ await fs.mkdir(FLAT_CONFIG_DIR, { recursive: true }).catch(() => {});
// Update/add rule files
await Promise.all(
diff --git a/code/lib/react-dom-shim/package.json b/code/lib/react-dom-shim/package.json
index fd70e785a7f8..b9992f027b62 100644
--- a/code/lib/react-dom-shim/package.json
+++ b/code/lib/react-dom-shim/package.json
@@ -52,5 +52,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/package.json b/code/package.json
index cbfe5958eb8d..d490a6f32a33 100644
--- a/code/package.json
+++ b/code/package.json
@@ -216,6 +216,9 @@
"vitest": "^3.2.4",
"wait-on": "^8.0.3"
},
+ "devDependencies": {
+ "react-popper-tooltip": "^4.4.2"
+ },
"dependenciesMeta": {
"ejs": {
"built": false
diff --git a/code/presets/create-react-app/package.json b/code/presets/create-react-app/package.json
index 366ed8c5189c..92cded1ff70f 100644
--- a/code/presets/create-react-app/package.json
+++ b/code/presets/create-react-app/package.json
@@ -54,5 +54,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/presets/react-webpack/package.json b/code/presets/react-webpack/package.json
index fb62aa76413c..acc5a6e0a19e 100644
--- a/code/presets/react-webpack/package.json
+++ b/code/presets/react-webpack/package.json
@@ -70,5 +70,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/presets/server-webpack/package.json b/code/presets/server-webpack/package.json
index 047ce4c753e0..a083c9d6e490 100644
--- a/code/presets/server-webpack/package.json
+++ b/code/presets/server-webpack/package.json
@@ -56,5 +56,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/renderers/html/package.json b/code/renderers/html/package.json
index ad6eba5fbeba..4fd967c01c35 100644
--- a/code/renderers/html/package.json
+++ b/code/renderers/html/package.json
@@ -59,5 +59,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/renderers/preact/package.json b/code/renderers/preact/package.json
index fdb94907bb6b..d0e98b89d12a 100644
--- a/code/renderers/preact/package.json
+++ b/code/renderers/preact/package.json
@@ -61,5 +61,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/renderers/react/package.json b/code/renderers/react/package.json
index 1154a5447bad..0843193aa751 100644
--- a/code/renderers/react/package.json
+++ b/code/renderers/react/package.json
@@ -92,5 +92,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/renderers/server/package.json b/code/renderers/server/package.json
index 3c9a2b5ce740..f16f01133968 100644
--- a/code/renderers/server/package.json
+++ b/code/renderers/server/package.json
@@ -58,5 +58,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/renderers/svelte/package.json b/code/renderers/svelte/package.json
index bf6f4f846f91..31659c97c6c2 100644
--- a/code/renderers/svelte/package.json
+++ b/code/renderers/svelte/package.json
@@ -74,5 +74,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/renderers/vue3/package.json b/code/renderers/vue3/package.json
index ec4e0af03d77..6b3b825183ad 100644
--- a/code/renderers/vue3/package.json
+++ b/code/renderers/vue3/package.json
@@ -69,5 +69,5 @@
"publishConfig": {
"access": "public"
},
- "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae16"
+ "gitHead": "a8e7fd8a655c69780bc20b9749d2699e45beae17"
}
diff --git a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/class-slots/component.vue b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/class-slots/component.vue
index a3491292f5d4..09f7cf908793 100644
--- a/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/class-slots/component.vue
+++ b/code/renderers/vue3/template/stories_vue3-vite-default-ts/component-meta/class-slots/component.vue
@@ -1,7 +1,7 @@