diff --git a/code/addons/a11y/src/manager.tsx b/code/addons/a11y/src/manager.tsx index 718766ab07b8..910e3d10050b 100644 --- a/code/addons/a11y/src/manager.tsx +++ b/code/addons/a11y/src/manager.tsx @@ -22,6 +22,7 @@ addons.register(ADDON_ID, (api) => { const totalNb = violationsNb + incompleteNb; return totalNb !== 0 ? `Accessibility (${totalNb})` : 'Accessibility'; }, + id: 'accessibility', type: types.PANEL, render: ({ active = true, key }) => ( diff --git a/code/addons/actions/src/manager.tsx b/code/addons/actions/src/manager.tsx index 147968d2860a..e47fc00b9d4e 100644 --- a/code/addons/actions/src/manager.tsx +++ b/code/addons/actions/src/manager.tsx @@ -1,27 +1,41 @@ -import React, { useState, useEffect } from 'react'; -import { addons, types } from '@storybook/manager-api'; +import React, { useState } from 'react'; +import { addons, types, useChannel } from '@storybook/manager-api'; import { STORY_CHANGED } from '@storybook/core-events'; import ActionLogger from './containers/ActionLogger'; import { ADDON_ID, EVENT_ID, PANEL_ID, PARAM_KEY } from './constants'; -addons.register(ADDON_ID, (api) => { - addons.addPanel(PANEL_ID, { - title() { - const [actionsCount, setActionsCount] = useState(0); - const onEvent = () => setActionsCount((previous) => previous + 1); - const onChange = () => setActionsCount(0); +function Title({ count }: { count: { current: number } }) { + // eslint-disable-next-line @typescript-eslint/naming-convention + const [_, setRerender] = useState(false); - useEffect(() => { - api.on(EVENT_ID, onEvent); - api.on(STORY_CHANGED, onChange); - return () => { - api.off(EVENT_ID, onEvent); - api.off(STORY_CHANGED, onChange); - }; - }); - const suffix = actionsCount === 0 ? '' : ` (${actionsCount})`; - return `Actions${suffix}`; + // Reactivity hack - force re-render on STORY_CHANGED and EVENT_ID events + useChannel({ + [EVENT_ID]: () => { + setRerender((r) => !r); }, + [STORY_CHANGED]: () => { + setRerender((r) => !r); + }, + }); + + const suffix = count.current === 0 ? '' : ` (${count.current})`; + return <>Actions{suffix}; +} + +addons.register(ADDON_ID, (api) => { + const countRef = { current: 0 }; + + api.on(STORY_CHANGED, (id) => { + countRef.current = 0; + }); + + api.on(EVENT_ID, () => { + countRef.current += 1; + }); + + addons.addPanel(PANEL_ID, { + title: , + id: 'actions', type: types.PANEL, render: ({ active, key }) => <ActionLogger key={key} api={api} active={!!active} />, paramKey: PARAM_KEY, diff --git a/code/addons/backgrounds/src/manager.tsx b/code/addons/backgrounds/src/manager.tsx index ecd36b0bb618..4cabc6011c84 100644 --- a/code/addons/backgrounds/src/manager.tsx +++ b/code/addons/backgrounds/src/manager.tsx @@ -8,6 +8,7 @@ import { GridSelector } from './containers/GridSelector'; addons.register(ADDON_ID, () => { addons.add(ADDON_ID, { title: 'Backgrounds', + id: 'backgrounds', type: types.TOOL, match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), render: () => ( diff --git a/code/addons/controls/src/manager.tsx b/code/addons/controls/src/manager.tsx index 1db002ae853f..eae78973b47a 100644 --- a/code/addons/controls/src/manager.tsx +++ b/code/addons/controls/src/manager.tsx @@ -4,16 +4,20 @@ import { AddonPanel } from '@storybook/components'; import { ControlsPanel } from './ControlsPanel'; import { ADDON_ID, PARAM_KEY } from './constants'; +function Title() { + const rows = useArgTypes(); + const controlsCount = Object.values(rows).filter( + (argType) => argType?.control && !argType?.table?.disable + ).length; + const suffix = controlsCount === 0 ? '' : ` (${controlsCount})`; + + return <>Controls{suffix}</>; +} + addons.register(ADDON_ID, (api: API) => { addons.addPanel(ADDON_ID, { - title() { - const rows = useArgTypes(); - const controlsCount = Object.values(rows).filter( - (argType) => argType?.control && !argType?.table?.disable - ).length; - const suffix = controlsCount === 0 ? '' : ` (${controlsCount})`; - return `Controls${suffix}`; - }, + title: <Title />, + id: 'controls', type: types.PANEL, paramKey: PARAM_KEY, render: ({ key, active }) => { diff --git a/code/addons/interactions/src/components/TabStatus.tsx b/code/addons/interactions/src/components/TabStatus.tsx index 66116985c7dd..bf6919612846 100644 --- a/code/addons/interactions/src/components/TabStatus.tsx +++ b/code/addons/interactions/src/components/TabStatus.tsx @@ -4,7 +4,7 @@ import ReactDOM from 'react-dom'; import { StatusIcon } from './StatusIcon'; export const TabStatus = ({ children }: { children: React.ReactChild }) => { - const container = global.document.getElementById('tabbutton-interactions'); + const container = global.document.getElementById('tabbutton-storybook-interactions-panel'); return container && ReactDOM.createPortal(children, container); }; diff --git a/code/addons/jest/src/manager.tsx b/code/addons/jest/src/manager.tsx index dbf43e9eb1f0..f4302002d6b7 100644 --- a/code/addons/jest/src/manager.tsx +++ b/code/addons/jest/src/manager.tsx @@ -7,6 +7,7 @@ import Panel from './components/Panel'; addons.register(ADDON_ID, (api) => { addons.addPanel(PANEL_ID, { title: 'Tests', + id: 'tests', render: ({ active, key }) => <Panel key={key} api={api} active={active} />, paramKey: PARAM_KEY, }); diff --git a/code/addons/measure/src/manager.tsx b/code/addons/measure/src/manager.tsx index 53bca4d0b716..f65075617ef3 100644 --- a/code/addons/measure/src/manager.tsx +++ b/code/addons/measure/src/manager.tsx @@ -7,6 +7,7 @@ import { Tool } from './Tool'; addons.register(ADDON_ID, () => { addons.add(TOOL_ID, { type: types.TOOL, + id: 'measure', title: 'Measure', match: ({ viewMode }) => viewMode === 'story', render: () => <Tool />, diff --git a/code/addons/outline/src/manager.tsx b/code/addons/outline/src/manager.tsx index 384ea24c07c6..d1852360356e 100644 --- a/code/addons/outline/src/manager.tsx +++ b/code/addons/outline/src/manager.tsx @@ -7,6 +7,7 @@ import { OutlineSelector } from './OutlineSelector'; addons.register(ADDON_ID, () => { addons.add(ADDON_ID, { title: 'Outline', + id: 'outline', type: types.TOOL, match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), render: () => <OutlineSelector />, diff --git a/code/addons/storysource/src/manager.tsx b/code/addons/storysource/src/manager.tsx index 5ca1794519ce..d4d63a704e3b 100644 --- a/code/addons/storysource/src/manager.tsx +++ b/code/addons/storysource/src/manager.tsx @@ -7,6 +7,7 @@ import { ADDON_ID, PANEL_ID } from './index'; addons.register(ADDON_ID, (api) => { addons.addPanel(PANEL_ID, { title: 'Code', + id: 'code', render: ({ active, key }) => (active ? <StoryPanel key={key} api={api} /> : null), paramKey: 'storysource', }); diff --git a/code/addons/toolbars/src/manager.tsx b/code/addons/toolbars/src/manager.tsx index c87d3fbf2d80..adfc9956bd7a 100644 --- a/code/addons/toolbars/src/manager.tsx +++ b/code/addons/toolbars/src/manager.tsx @@ -6,6 +6,7 @@ import { ADDON_ID } from './constants'; addons.register(ADDON_ID, () => addons.add(ADDON_ID, { title: ADDON_ID, + id: 'toolbar', type: types.TOOL, match: () => true, render: () => <ToolbarManager />, diff --git a/code/addons/viewport/src/manager.tsx b/code/addons/viewport/src/manager.tsx index e42e3fba40e1..602cd708a4ea 100644 --- a/code/addons/viewport/src/manager.tsx +++ b/code/addons/viewport/src/manager.tsx @@ -8,6 +8,7 @@ import { ViewportTool } from './Tool'; addons.register(ADDON_ID, () => { addons.add(ADDON_ID, { title: 'viewport / media-queries', + id: 'viewport', type: types.TOOL, match: ({ viewMode }) => viewMode === 'story', render: () => <ViewportTool />, diff --git a/code/e2e-tests/addon-interactions.spec.ts b/code/e2e-tests/addon-interactions.spec.ts index f7dc82ce8868..8e2c24b74484 100644 --- a/code/e2e-tests/addon-interactions.spec.ts +++ b/code/e2e-tests/addon-interactions.spec.ts @@ -29,7 +29,7 @@ test.describe('addon-interactions', () => { const welcome = await sbPage.previewRoot().locator('.welcome'); await expect(welcome).toContainText('Welcome, Jane Doe!'); - const interactionsTab = await page.locator('#tabbutton-interactions'); + const interactionsTab = await page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab).toContainText(/(1)/); await expect(interactionsTab).toBeVisible(); @@ -59,7 +59,7 @@ test.describe('addon-interactions', () => { const formInput = await sbPage.previewRoot().locator('#interaction-test-form input'); await expect(formInput).toHaveValue('final value'); - const interactionsTab = await page.locator('#tabbutton-interactions'); + const interactionsTab = await page.locator('#tabbutton-storybook-interactions-panel'); await expect(interactionsTab).toContainText(/(3)/); await expect(interactionsTab).toBeVisible(); diff --git a/code/lib/types/src/modules/addons.ts b/code/lib/types/src/modules/addons.ts index 95f289ee1e5a..0f38365451c3 100644 --- a/code/lib/types/src/modules/addons.ts +++ b/code/lib/types/src/modules/addons.ts @@ -295,8 +295,14 @@ export interface Addon_RenderOptions { key?: string; } +export type ReactJSXElement = { + type: any; + props: any; + key: any; +}; + export interface Addon_Type { - title: (() => string) | string; + title: (() => string) | string | ReactJSXElement; type?: Addon_Types; id?: string; route?: (routeOptions: RouterData) => string; diff --git a/code/lib/types/src/modules/api.ts b/code/lib/types/src/modules/api.ts index d9d2c54ec5de..b9ed52477190 100644 --- a/code/lib/types/src/modules/api.ts +++ b/code/lib/types/src/modules/api.ts @@ -7,7 +7,7 @@ import type { ViewMode } from './csf'; import type { DocsOptions } from './core-common'; import type { API_HashEntry, API_IndexHash } from './api-stories'; import type { SetStoriesStory, SetStoriesStoryData } from './channelApi'; -import type { Addon_Types } from './addons'; +import type { Addon_Type } from './addons'; import type { StoryIndex } from './storyIndex'; export type API_ViewMode = 'story' | 'info' | 'settings' | 'page' | undefined | string; @@ -30,17 +30,8 @@ export interface API_MatchOptions { path: string; } -export interface API_Addon { - title: string; - type?: Addon_Types; - id?: string; - route?: (routeOptions: API_RouteOptions) => string; - match?: (matchOptions: API_MatchOptions) => boolean; - render: (renderOptions: API_RenderOptions) => any; - paramKey?: string; - disabled?: boolean; - hidden?: boolean; -} +export type API_Addon = Addon_Type; + export interface API_Collection<T = API_Addon> { [key: string]: T; } diff --git a/code/ui/components/src/tabs/tabs.hooks.tsx b/code/ui/components/src/tabs/tabs.hooks.tsx index a3d506137937..5ee337ce3b71 100644 --- a/code/ui/components/src/tabs/tabs.hooks.tsx +++ b/code/ui/components/src/tabs/tabs.hooks.tsx @@ -98,10 +98,11 @@ export function useList(list: ChildrenList) { /> </AddonButton> </WithTooltip> - {invisibleList.map(({ title, id, color }) => { + {invisibleList.map(({ title, id, color }, index) => { + const indexId = `index-${index}`; return ( <TabButton - id={`tabbutton-${sanitize(title)}`} + id={`tabbutton-${sanitize(id) ?? indexId}`} style={{ visibility: 'hidden' }} aria-hidden tabIndex={-1} diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index 284725a759fd..89d95a11baec 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -146,10 +146,12 @@ export const Tabs: FC<TabsProps> = memo( <Wrapper absolute={absolute} bordered={bordered} id={htmlId}> <FlexBar scrollable={false} border backgroundColor={backgroundColor}> <TabBar style={{ whiteSpace: 'normal' }} ref={tabBarRef} role="tablist"> - {visibleList.map(({ title, id, active, color }) => { + {visibleList.map(({ title, id, active, color }, index) => { + const indexId = `index-${index}`; + return ( <TabButton - id={`tabbutton-${sanitize(title)}`} + id={`tabbutton-${sanitize(id) ?? indexId}`} ref={(ref: HTMLButtonElement) => { tabRefs.current.set(id, ref); }}