Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

UI: Improve tabs component, more type correct, allow for FC as title #23288

Merged
merged 22 commits into from
Jul 6, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e5ad678
make tabs component more type correct, allow for FC as title
ndelangen Jul 3, 2023
7cafd2f
introduce Addon_BaseType continue adding type property to addons
ndelangen Jul 3, 2023
e46838b
fix mockdata by adding the type property
ndelangen Jul 3, 2023
3e1637a
revert unintended change (bit of a sneak peak, I guess)
ndelangen Jul 3, 2023
581e422
change to jsdoc over normal code-comment
ndelangen Jul 3, 2023
a45faea
improve types of manager api add & addPanel
ndelangen Jul 3, 2023
8972649
improve api for manager addon registration, deprecate the extra `id` …
ndelangen Jul 3, 2023
5b43645
Merge branch 'next' into norbert/ui-tabs-types-improvements
ndelangen Jul 4, 2023
db062cc
Merge branch 'release/7.2' into norbert/ui-tabs-types-improvements
ndelangen Jul 4, 2023
8afab86
Merge branch 'release/7.2' into norbert/ui-tabs-types-improvements
ndelangen Jul 4, 2023
e6bbdee
Merge branch 'release/7.2' into norbert/ui-tabs-types-improvements
ndelangen Jul 4, 2023
ca90ce3
Merge branch 'release/7.2' into norbert/ui-tabs-types-improvements
ndelangen Jul 5, 2023
54eae22
cleanup
ndelangen Jul 6, 2023
c429c1f
make it prettier
ndelangen Jul 6, 2023
e5189a1
Merge branch 'norbert/ui-tabs-types-improvements' of github.com:story…
ndelangen Jul 6, 2023
909a851
simplify code
ndelangen Jul 6, 2023
967e09d
Merge branch 'release/7.2' into norbert/ui-tabs-types-improvements
ndelangen Jul 6, 2023
f32ead4
cleanup
ndelangen Jul 6, 2023
b7cee93
fix
ndelangen Jul 6, 2023
9facdc3
Merge branch 'release/7.2' into norbert/ui-tabs-types-improvements
ndelangen Jul 6, 2023
2964e4b
fix
ndelangen Jul 6, 2023
3f2bc43
cleanup
ndelangen Jul 6, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 0 additions & 1 deletion code/addons/a11y/src/manager.tsx
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
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
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
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
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
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
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
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
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
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
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
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
): 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
@@ -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 {
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
title: FCWithoutChildren | string | ReactElement | ReactNode;
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
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
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
@@ -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;
ndelangen marked this conversation as resolved.
Show resolved Hide resolved
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
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