Skip to content

Commit

Permalink
Merge pull request #23311 from storybookjs/norbert/addon-types-follow-up
Browse files Browse the repository at this point in the history
UI: refactor Canvas component so we can improve types for PREVIEW addons and TAB addons
  • Loading branch information
ndelangen committed Jul 13, 2023
2 parents 82afbe6 + a80b1ff commit c74ed74
Show file tree
Hide file tree
Showing 12 changed files with 269 additions and 179 deletions.
13 changes: 7 additions & 6 deletions code/addons/a11y/src/manager.test.tsx
@@ -1,4 +1,5 @@
import * as api from '@storybook/manager-api';
import type { Addon_BaseType } from '@storybook/types';
import { PANEL_ID } from './constants';
import './manager';

Expand All @@ -8,6 +9,8 @@ mockedApi.useAddonState = jest.fn();
const mockedAddons = api.addons as jest.Mocked<typeof api.addons>;
const registrationImpl = mockedAddons.register.mock.calls[0][1];

const isPanel = (input: Parameters<typeof mockedAddons.add>[1]): input is Addon_BaseType =>
input.type === api.types.PANEL;
describe('A11yManager', () => {
it('should register the panels', () => {
// when
Expand All @@ -31,9 +34,8 @@ describe('A11yManager', () => {
// given
mockedApi.useAddonState.mockImplementation(() => [undefined]);
registrationImpl(api as unknown as api.API);
const title = mockedAddons.add.mock.calls
.map(([_, def]) => def)
.find(({ type }) => type === api.types.PANEL)?.title as Function;
const title = mockedAddons.add.mock.calls.map(([_, def]) => def).find(isPanel)
?.title as Function;

// when / then
expect(title()).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -66,9 +68,8 @@ describe('A11yManager', () => {
},
]);
registrationImpl(mockedApi);
const title = mockedAddons.add.mock.calls
.map(([_, def]) => def)
.find(({ type }) => type === api.types.PANEL)?.title as Function;
const title = mockedAddons.add.mock.calls.map(([_, def]) => def).find(isPanel)
?.title as Function;

// when / then
expect(title()).toMatchInlineSnapshot(`
Expand Down
11 changes: 7 additions & 4 deletions code/lib/manager-api/src/lib/addons.ts
Expand Up @@ -11,6 +11,8 @@ import type {
Addon_BaseType,
Addon_PageType,
Addon_Types,
Addon_TypesMapping,
Addon_WrapperType,
} from '@storybook/types';
import { Addon_TypesEnum } from '@storybook/types';
import { logger } from '@storybook/client-logger';
Expand Down Expand Up @@ -97,9 +99,7 @@ export class AddonStore {

getElements<T extends Addon_Types | Addon_TypesEnum.experimental_PAGE>(
type: T
): T extends Addon_TypesEnum.experimental_PAGE
? Addon_Collection<Addon_PageType>
: Addon_Collection<Addon_BaseType> {
): Addon_Collection<Addon_TypesMapping[T]> {
if (!this.elements[type]) {
this.elements[type] = {};
}
Expand Down Expand Up @@ -139,7 +139,10 @@ export class AddonStore {
*/
add(
id: string,
addon: Addon_BaseType | (Omit<Addon_PageType, 'id'> & DeprecatedAddonWithId)
addon:
| Addon_BaseType
| (Omit<Addon_PageType, 'id'> & DeprecatedAddonWithId)
| (Omit<Addon_WrapperType, 'id'> & DeprecatedAddonWithId)
): void {
const { type } = addon;
const collection = this.getElements(type);
Expand Down
8 changes: 3 additions & 5 deletions code/lib/manager-api/src/modules/addons.ts
@@ -1,8 +1,8 @@
import type {
Addon_BaseType,
Addon_Collection,
Addon_PageType,
Addon_Types,
Addon_TypesMapping,
API_Panels,
API_StateMerger,
} from '@storybook/types';
Expand All @@ -25,9 +25,7 @@ export interface SubAPI {
*/
getElements: <T extends Addon_Types | Addon_TypesEnum.experimental_PAGE = Addon_Types>(
type: T
) => T extends Addon_TypesEnum.experimental_PAGE
? Addon_Collection<Addon_PageType>
: Addon_Collection<Addon_BaseType>;
) => Addon_Collection<Addon_TypesMapping[T]>;
/**
* Returns a collection of all panels.
* This is the same as calling getElements('panel')
Expand Down Expand Up @@ -58,7 +56,7 @@ export interface SubAPI {
* Sets the state of an addon with the given ID.
* @template S - The type of the addon state.
* @param {string} addonId - The ID of the addon to set the state for.
* @param {S | API_StateMerger<S>} newStateOrMerger - The new state to set, or a function that merges the current state with the new state.
* @param {S | API_StateMerger<S>} newStateOrMerger - The new state to set, or a function which receives the current state and returns the new state.
* @param {Options} [options] - Optional options for the state update.
* @deprecated This API might get dropped, if you are using this, please file an issue.
* @returns {Promise<S>} - A promise that resolves with the new state after it has been set.
Expand Down
12 changes: 6 additions & 6 deletions code/lib/manager-api/src/modules/stories.ts
Expand Up @@ -18,6 +18,7 @@ import type {
StoryPreparedPayload,
DocsPreparedPayload,
API_DocsEntry,
API_ViewMode,
API_StatusState,
API_StatusUpdate,
} from '@storybook/types';
Expand Down Expand Up @@ -60,7 +61,6 @@ const STORY_INDEX_PATH = './index.json';
type Direction = -1 | 1;
type ParameterName = string;

type ViewMode = 'story' | 'info' | 'settings' | string | undefined;
type StoryUpdate = Partial<
Pick<API_StoryEntry, 'prepared' | 'parameters' | 'initialArgs' | 'argTypes' | 'args'>
>;
Expand All @@ -69,7 +69,7 @@ type DocsUpdate = Partial<Pick<API_DocsEntry, 'prepared' | 'parameters'>>;

export interface SubState extends API_LoadedRefData {
storyId: StoryId;
viewMode: ViewMode;
viewMode: API_ViewMode;
status: API_StatusState;
}

Expand Down Expand Up @@ -102,13 +102,13 @@ export interface SubAPI {
* @param {StoryId} [story] - The ID of the story to select.
* @param {Object} [obj] - An optional object containing additional options.
* @param {string} [obj.ref] - The ref ID of the story to select.
* @param {ViewMode} [obj.viewMode] - The view mode to display the story in.
* @param {API_ViewMode} [obj.viewMode] - The view mode to display the story in.
* @returns {void}
*/
selectStory: (
kindOrId?: string,
story?: StoryId,
obj?: { ref?: string; viewMode?: ViewMode }
obj?: { ref?: string; viewMode?: API_ViewMode }
) => void;
/**
* Returns the current story's data, including its ID, kind, name, and parameters.
Expand Down Expand Up @@ -588,7 +588,7 @@ export const init: ModuleFn<SubAPI, SubState, true> = ({
viewMode,
}: {
storyId: string;
viewMode: ViewMode;
viewMode: API_ViewMode;
[k: string]: any;
}) {
const { sourceType } = getEventMetadata(this, fullAPI);
Expand Down Expand Up @@ -714,7 +714,7 @@ export const init: ModuleFn<SubAPI, SubState, true> = ({
story?: StoryName;
name?: StoryName;
storyId: string;
viewMode: ViewMode;
viewMode: API_ViewMode;
}) {
const { ref } = getEventMetadata(this, fullAPI);

Expand Down
98 changes: 90 additions & 8 deletions code/lib/types/src/modules/addons.ts
@@ -1,6 +1,13 @@
/* eslint-disable @typescript-eslint/naming-convention */

import type { ReactElement, ReactNode, ValidationMap, WeakValidationMap } from 'react';
import type {
FC,
PropsWithChildren,
ReactElement,
ReactNode,
ValidationMap,
WeakValidationMap,
} from 'react';
import type { RenderData as RouterData } from '../../../router/src/types';
import type { ThemeVars } from '../../../theming/src/types';
import type {
Expand Down Expand Up @@ -299,8 +306,8 @@ export type BaseStory<TArgs, StoryFnReturnType> =
| Addon_BaseStoryObject<TArgs, StoryFnReturnType>;

export interface Addon_RenderOptions {
active?: boolean;
key?: string;
active: boolean;
key: string;
}

/**
Expand All @@ -312,16 +319,59 @@ export type ReactJSXElement = {
key: any;
};

export type Addon_Type = Addon_BaseType | Addon_PageType;
export type Addon_Type = Addon_BaseType | Addon_PageType | Addon_WrapperType;
export interface Addon_BaseType {
/**
* The title of the addon.
* This can be a simple string, but it can also be a React.FunctionComponent or a React.ReactElement.
*/
title: FCWithoutChildren | ReactNode;
type: Addon_Types;
/**
* The type of the addon.
* @example Addon_TypesEnum.PANEL
*/
type: Exclude<Addon_Types, Addon_TypesEnum.PREVIEW>;
/**
* The unique id of the addon.
* @warn This will become non-optional in 8.0
*
* This needs to be globally unique, so we recommend prefixing it with your org name or npm package name.
*
* Do not prefix with `storybook`, this is reserved for core storybook feature and core addons.
*
* @example 'my-org-name/my-addon-name'
*/
id?: string;
/**
* This component will wrap your `render` function.
*
* With it you can determine if you want your addon to be rendered or not.
*
* This is to facilitate addons keeping state, and keep listening for events even when they are not currently on screen/rendered.
*/
route?: (routeOptions: RouterData) => string;
/**
* This will determine the value of `active` prop of your render function.
*/
match?: (matchOptions: RouterData) => boolean;
render: (renderOptions: Addon_RenderOptions) => any | null;
/**
* The actual contents of your addon.
*
* This is called as a function, so if you want to use hooks,
* your function needs to return a JSX.Element within which components are rendered
*/
render: (renderOptions: Partial<Addon_RenderOptions>) => ReactElement<any, any> | null;
/**
* @unstable
*/
paramKey?: string;
/**
* @unstable
*/
disabled?: boolean;
/**
* @unstable
*/
hidden?: boolean;
}

Expand Down Expand Up @@ -372,6 +422,38 @@ export interface Addon_PageType {
render: FCWithoutChildren;
}

export interface Addon_WrapperType {
type: Addon_TypesEnum.PREVIEW;
/**
* The unique id of the page.
*/
id: string;
/**
* A React.FunctionComponent that wraps the story.
*
* This component must accept a children prop, and render it.
*/
render: FC<
PropsWithChildren<{
index: number;
children: ReactNode;
id: string;
storyId: StoryId;
active: boolean;
}>
>;
}

type Addon_TypeBaseNames = Exclude<
Addon_TypesEnum,
Addon_TypesEnum.PREVIEW | Addon_TypesEnum.experimental_PAGE
>;

export interface Addon_TypesMapping extends Record<Addon_TypeBaseNames, Addon_BaseType> {
[Addon_TypesEnum.PREVIEW]: Addon_WrapperType;
[Addon_TypesEnum.experimental_PAGE]: Addon_PageType;
}

export type Addon_Loader<API> = (api: API) => void;

export interface Addon_Loaders<API> {
Expand Down Expand Up @@ -405,7 +487,7 @@ export enum Addon_TypesEnum {
*/
PANEL = 'panel',
/**
* This adds items in the toolbar above the canvas - on the right side.
* This adds items in the toolbar above the canvas - on the left side.
*/
TOOL = 'tool',
/**
Expand All @@ -414,7 +496,7 @@ export enum Addon_TypesEnum {
TOOLEXTRA = 'toolextra',
/**
* This adds wrapper components around the canvas/iframe component storybook renders.
* @unstable
* @unstable this API is not stable yet, and is likely to change in 8.0.
*/
PREVIEW = 'preview',
/**
Expand Down

0 comments on commit c74ed74

Please sign in to comment.