Skip to content

Commit

Permalink
Merge pull request #23288 from storybookjs/norbert/ui-tabs-types-impr…
Browse files Browse the repository at this point in the history
…ovements

UI: Improve tabs component, more type correct, allow for `FC` as title
  • Loading branch information
ndelangen authored Jul 6, 2023
2 parents 25b35ef + 3f2bc43 commit 8e1dbba
Show file tree
Hide file tree
Showing 29 changed files with 371 additions and 191 deletions.
1 change: 0 additions & 1 deletion code/addons/a11y/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}>
Expand Down
1 change: 0 additions & 1 deletion code/addons/actions/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
1 change: 0 additions & 1 deletion code/addons/backgrounds/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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: () => (
Expand Down
1 change: 0 additions & 1 deletion code/addons/controls/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
1 change: 0 additions & 1 deletion code/addons/jest/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Expand Down
1 change: 0 additions & 1 deletion code/addons/measure/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 />,
Expand Down
1 change: 0 additions & 1 deletion code/addons/outline/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 />,
Expand Down
1 change: 0 additions & 1 deletion code/addons/storysource/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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',
});
Expand Down
1 change: 0 additions & 1 deletion code/addons/toolbars/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 />,
Expand Down
1 change: 0 additions & 1 deletion code/addons/viewport/src/manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 />,
Expand Down
18 changes: 14 additions & 4 deletions code/lib/manager-api/src/lib/addons.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -93,17 +100,20 @@ 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'> & DeprecatedAddonWithId
): void => {
this.add(id, {
type: Addon_TypesEnum.PANEL,
...options,
});
};

add = (name: string, addon: Addon_Type) => {
add = (id: string, addon: Omit<Addon_Type, 'id'> & DeprecatedAddonWithId) => {
const { type } = addon;
const collection = this.getElements(type);
collection[name] = { id: name, ...addon };
collection[id] = { id, ...addon };
};

setConfig = (value: Addon_Config) => {
Expand Down
57 changes: 49 additions & 8 deletions code/lib/types/src/modules/addons.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand All @@ -20,7 +21,7 @@ import type {
} from './csf';
import type { IndexEntry } from './storyIndex';

export type Addon_Types = Addon_TypesEnum | string;
export type Addon_Types = Addon_TypesEnum;

export interface Addon_ArgType<TArg = unknown> extends InputType {
defaultValue?: TArg;
Expand Down Expand Up @@ -122,9 +123,10 @@ export interface Addon_AddStoryArgs<StoryFnReturnType = unknown> {
parameters: Parameters;
}

export interface Addon_ClientApiAddon<StoryFnReturnType = unknown> extends Addon_Type {
export type Addon_ClientApiAddon<StoryFnReturnType = unknown> = Addon_Type & {
apply: (a: Addon_StoryApi<StoryFnReturnType>, b: any[]) => any;
}
};

export interface Addon_ClientApiAddons<StoryFnReturnType> {
[key: string]: Addon_ClientApiAddon<StoryFnReturnType>;
}
Expand Down Expand Up @@ -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: FCWithoutChildren | ReactNode;
type: Addon_Types;
id?: string;
route?: (routeOptions: RouterData) => string;
match?: (matchOptions: RouterData) => boolean;
Expand All @@ -319,13 +325,27 @@ 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+
*
* @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;
defaultProps?: Partial<P> | undefined;
displayName?: string | undefined;
}

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

export interface Addon_Loaders<API> {
[key: string]: Addon_Loader<API>;
}
export interface Addon_Collection {
[key: string]: Addon_Type;
export interface Addon_Collection<T = Addon_Type> {
[key: string]: T;
}
export interface Addon_Elements {
[key: string]: Addon_Collection;
Expand All @@ -342,10 +362,31 @@ 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',

/**
* @deprecated This property does nothing, and will be removed in Storybook 8.0.
*/
NOTES_ELEMENT = 'notes-element',
}
8 changes: 3 additions & 5 deletions code/lib/types/src/modules/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -32,11 +32,9 @@ export interface API_MatchOptions {

export type API_Addon = Addon_Type;

export interface API_Collection<T = API_Addon> {
[key: string]: T;
}
export type API_Collection<T = Addon_Type> = Addon_Collection<T>;

export type API_Panels = API_Collection<API_Addon>;
export type API_Panels = Addon_Collection<Addon_Type>;

export type API_StateMerger<S> = (input: S) => S;

Expand Down
46 changes: 32 additions & 14 deletions code/ui/components/src/tabs/tabs.helpers.tsx
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -10,25 +12,41 @@ export const VisuallyHidden = styled.div<VisuallyHiddenProps>(({ 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<Addon_RenderOptions> | ReactChild | null;
title: ReactChild | null | FC;
id: string;
color?: string;
}>) => {
const content: FC<Addon_RenderOptions> | ReactNode = Array.isArray(childrenOfChild)
? childrenOfChild[0]
: childrenOfChild;

const render: FC<Addon_RenderOptions> = (
typeof content === 'function'
? content
: ({ active, key }: any) => (
<VisuallyHidden key={key} active={active} role="tabpanel">
{content}
</VisuallyHidden>
)
) as FC<Addon_RenderOptions>;
return {
active: selected ? id === selected : index === 0,
title,
id,
color,
render:
typeof content === 'function'
? content
: ({ active, key }: any) => (
<VisuallyHidden key={key} active={active} role="tabpanel">
{content}
</VisuallyHidden>
),
...(color ? { color } : {}),
render,
};
}
);

export type ChildrenList = ReturnType<typeof childrenToList>;
export type ChildrenListComplete = Array<
ReturnType<typeof childrenToList>[0] & {
active: boolean;
}
>;
10 changes: 5 additions & 5 deletions code/ui/components/src/tabs/tabs.hooks.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => ({
Expand All @@ -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<HTMLDivElement>();
const addonsRef = useRef<HTMLButtonElement>();
const tabRefs = useRef(new Map<string, HTMLButtonElement>());
Expand All @@ -41,8 +41,8 @@ export function useList(list: ChildrenList) {
});

const [visibleList, setVisibleList] = useState(list);
const [invisibleList, setInvisibleList] = useState<ChildrenList>([]);
const previousList = useRef<ChildrenList>(list);
const [invisibleList, setInvisibleList] = useState<ChildrenListComplete>([]);
const previousList = useRef<ChildrenListComplete>(list);

const AddonTab = useCallback(
({
Expand Down Expand Up @@ -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;

Expand Down
Loading

0 comments on commit 8e1dbba

Please sign in to comment.