From e5ad678635d4c9ffc742b5853d8f48d2f95936ef Mon Sep 17 00:00:00 2001 From: Norbert de Langen Date: Mon, 3 Jul 2023 21:47:12 +0200 Subject: [PATCH 01/14] make tabs component more type correct, allow for FC as title --- code/lib/types/src/modules/addons.ts | 58 +++++++-- code/lib/types/src/modules/api.ts | 8 +- code/ui/components/src/tabs/tabs.helpers.tsx | 46 ++++--- code/ui/components/src/tabs/tabs.hooks.tsx | 10 +- code/ui/components/src/tabs/tabs.stories.tsx | 35 +++--- code/ui/components/src/tabs/tabs.tsx | 28 +++-- .../src/components/panel/panel.stories.tsx | 117 +++++++++++++++++- .../ui/manager/src/components/panel/panel.tsx | 22 ++-- 8 files changed, 246 insertions(+), 78 deletions(-) diff --git a/code/lib/types/src/modules/addons.ts b/code/lib/types/src/modules/addons.ts index f4085d06d170..613c8080ccd4 100644 --- a/code/lib/types/src/modules/addons.ts +++ b/code/lib/types/src/modules/addons.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import type { ReactElement, ReactNode, ValidationMap, WeakValidationMap } from 'react'; import type { RenderData as RouterData } from '../../../router/src/types'; import type { ThemeVars } from '../../../theming/src/types'; import type { @@ -20,7 +21,7 @@ import type { } from './csf'; import type { IndexEntry } from './storyIndex'; -export type Addon_Types = Addon_TypesEnum | string; +export type Addon_Types = Exclude; export interface Addon_ArgType extends InputType { defaultValue?: TArg; @@ -122,9 +123,10 @@ export interface Addon_AddStoryArgs { parameters: Parameters; } -export interface Addon_ClientApiAddon extends Addon_Type { +export type Addon_ClientApiAddon = Addon_Type & { apply: (a: Addon_StoryApi, b: any[]) => any; -} +}; + export interface Addon_ClientApiAddons { [key: string]: Addon_ClientApiAddon; } @@ -301,15 +303,19 @@ export interface Addon_RenderOptions { key?: string; } +/** + * @deprecated This type is deprecated and will be removed in 8.0. + */ export type ReactJSXElement = { type: any; props: any; key: any; }; -export interface Addon_Type { - title: (() => string) | string | ReactJSXElement; - type?: Addon_Types; +export type Addon_Type = Addon_BaseType; +export interface Addon_BaseType { + title: MyFC | string | ReactElement | ReactNode; + type: Addon_Types; id?: string; route?: (routeOptions: RouterData) => string; match?: (matchOptions: RouterData) => boolean; @@ -319,13 +325,23 @@ export interface Addon_Type { hidden?: boolean; } +// This is a copy of FC from react/index.d.ts, but has the PropsWithChildren type removed +// this is correct and more type strict, and future compatible with React.FC in React 18+ +interface MyFC

{ + (props: P, context?: any): ReactElement | null; + propTypes?: WeakValidationMap

| undefined; + contextTypes?: ValidationMap | undefined; + defaultProps?: Partial

| undefined; + displayName?: string | undefined; +} + export type Addon_Loader = (api: API) => void; export interface Addon_Loaders { [key: string]: Addon_Loader; } -export interface Addon_Collection { - [key: string]: Addon_Type; +export interface Addon_Collection { + [key: string]: T; } export interface Addon_Elements { [key: string]: Addon_Collection; @@ -342,10 +358,36 @@ export interface Addon_Config { } export enum Addon_TypesEnum { + /** + * This API is used to create a tab the toolbar above the canvas, This API might be removed in the future. + * @unstable + */ TAB = 'tab', + /** + * This adds panels to the addons side panel. + */ PANEL = 'panel', + /** + * This adds items in the toolbar above the canvas - on the right side. + */ TOOL = 'tool', + /** + * This adds items in the toolbar above the canvas - on the right side. + */ TOOLEXTRA = 'toolextra', + /** + * This adds wrapper components around the canvas/iframe component storybook renders. + * @unstable + */ PREVIEW = 'preview', + /** + * This adds pages that render instead of the canvas. + * DO NOT USE + */ + experimental_PAGE = 'main', + + /** + * @deprecated This property does nothing, and will be removed in Storybook 8.0. + */ NOTES_ELEMENT = 'notes-element', } diff --git a/code/lib/types/src/modules/api.ts b/code/lib/types/src/modules/api.ts index b20e3d0e120e..985e0e551bf1 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_Type } from './addons'; +import type { Addon_Collection, Addon_Type } from './addons'; import type { StoryIndex } from './storyIndex'; export type API_ViewMode = 'story' | 'info' | 'settings' | 'page' | undefined | string; @@ -32,11 +32,9 @@ export interface API_MatchOptions { export type API_Addon = Addon_Type; -export interface API_Collection { - [key: string]: T; -} +export type API_Collection = Addon_Collection; -export type API_Panels = API_Collection; +export type API_Panels = Addon_Collection; export type API_StateMerger = (input: S) => S; diff --git a/code/ui/components/src/tabs/tabs.helpers.tsx b/code/ui/components/src/tabs/tabs.helpers.tsx index b92d38733077..024765fd5cf3 100644 --- a/code/ui/components/src/tabs/tabs.helpers.tsx +++ b/code/ui/components/src/tabs/tabs.helpers.tsx @@ -1,6 +1,8 @@ import { styled } from '@storybook/theming'; -import type { ReactElement } from 'react'; +import type { FC, ReactChild, ReactElement, ReactNode } from 'react'; import React, { Children } from 'react'; +import type { Addon_RenderOptions } from '@storybook/types'; +import type { TabsProps } from './tabs'; export interface VisuallyHiddenProps { active?: boolean; @@ -10,25 +12,41 @@ export const VisuallyHidden = styled.div(({ active }) => active ? { display: 'block' } : { display: 'none' } ); -export const childrenToList = (children: any, selected: string) => +export const childrenToList = (children: TabsProps['children']) => Children.toArray(children).map( - ({ props: { title, id, color, children: childrenOfChild } }: ReactElement, index) => { - const content = Array.isArray(childrenOfChild) ? childrenOfChild[0] : childrenOfChild; + ({ + props: { title, id, color, children: childrenOfChild }, + }: ReactElement<{ + children: FC | ReactChild | null; + title: ReactChild | null | FC; + id: string; + color?: string; + }>) => { + const content: FC | ReactNode = Array.isArray(childrenOfChild) + ? childrenOfChild[0] + : childrenOfChild; + + const render: FC = ( + typeof content === 'function' + ? content + : ({ active, key }: any) => ( + + {content} + + ) + ) as FC; return { - active: selected ? id === selected : index === 0, title, id, - color, - render: - typeof content === 'function' - ? content - : ({ active, key }: any) => ( - - {content} - - ), + ...(color ? { color } : {}), + render, }; } ); export type ChildrenList = ReturnType; +export type ChildrenListComplete = Array< + ReturnType[0] & { + active: boolean; + } +>; diff --git a/code/ui/components/src/tabs/tabs.hooks.tsx b/code/ui/components/src/tabs/tabs.hooks.tsx index 41df6e93f44f..0afb81ea9186 100644 --- a/code/ui/components/src/tabs/tabs.hooks.tsx +++ b/code/ui/components/src/tabs/tabs.hooks.tsx @@ -5,7 +5,7 @@ import useResizeObserver from 'use-resize-observer'; import { TabButton } from '../bar/button'; import { TooltipLinkList } from '../tooltip/TooltipLinkList'; import { WithTooltip } from '../tooltip/WithTooltip'; -import type { ChildrenList } from './tabs.helpers'; +import type { ChildrenListComplete } from './tabs.helpers'; import type { Link } from '../tooltip/TooltipLinkList'; const CollapseIcon = styled.span<{ isActive: boolean }>(({ theme, isActive }) => ({ @@ -32,7 +32,7 @@ const AddonButton = styled(TabButton)<{ preActive: boolean }>(({ active, theme, `; }); -export function useList(list: ChildrenList) { +export function useList(list: ChildrenListComplete) { const tabBarRef = useRef(); const addonsRef = useRef(); const tabRefs = useRef(new Map()); @@ -41,8 +41,8 @@ export function useList(list: ChildrenList) { }); const [visibleList, setVisibleList] = useState(list); - const [invisibleList, setInvisibleList] = useState([]); - const previousList = useRef(list); + const [invisibleList, setInvisibleList] = useState([]); + const previousList = useRef(list); const AddonTab = useCallback( ({ @@ -134,7 +134,7 @@ export function useList(list: ChildrenList) { const { width: widthAddonsTab } = addonsRef.current.getBoundingClientRect(); const rightBorder = invisibleList.length ? x + width - widthAddonsTab : x + width; - const newVisibleList: ChildrenList = []; + const newVisibleList: ChildrenListComplete = []; let widthSum = 0; diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index 7a9abdb24130..6d6973befcf6 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -1,5 +1,4 @@ import { expect } from '@storybook/jest'; -import type { Key } from 'react'; import React, { Fragment } from 'react'; import { action } from '@storybook/addon-actions'; import { logger } from '@storybook/client-logger'; @@ -9,10 +8,11 @@ import { fireEvent, waitFor, screen, - getByText, userEvent, + findByText, } from '@storybook/testing-library'; import { Tabs, TabsState, TabWrapper } from './tabs'; +import type { ChildrenList } from './tabs.helpers'; const colours = Array.from(new Array(15), (val, index) => index).map((i) => Math.floor((1 / 15) * i * 16777215) @@ -41,13 +41,7 @@ function fibonacci(num: number, memo?: FibonacciMap): number { /* eslint-enable no-param-reassign */ } -interface Panels { - [key: string]: { - title: string; - color?: string; - render: ({ active, key }: { active: boolean; key: Key }) => JSX.Element; - }; -} +type Panels = Record>; const panels: Panels = { test1: { @@ -121,7 +115,7 @@ const panels: Panels = { const onSelect = action('onSelect'); const content = Object.entries(panels).map(([k, v]) => ( -

+
{v.render}
)); @@ -224,9 +218,8 @@ export const StatefulDynamicWithOpenTooltip = { await expect(canvas.getByRole('tab', { name: /Addons/ })).toBeInTheDocument(); }); - const addonsTab = await canvas.findByRole('tab', { name: /Addons/ }); - await waitFor(async () => { + const addonsTab = await canvas.findByRole('tab', { name: /Addons/ }); const tooltip = await screen.queryByTestId('tooltip'); if (!tooltip) { @@ -236,14 +229,14 @@ export const StatefulDynamicWithOpenTooltip = { if (!tooltip) { throw new Error('Tooltip not found'); } - }); - expect(screen.queryByTestId('tooltip')).toBeInTheDocument(); + await expect(screen.queryByTestId('tooltip')).toBeInTheDocument(); + }); }, render: (args) => ( {Object.entries(panels).map(([k, v]) => ( -
+
{v.render}
))} @@ -262,10 +255,12 @@ export const StatefulDynamicWithSelectedAddon = { play: async (context) => { await StatefulDynamicWithOpenTooltip.play(context); - const popperContainer = screen.getByTestId('tooltip'); - const tab4 = getByText(popperContainer, 'Tab title #4', {}); - fireEvent(tab4, new MouseEvent('click', { bubbles: true })); - await waitFor(() => screen.getByText('CONTENT 4')); + await waitFor(async () => { + const popperContainer = await screen.findByTestId('tooltip'); + const tab4 = await findByText(popperContainer, 'Tab title #4', {}); + fireEvent(tab4, new MouseEvent('click', { bubbles: true })); + await waitFor(() => screen.findByText('CONTENT 4')); + }); // reopen the tooltip await StatefulDynamicWithOpenTooltip.play(context); @@ -273,7 +268,7 @@ export const StatefulDynamicWithSelectedAddon = { render: (args) => ( {Object.entries(panels).map(([k, v]) => ( -
+
{v.render}
))} diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index 454a92376d83..b5bfe93e8894 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -1,8 +1,9 @@ -import type { FC, MouseEvent, ReactNode } from 'react'; +import type { FC, MouseEvent, ReactElement, ReactNode } from 'react'; import React, { useMemo, Component, Fragment, memo } from 'react'; import { styled } from '@storybook/theming'; import { sanitize } from '@storybook/csf'; +import type { Addon_RenderOptions } from '@storybook/types'; import { Placeholder } from '../placeholder/placeholder'; import { TabButton } from '../bar/button'; import { FlexBar } from '../bar/bar'; @@ -113,7 +114,10 @@ export const TabWrapper: FC = ({ active, render, children }) => export const panelProps = {}; export interface TabsProps { - children?: FuncChildren[] | ReactNode; + children?: ReactElement<{ + children: FC; + title: ReactNode | FC; + }>[]; id?: string; tools?: ReactNode; selected?: string; @@ -138,9 +142,13 @@ export const Tabs: FC = memo( id: htmlId, menuName, }) => { - const list = useMemo( - () => childrenToList(children, selected), - [children, selected] + const idList = childrenToList(children).map((i) => i.id); + const primeList = useMemo(() => childrenToList(children), [...idList]); + + const list = useMemo( + () => + primeList.map((i, index) => ({ ...i, active: selected ? i.id === selected : index === 0 })), + [selected, primeList] ); const { visibleList, tabBarRef, tabRefs, AddonTab } = useList(list); @@ -169,7 +177,7 @@ export const Tabs: FC = memo( }} role="tab" > - {title} + {typeof title === 'function' ? : title} </TabButton> ); })} @@ -178,7 +186,9 @@ export const Tabs: FC<TabsProps> = memo( {tools} </FlexBar> <Content id="panel-tab-content" bordered={bordered} absolute={absolute}> - {list.map(({ id, active, render }) => render({ key: id, active }))} + {list.map(({ id, active, render }) => { + return React.createElement(render, { key: id, active }, null); + })} </Content> </Wrapper> ) : ( @@ -199,10 +209,8 @@ Tabs.defaultProps = { menuName: 'Tabs', }; -type FuncChildren = ({ active }: { active: boolean }) => JSX.Element; - export interface TabsStateProps { - children: FuncChildren[] | ReactNode; + children: TabsProps['children']; initial: string; absolute: boolean; bordered: boolean; diff --git a/code/ui/manager/src/components/panel/panel.stories.tsx b/code/ui/manager/src/components/panel/panel.stories.tsx index 75a66c60942d..1b31eacd21e7 100644 --- a/code/ui/manager/src/components/panel/panel.stories.tsx +++ b/code/ui/manager/src/components/panel/panel.stories.tsx @@ -1,5 +1,7 @@ -import React, { useState } from 'react'; +import React, { useCallback, useRef, useState } from 'react'; import { action } from '@storybook/addon-actions'; +import { Badge, Icons, Spaced } from '@storybook/components'; +import { Addon_TypesEnum } from '@storybook/types'; import Panel from './panel'; import { panels, shortcuts } from '../layout/app.mockdata'; @@ -25,6 +27,119 @@ export const Default = () => { ); }; +export const JSXTitles = () => { + const [selectedPanel, setSelectedPanel] = useState('function-string'); + return ( + <Panel + absolute={false} + panels={{ + 'function-string': { + type: Addon_TypesEnum.PANEL, + title: () => 'Test 1', + render: ({ active, key }) => + active ? ( + <div id="test1" key={key}> + TEST as string + </div> + ) : null, + }, + 'function-jsx': { + type: Addon_TypesEnum.PANEL, + title: () => ( + <div> + <Spaced col={1}> + <div style={{ display: 'inline-block', verticalAlign: 'middle' }}>Test 1</div> + <Badge status="critical">4</Badge> + </Spaced> + </div> + ), + render: ({ active, key }) => + active ? ( + <div id="test1" key={key}> + TEST with label + </div> + ) : null, + }, + 'function-jsx-icon': { + type: Addon_TypesEnum.PANEL, + title: () => ( + <div> + <Spaced col={1}> + <div style={{ display: 'inline-block', verticalAlign: 'middle' }}>Alert!</div> + <Icons icon="bell" /> + </Spaced> + </div> + ), + render: ({ active, key }) => + active ? ( + <div id="test1" key={key}> + TEST with label + </div> + ) : null, + }, + 'function-jsx-state': { + type: Addon_TypesEnum.PANEL, + title: () => { + const MAX = 10; + const [count, setCount] = useState(0); + const timer = useRef(null); + + const startTimer = useCallback((event) => { + event.stopPropagation(); + if (timer.current) { + return; + } + timer.current = setInterval(() => { + setCount((c) => { + if (c === MAX) { + clearInterval(timer.current); + timer.current = null; + return c; + } + return c + 1; + }); + }, 1000); + }, []); + const stopTimer = useCallback((event) => { + event.stopPropagation(); + if (timer.current) { + clearInterval(timer.current); + timer.current = null; + } + }, []); + + return ( + <div + onMouseEnter={startTimer} + onMouseLeave={stopTimer} + onBlur={stopTimer} + tabIndex={-1} + > + <Spaced col={1}> + <div style={{ display: 'inline-block' }}>Hover over me!</div> + {count ? ( + <Badge status={count > 8 ? 'critical' : 'warning'}>{count}</Badge> + ) : null} + </Spaced> + </div> + ); + }, + render: ({ active, key }) => { + return active ? ( + <div id="test1" key={key}> + TEST with timer + </div> + ) : null; + }, + }, + }} + actions={{ onSelect: setSelectedPanel, toggleVisibility, togglePosition }} + selectedPanel={selectedPanel} + shortcuts={shortcuts} + /> + ); +}; + export const NoPanels = () => ( <Panel panels={{}} diff --git a/code/ui/manager/src/components/panel/panel.tsx b/code/ui/manager/src/components/panel/panel.tsx index 6519ee9d9219..90496e38b7a6 100644 --- a/code/ui/manager/src/components/panel/panel.tsx +++ b/code/ui/manager/src/components/panel/panel.tsx @@ -1,20 +1,16 @@ -import type { ReactElement } from 'react'; import React, { Component, Fragment } from 'react'; import { Tabs, Icons, IconButton } from '@storybook/components'; import type { State } from '@storybook/manager-api'; import { shortcutToHumanString } from '@storybook/manager-api'; +import type { Addon_Type } from '@storybook/types'; import useMediaQuery from '../hooks/useMedia'; export interface SafeTabProps { - title: (() => string) | string; + title: Addon_Type['title']; id: string; - children: ReactElement; + children: Addon_Type['render']; } -const SafeTabContent = React.memo<SafeTabProps>(function SafeTabContent({ children }) { - return children; -}); - class SafeTab extends Component<SafeTabProps, { hasError: boolean }> { constructor(props: SafeTabProps) { super(props); @@ -29,22 +25,18 @@ class SafeTab extends Component<SafeTabProps, { hasError: boolean }> { render() { const { hasError } = this.state; - const { children, title, id } = this.props; + const { children } = this.props; if (hasError) { return <h1>Something went wrong.</h1>; } - return ( - <SafeTabContent id={id} title={title}> - {children} - </SafeTabContent> - ); + return children; } } const AddonPanel = React.memo<{ selectedPanel?: string; actions: { onSelect: (id: string) => void } & Record<string, any>; - panels: Record<string, any>; + panels: Record<string, Addon_Type>; shortcuts: State['shortcuts']; panelPosition?: 'bottom' | 'right'; absolute?: boolean; @@ -89,7 +81,7 @@ const AddonPanel = React.memo<{ id="storybook-panel-root" > {Object.entries(panels).map(([k, v]) => ( - <SafeTab key={k} id={k} title={typeof v.title === 'function' ? v.title() : v.title}> + <SafeTab key={k} id={k} title={typeof v.title === 'function' ? <v.title /> : v.title}> {v.render} </SafeTab> ))} From 7cafd2fe7b386453ea7277cd3ae1b5780649efd1 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 3 Jul 2023 21:59:48 +0200 Subject: [PATCH 02/14] introduce Addon_BaseType continue adding type property to addons --- .../components/preview/preview.mockdata.tsx | 39 ++++++++++--------- .../src/components/preview/preview.tsx | 25 +++++------- .../src/components/preview/toolbar.tsx | 38 +++++++++++------- .../src/components/preview/tools/addons.tsx | 8 ++-- .../src/components/preview/tools/copy.tsx | 8 ++-- .../src/components/preview/tools/eject.tsx | 8 ++-- .../src/components/preview/tools/menu.tsx | 8 ++-- .../src/components/preview/tools/remount.tsx | 8 ++-- .../src/components/preview/tools/zoom.tsx | 6 ++- 9 files changed, 82 insertions(+), 66 deletions(-) diff --git a/code/ui/manager/src/components/preview/preview.mockdata.tsx b/code/ui/manager/src/components/preview/preview.mockdata.tsx index f466b170deec..3129c92bbc72 100644 --- a/code/ui/manager/src/components/preview/preview.mockdata.tsx +++ b/code/ui/manager/src/components/preview/preview.mockdata.tsx @@ -1,28 +1,29 @@ import { types } from '@storybook/manager-api'; -import type { API, State, Addon } from '@storybook/manager-api'; +import type { API, State } from '@storybook/manager-api'; +import type { Addon_BaseType, Addon_Collection } from '@storybook/types'; import type { PreviewProps } from './utils/types'; +const addonNotes: Addon_BaseType = { + id: 'notes', + type: types.TAB, + title: 'Notes', + route: ({ storyId }) => `/info/${storyId}`, + match: ({ viewMode }) => viewMode === 'info', + render: () => null, +}; + +const mockAPI: Partial<API> = { + on: (a, b) => () => {}, + emit: () => {}, + off: () => {}, + getElements: (type) => + type === types.TAB ? ({ notes: addonNotes } as Addon_Collection<any>) : {}, +}; + export const previewProps: PreviewProps = { id: 'string', storyId: 'story--id', - api: { - on: () => {}, - emit: () => {}, - off: () => {}, - getElements: ((type) => - type === types.TAB - ? [ - { - id: 'notes', - type: types.TAB, - title: 'Notes', - route: ({ storyId }) => `/info/${storyId}`, - match: ({ viewMode }) => viewMode === 'info', - render: () => null, - } as Addon, - ] - : []) as API['getElements'], - } as any as API, + api: mockAPI as API, entry: { tags: [], type: 'story', diff --git a/code/ui/manager/src/components/preview/preview.tsx b/code/ui/manager/src/components/preview/preview.tsx index 9d01ed28eaf5..ad4c0b634fed 100644 --- a/code/ui/manager/src/components/preview/preview.tsx +++ b/code/ui/manager/src/components/preview/preview.tsx @@ -2,15 +2,8 @@ import React, { Fragment, useMemo, useEffect, useRef, useState } from 'react'; import { Helmet } from 'react-helmet-async'; import { global } from '@storybook/global'; -import { - type API, - Consumer, - type Combo, - merge, - addons, - types, - type Addon, -} from '@storybook/manager-api'; +import { type API, Consumer, type Combo, merge, addons, types } from '@storybook/manager-api'; +import { type Addon_BaseType } from '@storybook/types'; import { PREVIEW_BUILDER_PROGRESS, SET_CURRENT_STORY } from '@storybook/core-events'; import { Loader } from '@storybook/components'; @@ -26,8 +19,9 @@ import type { PreviewProps } from './utils/types'; const { FEATURES } = global; -const getWrappers = (getFn: API['getElements']) => Object.values(getFn<Addon>(types.PREVIEW)); -const getTabs = (getFn: API['getElements']) => Object.values(getFn<Addon>(types.TAB)); +const getWrappers = (getFn: API['getElements']) => + Object.values(getFn<Addon_BaseType>(types.PREVIEW)); +const getTabs = (getFn: API['getElements']) => Object.values(getFn<Addon_BaseType>(types.TAB)); const canvasMapper = ({ state, api }: Combo) => ({ storyId: state.storyId, @@ -42,8 +36,9 @@ const canvasMapper = ({ state, api }: Combo) => ({ active: !!(state.viewMode && state.viewMode.match(/^(story|docs)$/)), }); -const createCanvas = (id: string, baseUrl = 'iframe.html', withLoader = true): Addon => ({ +const createCanvas = (id: string, baseUrl = 'iframe.html', withLoader = true): Addon_BaseType => ({ id: 'canvas', + type: types.PREVIEW, title: 'Canvas', route: ({ storyId, refId }) => (refId ? `/story/${refId}_${storyId}` : `/story/${storyId}`), match: ({ viewMode }) => !!(viewMode && viewMode.match(/^(story|docs)$/)), @@ -228,7 +223,7 @@ const Preview = React.memo<PreviewProps>(function Preview(props) { export { Preview }; -function filterTabs(panels: Addon[], parameters: Record<string, any>) { +function filterTabs(panels: Addon_BaseType[], parameters: Record<string, any>) { const { previewTabs } = addons.getConfig(); const parametersTabs = parameters ? parameters.previewTabs : undefined; @@ -245,7 +240,7 @@ function filterTabs(panels: Addon[], parameters: Record<string, any>) { const t = arrTabs.find((tab) => tab.id === panel.id); return t === undefined || t.id === 'canvas' || !t.hidden; }) - .map((panel, index) => ({ ...panel, index } as Addon)) + .map((panel, index) => ({ ...panel, index } as Addon_BaseType)) .sort((p1, p2) => { /* eslint-disable @typescript-eslint/naming-convention */ const tab_1 = arrTabs.find((tab) => tab.id === p1.id); @@ -265,7 +260,7 @@ function filterTabs(panels: Addon[], parameters: Record<string, any>) { title: t.title || panel.title, disabled: t.disabled, hidden: t.hidden, - } as Addon; + } as Addon_BaseType; } return panel; }); diff --git a/code/ui/manager/src/components/preview/toolbar.tsx b/code/ui/manager/src/components/preview/toolbar.tsx index 454ee5d1f4f0..c4425bcc982c 100644 --- a/code/ui/manager/src/components/preview/toolbar.tsx +++ b/code/ui/manager/src/components/preview/toolbar.tsx @@ -13,11 +13,11 @@ import { merge, type LeafEntry, addons, - type Addon, types, } from '@storybook/manager-api'; import { Location, type RenderData } from '@storybook/router'; +import type { Addon_BaseType } from '@storybook/types'; import { zoomTool } from './tools/zoom'; import * as S from './utils/components'; @@ -29,10 +29,11 @@ import { menuTool } from './tools/menu'; import { addonsTool } from './tools/addons'; import { remountTool } from './tools/remount'; -export const getTools = (getFn: API['getElements']) => Object.values(getFn<Addon>(types.TOOL)); +export const getTools = (getFn: API['getElements']) => + Object.values(getFn<Addon_BaseType>(types.TOOL)); export const getToolsExtra = (getFn: API['getElements']) => - Object.values(getFn<Addon>(types.TOOLEXTRA)); + Object.values(getFn<Addon_BaseType>(types.TOOLEXTRA)); const Bar: FunctionComponent<{ shown: boolean } & Record<string, any>> = ({ shown, ...props }) => ( <FlexBar {...props} /> @@ -59,9 +60,10 @@ const fullScreenMapper = ({ api, state }: Combo) => ({ singleStory: state.singleStory, }); -export const fullScreenTool: Addon = { +export const fullScreenTool: Addon_BaseType = { title: 'fullscreen', id: 'fullscreen', + type: types.TOOL, match: (p) => ['story', 'docs'].includes(p.viewMode), render: () => ( <Consumer filter={fullScreenMapper}> @@ -88,9 +90,10 @@ const tabsMapper = ({ state }: Combo) => ({ refId: state.refId, }); -export const createTabsTool = (tabs: Addon[]): Addon => ({ +export const createTabsTool = (tabs: Addon_BaseType[]): Addon_BaseType => ({ title: 'title', id: 'title', + type: types.TOOL, render: () => ( <Consumer filter={tabsMapper}> {(rp) => ( @@ -117,12 +120,17 @@ export const createTabsTool = (tabs: Addon[]): Addon => ({ ), }); -export const defaultTools: Addon[] = [remountTool, zoomTool]; -export const defaultToolsExtra: Addon[] = [addonsTool, fullScreenTool, ejectTool, copyTool]; +export const defaultTools: Addon_BaseType[] = [remountTool, zoomTool]; +export const defaultToolsExtra: Addon_BaseType[] = [ + addonsTool, + fullScreenTool, + ejectTool, + copyTool, +]; const useTools = ( getElements: API['getElements'], - tabs: Addon[], + tabs: Addon_BaseType[], viewMode: PreviewProps['viewMode'], entry: PreviewProps['entry'], location: PreviewProps['location'], @@ -154,7 +162,7 @@ const useTools = ( export interface ToolData { isShown: boolean; - tabs: Addon[]; + tabs: Addon_BaseType[]; api: API; entry: LeafEntry; } @@ -180,7 +188,7 @@ export const ToolbarComp = React.memo<ToolData>(function ToolbarComp(props) { ); }); -export const Tools = React.memo<{ list: Addon[] }>(function Tools({ list }) { +export const Tools = React.memo<{ list: Addon_BaseType[] }>(function Tools({ list }) { return ( <> {list.filter(Boolean).map(({ render: Render, id, ...t }, index) => ( @@ -191,7 +199,7 @@ export const Tools = React.memo<{ list: Addon[] }>(function Tools({ list }) { ); }); -function toolbarItemHasBeenExcluded(item: Partial<Addon>, entry: LeafEntry) { +function toolbarItemHasBeenExcluded(item: Partial<Addon_BaseType>, entry: LeafEntry) { const parameters = entry.type === 'story' && entry.prepared ? entry.parameters : {}; const toolbarItemsFromStoryParameters = 'toolbar' in parameters ? parameters.toolbar : undefined; const { toolbar: toolbarItemsFromAddonsConfig } = addons.getConfig(); @@ -202,9 +210,9 @@ function toolbarItemHasBeenExcluded(item: Partial<Addon>, entry: LeafEntry) { } export function filterTools( - tools: Addon[], - toolsExtra: Addon[], - tabs: Addon[], + tools: Addon_BaseType[], + toolsExtra: Addon_BaseType[], + tabs: Addon_BaseType[], { viewMode, entry, @@ -224,7 +232,7 @@ export function filterTools( ]; const toolsRight = [...toolsExtra]; - const filter = (item: Partial<Addon>) => + const filter = (item: Partial<Addon_BaseType>) => item && (!item.match || item.match({ diff --git a/code/ui/manager/src/components/preview/tools/addons.tsx b/code/ui/manager/src/components/preview/tools/addons.tsx index 9fc35f6de659..53fe821ae4af 100644 --- a/code/ui/manager/src/components/preview/tools/addons.tsx +++ b/code/ui/manager/src/components/preview/tools/addons.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { IconButton, Icons } from '@storybook/components'; -import { Consumer } from '@storybook/manager-api'; -import type { Addon, Combo } from '@storybook/manager-api'; +import { Consumer, types } from '@storybook/manager-api'; +import type { Combo } from '@storybook/manager-api'; +import type { Addon_BaseType } from '@storybook/types'; const menuMapper = ({ api, state }: Combo) => ({ isVisible: state.layout.showPanel, @@ -10,9 +11,10 @@ const menuMapper = ({ api, state }: Combo) => ({ toggle: () => api.togglePanel(), }); -export const addonsTool: Addon = { +export const addonsTool: Addon_BaseType = { title: 'addons', id: 'addons', + type: types.TOOL, match: ({ viewMode }) => viewMode === 'story', render: () => ( <Consumer filter={menuMapper}> diff --git a/code/ui/manager/src/components/preview/tools/copy.tsx b/code/ui/manager/src/components/preview/tools/copy.tsx index 857f8a82761e..0b0084aae857 100644 --- a/code/ui/manager/src/components/preview/tools/copy.tsx +++ b/code/ui/manager/src/components/preview/tools/copy.tsx @@ -2,8 +2,9 @@ import { global } from '@storybook/global'; import React from 'react'; import copy from 'copy-to-clipboard'; import { getStoryHref, IconButton, Icons } from '@storybook/components'; -import { Consumer } from '@storybook/manager-api'; -import type { Addon, Combo } from '@storybook/manager-api'; +import { Consumer, types } from '@storybook/manager-api'; +import type { Combo } from '@storybook/manager-api'; +import type { Addon_BaseType } from '@storybook/types'; const { PREVIEW_URL, document } = global; @@ -22,9 +23,10 @@ const copyMapper = ({ state }: Combo) => { }; }; -export const copyTool: Addon = { +export const copyTool: Addon_BaseType = { title: 'copy', id: 'copy', + type: types.TOOL, match: ({ viewMode }) => viewMode === 'story', render: () => ( <Consumer filter={copyMapper}> diff --git a/code/ui/manager/src/components/preview/tools/eject.tsx b/code/ui/manager/src/components/preview/tools/eject.tsx index 6a08419f3077..65688038b859 100644 --- a/code/ui/manager/src/components/preview/tools/eject.tsx +++ b/code/ui/manager/src/components/preview/tools/eject.tsx @@ -1,8 +1,9 @@ import { global } from '@storybook/global'; import React from 'react'; import { getStoryHref, IconButton, Icons } from '@storybook/components'; -import { Consumer } from '@storybook/manager-api'; -import type { Addon, Combo } from '@storybook/manager-api'; +import { Consumer, types } from '@storybook/manager-api'; +import type { Combo } from '@storybook/manager-api'; +import type { Addon_BaseType } from '@storybook/types'; const { PREVIEW_URL } = global; @@ -18,9 +19,10 @@ const ejectMapper = ({ state }: Combo) => { }; }; -export const ejectTool: Addon = { +export const ejectTool: Addon_BaseType = { title: 'eject', id: 'eject', + type: types.TOOL, match: ({ viewMode }) => viewMode === 'story', render: () => ( <Consumer filter={ejectMapper}> diff --git a/code/ui/manager/src/components/preview/tools/menu.tsx b/code/ui/manager/src/components/preview/tools/menu.tsx index 6edb1366f77d..8512e9e16abc 100644 --- a/code/ui/manager/src/components/preview/tools/menu.tsx +++ b/code/ui/manager/src/components/preview/tools/menu.tsx @@ -1,7 +1,8 @@ import React from 'react'; import { IconButton, Icons, Separator } from '@storybook/components'; -import { Consumer } from '@storybook/manager-api'; -import type { Addon, Combo } from '@storybook/manager-api'; +import { Consumer, types } from '@storybook/manager-api'; +import type { Combo } from '@storybook/manager-api'; +import type { Addon_BaseType } from '@storybook/types'; const menuMapper = ({ api, state }: Combo) => ({ isVisible: state.layout.showNav, @@ -9,9 +10,10 @@ const menuMapper = ({ api, state }: Combo) => ({ toggle: () => api.toggleNav(), }); -export const menuTool: Addon = { +export const menuTool: Addon_BaseType = { title: 'menu', id: 'menu', + type: types.TOOL, match: ({ viewMode }) => ['story', 'docs'].includes(viewMode), render: () => ( <Consumer filter={menuMapper}> diff --git a/code/ui/manager/src/components/preview/tools/remount.tsx b/code/ui/manager/src/components/preview/tools/remount.tsx index b095c6f431c1..6bf4c1f7553e 100644 --- a/code/ui/manager/src/components/preview/tools/remount.tsx +++ b/code/ui/manager/src/components/preview/tools/remount.tsx @@ -1,10 +1,11 @@ import type { ComponentProps } from 'react'; import React, { useState } from 'react'; import { IconButton, Icons } from '@storybook/components'; -import { Consumer } from '@storybook/manager-api'; -import type { Addon, Combo } from '@storybook/manager-api'; +import { Consumer, types } from '@storybook/manager-api'; +import type { Combo } from '@storybook/manager-api'; import { styled } from '@storybook/theming'; import { FORCE_REMOUNT } from '@storybook/core-events'; +import type { Addon_BaseType } from '@storybook/types'; interface AnimatedButtonProps { animating?: boolean; @@ -28,9 +29,10 @@ const menuMapper = ({ api, state }: Combo) => { }; }; -export const remountTool: Addon = { +export const remountTool: Addon_BaseType = { title: 'remount', id: 'remount', + type: types.TOOL, match: ({ viewMode }) => viewMode === 'story', render: () => ( <Consumer filter={menuMapper}> diff --git a/code/ui/manager/src/components/preview/tools/zoom.tsx b/code/ui/manager/src/components/preview/tools/zoom.tsx index fe6530b96d17..84e0f72bafae 100644 --- a/code/ui/manager/src/components/preview/tools/zoom.tsx +++ b/code/ui/manager/src/components/preview/tools/zoom.tsx @@ -2,7 +2,8 @@ import type { SyntheticEvent, MouseEventHandler } from 'react'; import React, { Component, useCallback } from 'react'; import { Icons, IconButton, Separator } from '@storybook/components'; -import type { Addon } from '@storybook/manager-api'; +import type { Addon_BaseType } from '@storybook/types'; +import { types } from '@storybook/manager-api'; const initialZoom = 1 as const; @@ -78,9 +79,10 @@ const ZoomWrapper = React.memo<{ set: (zoomLevel: number) => void; value: number } ); -export const zoomTool: Addon = { +export const zoomTool: Addon_BaseType = { title: 'zoom', id: 'zoom', + type: types.TOOL, match: ({ viewMode }) => viewMode === 'story', render: React.memo(function ZoomToolRenderer() { return ( From e46838b1da71a808c943afa34e41a16822aa249d Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 3 Jul 2023 22:46:57 +0200 Subject: [PATCH 03/14] fix mockdata by adding the type property --- code/ui/manager/src/components/layout/app.mockdata.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/code/ui/manager/src/components/layout/app.mockdata.tsx b/code/ui/manager/src/components/layout/app.mockdata.tsx index 48e2696d3306..40f85f51cd37 100644 --- a/code/ui/manager/src/components/layout/app.mockdata.tsx +++ b/code/ui/manager/src/components/layout/app.mockdata.tsx @@ -4,6 +4,7 @@ import React, { Component } from 'react'; import { styled } from '@storybook/theming'; import type { Addon_Collection } from '@storybook/types'; import type { State } from '@storybook/manager-api'; +import { types } from '@storybook/manager-api'; import type { SidebarProps } from '../sidebar/Sidebar'; import { Sidebar } from '../sidebar/Sidebar'; import Panel from '../panel/panel'; @@ -39,6 +40,7 @@ export const shortcuts: State['shortcuts'] = { export const panels: Addon_Collection = { test1: { + type: types.PANEL, title: 'Test 1', render: ({ active, key }) => active ? ( @@ -48,6 +50,7 @@ export const panels: Addon_Collection = { ) : null, }, test2: { + type: types.PANEL, title: 'Test 2', render: ({ active, key }) => active ? ( From 3e1637a93357e80ec67ab3cf49fdf7be4f3d0662 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 3 Jul 2023 23:10:18 +0200 Subject: [PATCH 04/14] revert unintended change (bit of a sneak peak, I guess) --- code/lib/types/src/modules/addons.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/code/lib/types/src/modules/addons.ts b/code/lib/types/src/modules/addons.ts index 613c8080ccd4..8190a6a04a62 100644 --- a/code/lib/types/src/modules/addons.ts +++ b/code/lib/types/src/modules/addons.ts @@ -21,7 +21,7 @@ import type { } from './csf'; import type { IndexEntry } from './storyIndex'; -export type Addon_Types = Exclude<Addon_TypesEnum, Addon_TypesEnum.experimental_PAGE>; +export type Addon_Types = Addon_TypesEnum; export interface Addon_ArgType<TArg = unknown> extends InputType { defaultValue?: TArg; @@ -380,11 +380,6 @@ export enum Addon_TypesEnum { * @unstable */ PREVIEW = 'preview', - /** - * This adds pages that render instead of the canvas. - * DO NOT USE - */ - experimental_PAGE = 'main', /** * @deprecated This property does nothing, and will be removed in Storybook 8.0. From 581e422d4aba124389217a5d460845ffc16d6d5e Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 3 Jul 2023 23:12:39 +0200 Subject: [PATCH 05/14] change to jsdoc over normal code-comment --- code/lib/types/src/modules/addons.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/code/lib/types/src/modules/addons.ts b/code/lib/types/src/modules/addons.ts index 8190a6a04a62..4d1221a66e3f 100644 --- a/code/lib/types/src/modules/addons.ts +++ b/code/lib/types/src/modules/addons.ts @@ -314,7 +314,7 @@ export type ReactJSXElement = { export type Addon_Type = Addon_BaseType; export interface Addon_BaseType { - title: MyFC | string | ReactElement | ReactNode; + title: FCWithoutChildren | string | ReactElement | ReactNode; type: Addon_Types; id?: string; route?: (routeOptions: RouterData) => string; @@ -325,9 +325,13 @@ export interface Addon_BaseType { hidden?: boolean; } -// This is a copy of FC from react/index.d.ts, but has the PropsWithChildren type removed -// this is correct and more type strict, and future compatible with React.FC in React 18+ -interface MyFC<P = {}> { +/** + * This is a copy of FC from react/index.d.ts, but has the PropsWithChildren type removed + * this is correct and more type strict, and future compatible with React.FC in React 18+ + * + * @deprecated This type is deprecated and will be removed in 8.0. (assuming the manager uses React 18 is out by then) + */ +interface FCWithoutChildren<P = {}> { (props: P, context?: any): ReactElement<any, any> | null; propTypes?: WeakValidationMap<P> | undefined; contextTypes?: ValidationMap<any> | undefined; From a45faea703d2d582f32461183ffc8e1a8e275017 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 3 Jul 2023 23:40:16 +0200 Subject: [PATCH 06/14] improve types of manager api add & addPanel --- code/lib/manager-api/src/lib/addons.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/code/lib/manager-api/src/lib/addons.ts b/code/lib/manager-api/src/lib/addons.ts index aa508b535ec1..9ec462e1eb6c 100644 --- a/code/lib/manager-api/src/lib/addons.ts +++ b/code/lib/manager-api/src/lib/addons.ts @@ -93,17 +93,17 @@ export class AddonStore { return this.elements[type]; }; - addPanel = (name: string, options: Addon_Type): void => { - this.add(name, { + addPanel = (id: string, options: Omit<Addon_Type, 'type' | 'id'>): void => { + this.add(id, { type: Addon_TypesEnum.PANEL, ...options, }); }; - add = (name: string, addon: Addon_Type) => { + add = (id: string, addon: Omit<Addon_Type, 'id'>) => { const { type } = addon; const collection = this.getElements(type); - collection[name] = { id: name, ...addon }; + collection[id] = { id, ...addon }; }; setConfig = (value: Addon_Config) => { From 89726497a99cb9f5ed43adf2ca6329eec8969c3b Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Mon, 3 Jul 2023 23:51:42 +0200 Subject: [PATCH 07/14] improve api for manager addon registration, deprecate the extra `id` property, remove use in monorepo --- code/addons/a11y/src/manager.tsx | 1 - code/addons/actions/src/manager.tsx | 1 - code/addons/backgrounds/src/manager.tsx | 1 - code/addons/controls/src/manager.tsx | 1 - code/addons/jest/src/manager.tsx | 1 - code/addons/measure/src/manager.tsx | 1 - code/addons/outline/src/manager.tsx | 1 - code/addons/storysource/src/manager.tsx | 1 - code/addons/toolbars/src/manager.tsx | 1 - code/addons/viewport/src/manager.tsx | 1 - code/lib/manager-api/src/lib/addons.ts | 14 ++++++++++++-- 11 files changed, 12 insertions(+), 12 deletions(-) diff --git a/code/addons/a11y/src/manager.tsx b/code/addons/a11y/src/manager.tsx index 910e3d10050b..718766ab07b8 100644 --- a/code/addons/a11y/src/manager.tsx +++ b/code/addons/a11y/src/manager.tsx @@ -22,7 +22,6 @@ addons.register(ADDON_ID, (api) => { const totalNb = violationsNb + incompleteNb; return totalNb !== 0 ? `Accessibility (${totalNb})` : 'Accessibility'; }, - id: 'accessibility', type: types.PANEL, render: ({ active = true, key }) => ( <A11yContextProvider key={key} active={active}> diff --git a/code/addons/actions/src/manager.tsx b/code/addons/actions/src/manager.tsx index 38e69e4b11ac..e7b81047231b 100644 --- a/code/addons/actions/src/manager.tsx +++ b/code/addons/actions/src/manager.tsx @@ -42,7 +42,6 @@ addons.register(ADDON_ID, (api) => { addons.add(PANEL_ID, { title: <Title count={countRef} />, - 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 4cabc6011c84..ecd36b0bb618 100644 --- a/code/addons/backgrounds/src/manager.tsx +++ b/code/addons/backgrounds/src/manager.tsx @@ -8,7 +8,6 @@ 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 6558aa6eda0f..eee2ac03e7c5 100644 --- a/code/addons/controls/src/manager.tsx +++ b/code/addons/controls/src/manager.tsx @@ -17,7 +17,6 @@ function Title() { addons.register(ADDON_ID, (api) => { addons.add(ADDON_ID, { title: <Title />, - id: 'controls', type: types.PANEL, paramKey: PARAM_KEY, render: ({ key, active }) => { diff --git a/code/addons/jest/src/manager.tsx b/code/addons/jest/src/manager.tsx index 82bcee71dc4f..2c774b739b88 100644 --- a/code/addons/jest/src/manager.tsx +++ b/code/addons/jest/src/manager.tsx @@ -8,7 +8,6 @@ addons.register(ADDON_ID, (api) => { addons.add(PANEL_ID, { title: 'Tests', type: types.PANEL, - 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 f65075617ef3..53bca4d0b716 100644 --- a/code/addons/measure/src/manager.tsx +++ b/code/addons/measure/src/manager.tsx @@ -7,7 +7,6 @@ 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 d1852360356e..384ea24c07c6 100644 --- a/code/addons/outline/src/manager.tsx +++ b/code/addons/outline/src/manager.tsx @@ -7,7 +7,6 @@ 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 8663612d68f2..0322de32d8d5 100644 --- a/code/addons/storysource/src/manager.tsx +++ b/code/addons/storysource/src/manager.tsx @@ -8,7 +8,6 @@ addons.register(ADDON_ID, (api) => { addons.add(PANEL_ID, { type: types.PANEL, 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 adfc9956bd7a..c87d3fbf2d80 100644 --- a/code/addons/toolbars/src/manager.tsx +++ b/code/addons/toolbars/src/manager.tsx @@ -6,7 +6,6 @@ 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 602cd708a4ea..e42e3fba40e1 100644 --- a/code/addons/viewport/src/manager.tsx +++ b/code/addons/viewport/src/manager.tsx @@ -8,7 +8,6 @@ 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/lib/manager-api/src/lib/addons.ts b/code/lib/manager-api/src/lib/addons.ts index 9ec462e1eb6c..647cbdb19dd7 100644 --- a/code/lib/manager-api/src/lib/addons.ts +++ b/code/lib/manager-api/src/lib/addons.ts @@ -21,6 +21,13 @@ export function isSupportedType(type: Addon_Types): boolean { return !!Object.values(Addon_TypesEnum).find((typeVal) => typeVal === type); } +interface DeprecatedAddonWithId { + /** + * @deprecated will be removed in 8.0, when registering addons, please use the addon id as the first argument + */ + id?: string; +} + export class AddonStore { constructor() { this.promise = new Promise((res) => { @@ -93,14 +100,17 @@ export class AddonStore { return this.elements[type]; }; - addPanel = (id: string, options: Omit<Addon_Type, 'type' | 'id'>): void => { + addPanel = ( + id: string, + options: Omit<Addon_Type, 'type' | 'id'> & DeprecatedAddonWithId + ): void => { this.add(id, { type: Addon_TypesEnum.PANEL, ...options, }); }; - add = (id: string, addon: Omit<Addon_Type, 'id'>) => { + add = (id: string, addon: Omit<Addon_Type, 'id'> & DeprecatedAddonWithId) => { const { type } = addon; const collection = this.getElements(type); collection[id] = { id, ...addon }; From 54eae22cf834583d88c0980ad8af9b1392e75788 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 6 Jul 2023 17:08:39 +0200 Subject: [PATCH 08/14] cleanup --- code/lib/types/src/modules/addons.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/code/lib/types/src/modules/addons.ts b/code/lib/types/src/modules/addons.ts index 4d1221a66e3f..8047611f325a 100644 --- a/code/lib/types/src/modules/addons.ts +++ b/code/lib/types/src/modules/addons.ts @@ -314,7 +314,7 @@ export type ReactJSXElement = { export type Addon_Type = Addon_BaseType; export interface Addon_BaseType { - title: FCWithoutChildren | string | ReactElement | ReactNode; + title: FCWithoutChildren | ReactNode; type: Addon_Types; id?: string; route?: (routeOptions: RouterData) => string; From c429c1f46bf89213d02def839c4fa9d32bb5ad3b Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 6 Jul 2023 17:24:11 +0200 Subject: [PATCH 09/14] make it prettier --- code/ui/components/src/tabs/tabs.stories.tsx | 50 ++++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index 6d6973befcf6..3942b885df17 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -13,6 +13,8 @@ import { } from '@storybook/testing-library'; import { Tabs, TabsState, TabWrapper } from './tabs'; import type { ChildrenList } from './tabs.helpers'; +import { IconButton } from '../bar/button'; +import { Icons } from '../icon/icon'; const colours = Array.from(new Array(15), (val, index) => index).map((i) => Math.floor((1 / 15) * i * 16777215) @@ -122,20 +124,6 @@ const content = Object.entries(panels).map(([k, v]) => ( export default { title: 'Tabs', - decorators: [ - (story) => ( - <div - style={{ - position: 'relative', - height: 'calc(100vh - 20px)', - width: 'calc(100vw - 20px)', - margin: 10, - }} - > - {story()} - </div> - ), - ], args: { menuName: 'Addons', }, @@ -193,11 +181,11 @@ export const StatefulStaticWithSetBackgroundColor = { } satisfies Story; const customViewports = { - chromatic: { - name: 'Chromatic', + sized: { + name: 'Sized', styles: { width: '380px', - height: '963px', + height: '500px', }, }, }; @@ -205,7 +193,7 @@ const customViewports = { export const StatefulDynamicWithOpenTooltip = { parameters: { viewport: { - defaultViewport: 'chromatic', + defaultViewport: 'sized', viewports: customViewports, }, chromatic: { viewports: [380] }, @@ -254,12 +242,14 @@ export const StatefulDynamicWithSelectedAddon = { }, play: async (context) => { await StatefulDynamicWithOpenTooltip.play(context); + const canvas = within(context.canvasElement); await waitFor(async () => { const popperContainer = await screen.findByTestId('tooltip'); const tab4 = await findByText(popperContainer, 'Tab title #4', {}); fireEvent(tab4, new MouseEvent('click', { bubbles: true })); - await waitFor(() => screen.findByText('CONTENT 4')); + const content4 = await canvas.findByText('CONTENT 4'); + await expect(content4).toBeVisible(); }); // reopen the tooltip @@ -300,6 +290,7 @@ export const StatelessBordered = { export const StatelessWithTools = { render: (args) => ( <Tabs + bordered selected="test3" menuName="Addons" actions={{ @@ -307,12 +298,12 @@ export const StatelessWithTools = { }} tools={ <Fragment> - <button type="button" onClick={() => logger.log('1')}> - 1 - </button> - <button type="button" onClick={() => logger.log('2')}> - 2 - </button> + <IconButton title="Tool 1"> + <Icons icon="memory" /> + </IconButton> + <IconButton title="Tool 2"> + <Icons icon="cpu" /> + </IconButton> </Fragment> } {...args} @@ -323,6 +314,9 @@ export const StatelessWithTools = { } satisfies Story; export const StatelessAbsolute = { + parameters: { + layout: 'fullscreen', + }, render: (args) => ( <Tabs absolute @@ -339,6 +333,9 @@ export const StatelessAbsolute = { } satisfies Story; export const StatelessAbsoluteBordered = { + parameters: { + layout: 'fullscreen', + }, render: (args) => ( <Tabs absolute @@ -356,6 +353,9 @@ export const StatelessAbsoluteBordered = { } satisfies Story; export const StatelessEmpty = { + parameters: { + layout: 'fullscreen', + }, render: (args) => ( <Tabs actions={{ From 909a8512b5003dc43b2de0991acdfa74ca4ca164 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 6 Jul 2023 17:32:30 +0200 Subject: [PATCH 10/14] simplify code --- code/ui/components/src/tabs/tabs.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index b5bfe93e8894..7e4d61429f77 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -143,12 +143,13 @@ export const Tabs: FC<TabsProps> = memo( menuName, }) => { const idList = childrenToList(children).map((i) => i.id); - const primeList = useMemo<ChildrenList>(() => childrenToList(children), [...idList]); - const list = useMemo( () => - primeList.map((i, index) => ({ ...i, active: selected ? i.id === selected : index === 0 })), - [selected, primeList] + childrenToList(children).map((i, index) => ({ + ...i, + active: selected ? i.id === selected : index === 0, + })), + [selected, ...idList] ); const { visibleList, tabBarRef, tabRefs, AddonTab } = useList(list); From f32ead4746bddf7340a2ae056c507dc473a68db4 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 6 Jul 2023 17:39:40 +0200 Subject: [PATCH 11/14] cleanup --- code/ui/components/src/tabs/tabs.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/code/ui/components/src/tabs/tabs.tsx b/code/ui/components/src/tabs/tabs.tsx index 7e4d61429f77..c8a18bf29c06 100644 --- a/code/ui/components/src/tabs/tabs.tsx +++ b/code/ui/components/src/tabs/tabs.tsx @@ -7,7 +7,6 @@ import type { Addon_RenderOptions } from '@storybook/types'; import { Placeholder } from '../placeholder/placeholder'; import { TabButton } from '../bar/button'; import { FlexBar } from '../bar/bar'; -import type { ChildrenList } from './tabs.helpers'; import { childrenToList, VisuallyHidden } from './tabs.helpers'; import { useList } from './tabs.hooks'; From b7cee9329fb0adf2032dafd941183b5315e72dcb Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 6 Jul 2023 21:28:51 +0200 Subject: [PATCH 12/14] fix --- code/ui/components/src/tabs/tabs.stories.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index 3942b885df17..1eb07b0b3397 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -1,7 +1,6 @@ import { expect } from '@storybook/jest'; import React, { Fragment } from 'react'; import { action } from '@storybook/addon-actions'; -import { logger } from '@storybook/client-logger'; import type { Meta, StoryObj } from '@storybook/react'; import { within, From 2964e4b296d27d07e58bb982aaa5bbf48336b8e5 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 6 Jul 2023 22:00:13 +0200 Subject: [PATCH 13/14] fix --- code/ui/components/src/tabs/tabs.stories.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index 1eb07b0b3397..1f0b21f65d44 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -195,6 +195,7 @@ export const StatefulDynamicWithOpenTooltip = { defaultViewport: 'sized', viewports: customViewports, }, + theme: 'light', chromatic: { viewports: [380] }, }, play: async ({ canvasElement }) => { @@ -234,9 +235,10 @@ export const StatefulDynamicWithOpenTooltip = { export const StatefulDynamicWithSelectedAddon = { parameters: { viewport: { - defaultViewport: 'chromatic', + defaultViewport: 'sized', viewports: customViewports, }, + theme: 'light', chromatic: { viewports: [380] }, }, play: async (context) => { From 3f2bc4380b4f6d233f95cbffaa6439d73af65ef2 Mon Sep 17 00:00:00 2001 From: Norbert de Langen <ndelangen@me.com> Date: Thu, 6 Jul 2023 22:58:19 +0200 Subject: [PATCH 14/14] cleanup --- code/ui/components/src/tabs/tabs.stories.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/code/ui/components/src/tabs/tabs.stories.tsx b/code/ui/components/src/tabs/tabs.stories.tsx index 1f0b21f65d44..77d357acf0b9 100644 --- a/code/ui/components/src/tabs/tabs.stories.tsx +++ b/code/ui/components/src/tabs/tabs.stories.tsx @@ -233,14 +233,7 @@ export const StatefulDynamicWithOpenTooltip = { } satisfies Story; export const StatefulDynamicWithSelectedAddon = { - parameters: { - viewport: { - defaultViewport: 'sized', - viewports: customViewports, - }, - theme: 'light', - chromatic: { viewports: [380] }, - }, + ...StatefulDynamicWithOpenTooltip, play: async (context) => { await StatefulDynamicWithOpenTooltip.play(context); const canvas = within(context.canvasElement);