diff --git a/addons/docs/src/blocks/ArgsTable.tsx b/addons/docs/src/blocks/ArgsTable.tsx index 92b8b74b784f..3820b6ea00bf 100644 --- a/addons/docs/src/blocks/ArgsTable.tsx +++ b/addons/docs/src/blocks/ArgsTable.tsx @@ -162,9 +162,7 @@ export const StoryTable: FC< const story = useStory(storyId, context); // eslint-disable-next-line prefer-const let [args, updateArgs, resetArgs] = useArgs(storyId, context); - if (!story) { - return
Loading...
; - } + if (!story) return ; const argTypes = filterArgTypes(story.argTypes, include, exclude); diff --git a/addons/docs/src/blocks/Canvas.tsx b/addons/docs/src/blocks/Canvas.tsx index 42cfb0f31b1a..bf5362857f0c 100644 --- a/addons/docs/src/blocks/Canvas.tsx +++ b/addons/docs/src/blocks/Canvas.tsx @@ -5,6 +5,7 @@ import { resetComponents, Preview as PurePreview, PreviewProps as PurePreviewProps, + PreviewSkeleton, } from '@storybook/components'; import { DocsContext, DocsContextProps } from './DocsContext'; import { SourceContext, SourceContextProps } from './SourceContainer'; @@ -71,7 +72,9 @@ export const Canvas: FC = (props) => { const { isLoading, previewProps } = getPreviewProps(props, docsContext, sourceContext); const { children } = props; - return isLoading ? null : ( + if (isLoading) return ; + + return ( {children} diff --git a/addons/docs/src/blocks/Story.tsx b/addons/docs/src/blocks/Story.tsx index 59433f7ab469..65fdb19c1e72 100644 --- a/addons/docs/src/blocks/Story.tsx +++ b/addons/docs/src/blocks/Story.tsx @@ -6,10 +6,11 @@ import React, { useContext, useRef, useEffect, + useState, } from 'react'; import { MDXProvider } from '@mdx-js/react'; import global from 'global'; -import { resetComponents, Story as PureStory } from '@storybook/components'; +import { resetComponents, Story as PureStory, StorySkeleton } from '@storybook/components'; import { StoryId, toId, storyNameFromExport, StoryAnnotations, AnyFramework } from '@storybook/csf'; import { Story as StoryType } from '@storybook/store'; import { addons } from '@storybook/addons'; @@ -113,13 +114,14 @@ export const getStoryProps = ( const Story: FunctionComponent = (props) => { const context = useContext(DocsContext); const channel = addons.getChannel(); - const ref = useRef(); + const storyRef = useRef(); const storyId = getStoryId(props, context); const story = useStory(storyId, context); + const [showLoader, setShowLoader] = useState(true); useEffect(() => { let cleanup: () => void; - if (story && ref.current) { + if (story && storyRef.current) { const { componentId, id, title, name } = story; const renderContext = { componentId, @@ -136,14 +138,15 @@ const Story: FunctionComponent = (props) => { cleanup = context.renderStoryToElement({ story, renderContext, - element: ref.current as HTMLElement, + element: storyRef.current as HTMLElement, }); + setShowLoader(false); } return () => cleanup && cleanup(); }, [story]); if (!story) { - return
Loading...
; + return ; } // If we are rendering a old-style inline Story via `PureStory` below, we want to emit @@ -158,7 +161,7 @@ const Story: FunctionComponent = (props) => { if (global?.FEATURES?.modernInlineRender) { // We do this so React doesn't complain when we replace the span in a secondary render - const htmlContents = `loading story...`; + const htmlContents = ``; // FIXME: height/style/etc. lifted from PureStory const { height } = storyProps; @@ -168,8 +171,9 @@ const Story: FunctionComponent = (props) => { {height ? ( ) : null} + {showLoader && }
diff --git a/addons/docs/src/blocks/useStory.ts b/addons/docs/src/blocks/useStory.ts index e6266744eeb7..86bab14ea429 100644 --- a/addons/docs/src/blocks/useStory.ts +++ b/addons/docs/src/blocks/useStory.ts @@ -16,7 +16,12 @@ export function useStories( storyIds: StoryId[], context: DocsContextProps ): (Story | void)[] { - const [storiesById, setStories] = useState({} as Record>); + const initialStoriesById = context.componentStories().reduce((acc, story) => { + acc[story.id] = story; + return acc; + }, {} as Record>); + + const [storiesById, setStories] = useState(initialStoriesById as typeof initialStoriesById); useEffect(() => { Promise.all( diff --git a/lib/components/src/bar/button.stories.tsx b/lib/components/src/bar/button.stories.tsx index ae17afe72180..0e0d03a580f8 100644 --- a/lib/components/src/bar/button.stories.tsx +++ b/lib/components/src/bar/button.stories.tsx @@ -1,6 +1,6 @@ import React from 'react'; -import { IconButton } from './button'; +import { IconButton, IconButtonSkeleton } from './button'; import { Icons } from '../icon/icon'; export default { @@ -8,7 +8,9 @@ export default { title: 'Basics/IconButton', }; -/* eslint-disable-next-line no-underscore-dangle */ +export const Loading = () => ; + +// eslint-disable-next-line no-underscore-dangle export const _IconButton = () => ( diff --git a/lib/components/src/bar/button.tsx b/lib/components/src/bar/button.tsx index 42bc49f7bd62..a24b53f6bcd7 100644 --- a/lib/components/src/bar/button.tsx +++ b/lib/components/src/bar/button.tsx @@ -124,3 +124,20 @@ export const IconButton = styled(ButtonOrLink, { shouldForwardProp: isPropValid } ); IconButton.displayName = 'IconButton'; + +const IconPlaceholder = styled.div(({ theme }) => ({ + width: 14, + height: 14, + backgroundColor: theme.appBorderColor, + animation: `${theme.animation.glow} 1.5s ease-in-out infinite`, +})); + +const IconButtonSkeletonWrapper = styled.div(() => ({ + padding: 5, +})); + +export const IconButtonSkeleton = () => ( + + + +); diff --git a/lib/components/src/blocks/ArgsTable/ArgRow.stories.tsx b/lib/components/src/blocks/ArgsTable/ArgRow.stories.tsx index d6ffb2b42d3d..39f7cab987ef 100644 --- a/lib/components/src/blocks/ArgsTable/ArgRow.stories.tsx +++ b/lib/components/src/blocks/ArgsTable/ArgRow.stories.tsx @@ -23,6 +23,8 @@ const Template = (args) => ; const baseArgs = { updateArgs: action('updateArgs'), }; +export const Loading = Template.bind({}); +Loading.args = { isLoading: true }; export const String = Template.bind({}); String.args = { diff --git a/lib/components/src/blocks/ArgsTable/ArgRow.tsx b/lib/components/src/blocks/ArgsTable/ArgRow.tsx index e62aef714004..3087fe418efa 100644 --- a/lib/components/src/blocks/ArgsTable/ArgRow.tsx +++ b/lib/components/src/blocks/ArgsTable/ArgRow.tsx @@ -8,7 +8,7 @@ import { ArgValue } from './ArgValue'; import { ArgControl, ArgControlProps } from './ArgControl'; import { codeCommon } from '../../typography/shared'; -export interface ArgRowProps { +interface ArgRowData { row: ArgType; arg: any; updateArgs?: (args: Args) => void; @@ -17,6 +17,17 @@ export interface ArgRowProps { initialExpandedArgs?: boolean; } +interface ArgRowLoading { + isLoading: true; +} + +export const argRowLoadingData: ArgRowData = { + row: { name: 'loading', description: 'loading' }, + arg: 0, +}; + +export type ArgRowProps = ArgRowData | ArgRowLoading; + const Name = styled.span({ fontWeight: 'bold' }); const Required = styled.span(({ theme }) => ({ @@ -73,7 +84,9 @@ const StyledTd = styled.td<{ expandable: boolean }>(({ theme, expandable }) => ( })); export const ArgRow: FC = (props) => { - const { row, updateArgs, compact, expandable, initialExpandedArgs } = props; + // const isLoading = 'isLoading' in props; + const { row, updateArgs, compact, expandable, initialExpandedArgs } = + 'row' in props ? props : argRowLoadingData; const { name, description } = row; const table = (row.table || {}) as TableAnnotation; const type = table.type || row.type; diff --git a/lib/components/src/blocks/ArgsTable/ArgsTable.stories.tsx b/lib/components/src/blocks/ArgsTable/ArgsTable.stories.tsx index 46f9e2ca644a..dc9bff0a1a11 100644 --- a/lib/components/src/blocks/ArgsTable/ArgsTable.stories.tsx +++ b/lib/components/src/blocks/ArgsTable/ArgsTable.stories.tsx @@ -27,6 +27,8 @@ const longEnumType = ArgRow.LongEnum.args.row; const Template = (args) => ; +export const Loading = Template.bind({}); +Loading.args = { isLoading: true }; export const Normal = Template.bind({}); Normal.args = { rows: { diff --git a/lib/components/src/blocks/ArgsTable/ArgsTable.tsx b/lib/components/src/blocks/ArgsTable/ArgsTable.tsx index c71f38bdbfb7..cb1a63a3144b 100644 --- a/lib/components/src/blocks/ArgsTable/ArgsTable.tsx +++ b/lib/components/src/blocks/ArgsTable/ArgsTable.tsx @@ -3,7 +3,7 @@ import pickBy from 'lodash/pickBy'; import { styled, ignoreSsrWarning } from '@storybook/theming'; import { opacify, transparentize, darken, lighten } from 'polished'; import { Icons } from '../../icon/icon'; -import { ArgRow } from './ArgRow'; +import { ArgRow, argRowLoadingData } from './ArgRow'; import { SectionRow } from './SectionRow'; import { ArgType, ArgTypes, Args } from './types'; import { EmptyBlock } from '../EmptyBlock'; @@ -240,7 +240,7 @@ const sortFns: Record = { Number(!!b.type?.required) - Number(!!a.type?.required) || a.name.localeCompare(b.name), none: undefined, }; -export interface ArgsTableRowProps { +export interface ArgsTableData { rows: ArgTypes; args?: Args; updateArgs?: (args: Args) => void; @@ -248,14 +248,26 @@ export interface ArgsTableRowProps { compact?: boolean; inAddonPanel?: boolean; initialExpandedArgs?: boolean; + isLoading?: boolean; sort?: SortType; } export interface ArgsTableErrorProps { error: ArgsTableError; } +interface ArgTableLoading { + isLoading: true; +} + +export const argTableLoadingData: ArgsTableData = { + rows: { + row1: argRowLoadingData.row, + row2: argRowLoadingData.row, + row3: argRowLoadingData.row, + }, +}; -export type ArgsTableProps = ArgsTableRowProps | ArgsTableErrorProps; +export type ArgsTableProps = ArgsTableData | ArgsTableErrorProps | ArgTableLoading; type Rows = ArgType[]; type Subsection = Rows; @@ -326,16 +338,139 @@ const groupRows = (rows: ArgType, sort: SortType) => { return sorted; }; +const SkeletonHeader = styled.div(({ theme }) => ({ + alignContent: 'stretch', + display: 'flex', + gap: 16, + marginTop: 25, + padding: '10px 20px', + + div: { + animation: `${theme.animation.glow} 1.5s ease-in-out infinite`, + background: theme.appBorderColor, + flexShrink: 0, + height: 20, + + '&:first-child, &:nth-child(4)': { + width: '20%', + }, + + '&:nth-child(2)': { + width: '30%', + }, + + '&:nth-child(3)': { + flexGrow: 1, + }, + + '&:last-child': { + width: 30, + }, + + '@media ( max-width: 500px )': { + '&:nth-child( n + 4 )': { + display: 'none', + }, + }, + }, +})); + +const SkeletonBody = styled.div(({ theme }) => ({ + background: theme.background.content, + boxShadow: + theme.base === 'light' + ? `rgba(0, 0, 0, 0.10) 0 1px 3px 1px, + ${transparentize(0.035, theme.appBorderColor)} 0 0 0 1px` + : `rgba(0, 0, 0, 0.20) 0 2px 5px 1px, + ${opacify(0.05, theme.appBorderColor)} 0 0 0 1px`, + borderRadius: theme.appBorderRadius, + + '> div': { + alignContent: 'stretch', + borderTopColor: + theme.base === 'light' + ? darken(0.1, theme.background.content) + : lighten(0.05, theme.background.content), + borderTopStyle: 'solid', + borderTopWidth: 1, + display: 'flex', + gap: 16, + padding: 20, + + '&:first-child': { + borderTop: 0, + }, + }, + + '> div div': { + animation: `${theme.animation.glow} 1.5s ease-in-out infinite`, + background: theme.appBorderColor, + flexShrink: 0, + height: 20, + + '&:first-child': { + width: '20%', + }, + + '&:nth-child(2)': { + width: '30%', + }, + + '&:nth-child(3)': { + flexGrow: 1, + }, + + '&:last-child': { + width: 'calc(20% + 47px)', + + '@media ( max-width: 500px )': { + display: 'none', + }, + }, + }, +})); + +const Skeleton = () => ( +
+ +
+
+
+
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+); + /** * Display the props for a component as a props table. Each row is a collection of * ArgDefs, usually derived from docgen info for the component. */ export const ArgsTable: FC = (props) => { - const { error } = props as ArgsTableErrorProps; - if (error) { + if ('error' in props) { return ( - {error}  + {props.error}  Read the docs @@ -343,6 +478,7 @@ export const ArgsTable: FC = (props) => { ); } + const isLoading = 'isLoading' in props; const { rows, args, @@ -352,7 +488,11 @@ export const ArgsTable: FC = (props) => { inAddonPanel, initialExpandedArgs, sort = 'none', - } = props as ArgsTableRowProps; + } = 'rows' in props ? props : argTableLoadingData; + + if (isLoading) { + return ; + } const groups = groupRows( pickBy(rows, (row) => !row?.table?.disable), @@ -380,15 +520,16 @@ export const ArgsTable: FC = (props) => { const expandable = Object.keys(groups.sections).length > 0; const common = { updateArgs, compact, inAddonPanel, initialExpandedArgs }; + return ( Name - {compact ? null : Description} - {compact ? null : Default} - {updateArgs ? ( + {compact || Description} + {compact || Default} + {updateArgs && ( Control{' '} @@ -399,7 +540,8 @@ export const ArgsTable: FC = (props) => { )} - ) : null} + )} + {null} diff --git a/lib/components/src/blocks/DocsPage.stories.tsx b/lib/components/src/blocks/DocsPage.stories.tsx index dabf27dbd760..3b55d6ea3fe4 100644 --- a/lib/components/src/blocks/DocsPage.stories.tsx +++ b/lib/components/src/blocks/DocsPage.stories.tsx @@ -34,6 +34,19 @@ export default { }, }; +export const Loading = () => ( + + DocsPage + + What the DocsPage looks like. Meant to be QAed in Canvas tab not in Docs tab. + + + + + + +); + export const WithSubtitle = () => ( DocsPage diff --git a/lib/components/src/blocks/Preview.stories.tsx b/lib/components/src/blocks/Preview.stories.tsx index 98ce4a763c74..b042ccfa7266 100644 --- a/lib/components/src/blocks/Preview.stories.tsx +++ b/lib/components/src/blocks/Preview.stories.tsx @@ -3,7 +3,7 @@ import { styled } from '@storybook/theming'; import global from 'global'; import { Spaced } from '../spaced/Spaced'; -import { Preview } from './Preview'; +import { Preview, PreviewSkeleton } from './Preview'; import { Story } from './Story'; import { Button } from '../Button/Button'; import * as Source from './Source.stories'; @@ -15,6 +15,8 @@ export default { component: Preview, }; +export const Loading = () => ; + export const CodeCollapsed = () => ( diff --git a/lib/components/src/blocks/Preview.tsx b/lib/components/src/blocks/Preview.tsx index 9b8c07c707ee..1ee62bc0956a 100644 --- a/lib/components/src/blocks/Preview.tsx +++ b/lib/components/src/blocks/Preview.tsx @@ -17,6 +17,7 @@ import { ActionBar, ActionItem } from '../ActionBar/ActionBar'; import { Toolbar } from './Toolbar'; import { ZoomContext } from './ZoomContext'; import { Zoom } from '../Zoom/Zoom'; +import { StorySkeleton } from '.'; export interface PreviewProps { isColumn?: boolean; @@ -189,7 +190,7 @@ const getLayout = (children: ReactElement[]): layout => { * items. The preview also shows the source for the component * as a drop-down. */ -const Preview: FunctionComponent = ({ +export const Preview: FunctionComponent = ({ isColumn, columns, children, @@ -284,4 +285,8 @@ const Preview: FunctionComponent = ({ ); }; -export { Preview }; +export const PreviewSkeleton = () => ( + + + +); diff --git a/lib/components/src/blocks/Source.stories.tsx b/lib/components/src/blocks/Source.stories.tsx index 5af0dfe0710d..a3dc80c2c859 100644 --- a/lib/components/src/blocks/Source.stories.tsx +++ b/lib/components/src/blocks/Source.stories.tsx @@ -6,6 +6,11 @@ export default { component: Source, }; +export const Loading = (args) => ; +Loading.args = { + isLoading: true, +}; + const jsxCode = ` a.id} /> diff --git a/lib/components/src/blocks/Source.tsx b/lib/components/src/blocks/Source.tsx index 546c2d9ab98a..d8c0d6e73dcf 100644 --- a/lib/components/src/blocks/Source.tsx +++ b/lib/components/src/blocks/Source.tsx @@ -24,6 +24,7 @@ export enum SourceError { } interface SourceErrorProps { + isLoading?: boolean; error?: SourceError; } @@ -34,6 +35,37 @@ interface SourceCodeProps { dark?: boolean; } +const SourceSkeletonWrapper = styled.div<{}>(({ theme }) => ({ + background: theme.background.content, + borderRadius: theme.appBorderRadius, + border: `1px solid ${theme.appBorderColor}`, + boxShadow: + theme.base === 'light' ? 'rgba(0, 0, 0, 0.10) 0 1px 3px 0' : 'rgba(0, 0, 0, 0.20) 0 2px 5px 0', + margin: '25px 0 40px', + padding: '20px 20px 20px 22px', +})); + +const SourceSkeletonPlaceholder = styled.div<{}>(({ theme }) => ({ + animation: `${theme.animation.glow} 1.5s ease-in-out infinite`, + background: theme.appBorderColor, + height: 17, + marginTop: 1, + width: '60%', + + '&:first-child': { + margin: 0, + }, +})); + +const SourceSkeleton = () => ( + + + + + + +); + // FIXME: Using | causes a typescript error, so stubbing it with & for now // and making `error` optional export type SourceProps = SourceErrorProps & SourceCodeProps; @@ -42,7 +74,10 @@ export type SourceProps = SourceErrorProps & SourceCodeProps; * Syntax-highlighted source code for a component (or anything!) */ const Source: FunctionComponent = (props) => { - const { error } = props as SourceErrorProps; + const { isLoading, error } = props as SourceErrorProps; + if (isLoading) { + return ; + } if (error) { return {error}; } diff --git a/lib/components/src/blocks/Story.stories.tsx b/lib/components/src/blocks/Story.stories.tsx index 3d2bacc90cec..79136501df5c 100644 --- a/lib/components/src/blocks/Story.stories.tsx +++ b/lib/components/src/blocks/Story.stories.tsx @@ -1,5 +1,5 @@ import React, { useState } from 'react'; -import { Story, StoryError } from './Story'; +import { Story, StorySkeleton, StoryError } from './Story'; import { Button } from '../Button/Button'; export default { @@ -18,8 +18,10 @@ const buttonHookFn = () => { ); }; -export const Inline = () => ; +export const Loading = () => ; -export const Error = () => ; +export const Inline = () => ; -export const ReactHook = () => ; +export const Error = () => ; + +export const ReactHook = () => ; diff --git a/lib/components/src/blocks/Story.tsx b/lib/components/src/blocks/Story.tsx index 2a44d1b8d10b..e32962a6242b 100644 --- a/lib/components/src/blocks/Story.tsx +++ b/lib/components/src/blocks/Story.tsx @@ -5,6 +5,7 @@ import type { Parameters } from '@storybook/api'; import { IFrame } from './IFrame'; import { EmptyBlock } from './EmptyBlock'; import { ZoomContext } from './ZoomContext'; +import { Loader } from '..'; const BASE_URL = 'iframe.html'; @@ -19,7 +20,7 @@ export enum StoryError { const MISSING_STORY = (id?: string) => (id ? `Story "${id}" doesn't exist.` : StoryError.NO_STORY); interface CommonProps { - title: string; + title?: string; height?: string; id: string; } @@ -72,7 +73,7 @@ const IFrameStory: FunctionComponent = ({ id, title, height = * A story element, either rendered inline or in an iframe, * with configurable height. */ -const Story: FunctionComponent = ({ +const Story: FunctionComponent = ({ children, error, inline, @@ -90,4 +91,6 @@ const Story: FunctionComponent ; + +export { Story, StorySkeleton }; diff --git a/lib/core-common/src/templates/base-preview-body.html b/lib/core-common/src/templates/base-preview-body.html index 71c0b67fde9b..43970c2a9d7f 100644 --- a/lib/core-common/src/templates/base-preview-body.html +++ b/lib/core-common/src/templates/base-preview-body.html @@ -1,3 +1,231 @@ + + +
+
+
+ +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

No Preview

@@ -6,7 +234,9 @@

No Preview

  • Please check the Storybook config.
  • Try reloading the page.
  • -

    If the problem persists, check the browser console, or the terminal you've run Storybook from.

    +

    + If the problem persists, check the browser console, or the terminal you've run Storybook from. +

    diff --git a/lib/core-common/src/templates/base-preview-head.html b/lib/core-common/src/templates/base-preview-head.html index 0af7eb67d9e4..def373e8608d 100644 --- a/lib/core-common/src/templates/base-preview-head.html +++ b/lib/core-common/src/templates/base-preview-head.html @@ -1,7 +1,10 @@ - +