diff --git a/code/package.json b/code/package.json index 0aba4dca7f3..a899b4a3c4a 100644 --- a/code/package.json +++ b/code/package.json @@ -76,9 +76,9 @@ "publish:debug": "npm run publish:latest -- --npm-tag=debug --no-push", "publish:latest": "lerna publish --exact --concurrency 1 --force-publish", "publish:next": "npm run publish:latest -- --npm-tag=next", - "storybook:blocks": "BLOCKS_ONLY=true yarn storybook:ui", - "storybook:blocks:build": "BLOCKS_ONLY=true yarn storybook:ui:build", - "storybook:blocks:chromatic": "BLOCKS_ONLY=true yarn storybook:ui:chromatic --project-token=${CHROMATIC_TOKEN_STORYBOOK_BLOCKS:-MISSING_PROJECT_TOKEN}", + "storybook:blocks": "STORYBOOK_BLOCKS_ONLY=true yarn storybook:ui", + "storybook:blocks:build": "STORYBOOK_BLOCKS_ONLY=true yarn storybook:ui:build", + "storybook:blocks:chromatic": "STORYBOOK_BLOCKS_ONLY=true yarn storybook:ui:chromatic --project-token=${CHROMATIC_TOKEN_STORYBOOK_BLOCKS:-MISSING_PROJECT_TOKEN}", "storybook:ui": "NODE_OPTIONS=\"--preserve-symlinks --preserve-symlinks-main\" ./lib/cli/bin/index.js dev --port 6006 --config-dir ./ui/.storybook --no-manager-cache", "storybook:ui:build": "NODE_OPTIONS=\"--preserve-symlinks --preserve-symlinks-main\" ./lib/cli/bin/index.js build --config-dir ./ui/.storybook", "storybook:ui:chromatic": "yarn chromatic --build-script-name storybook:ui:build --storybook-config-dir ./ui/.storybook --storybook-base-dir ./code/ui --project-token=${CHROMATIC_TOKEN_STORYBOOK_UI:-MISSING_PROJECT_TOKEN} --only-changed --exit-zero-on-changes --exit-once-uploaded", diff --git a/code/ui/.storybook/main.ts b/code/ui/.storybook/main.ts index 62affdded35..26d61a6251b 100644 --- a/code/ui/.storybook/main.ts +++ b/code/ui/.storybook/main.ts @@ -2,7 +2,7 @@ import { vite as csfPlugin } from '@storybook/csf-plugin'; import pluginTurbosnap from 'vite-plugin-turbosnap'; import type { StorybookConfig } from '../../frameworks/react-vite/dist'; -const isBlocksOnly = process.env.BLOCKS_ONLY === 'true'; +const isBlocksOnly = process.env.STORYBOOK_BLOCKS_ONLY === 'true'; const allStories = [ { @@ -56,6 +56,9 @@ const config: StorybookConfig = { core: { disableTelemetry: true, }, + features: { + interactionsDebugger: true, + }, viteFinal: (viteConfig, { configType }) => ({ ...viteConfig, plugins: [ diff --git a/code/ui/blocks/src/blocks/Story.stories.tsx b/code/ui/blocks/src/blocks/Story.stories.tsx index bb09d8c635d..38ad9646acf 100644 --- a/code/ui/blocks/src/blocks/Story.stories.tsx +++ b/code/ui/blocks/src/blocks/Story.stories.tsx @@ -1,3 +1,5 @@ +/// +import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { Story as StoryComponent } from './Story'; @@ -6,15 +8,109 @@ import * as BooleanStories from '../controls/Boolean.stories'; const meta: Meta = { component: StoryComponent, parameters: { - relativeCsfPaths: ['../controls/Boolean.stories'], + relativeCsfPaths: ['../controls/Boolean.stories', '../blocks/Story.stories'], }, }; export default meta; type Story = StoryObj; -export const BasicOf: Story = { +export const Of: Story = { args: { of: BooleanStories.Undefined, }, }; + +export const OfWithMeta: Story = { + args: { + of: BooleanStories.True, + meta: BooleanStories.default, + }, +}; + +const blocksAwareId = `${ + import.meta.env.STORYBOOK_BLOCKS_ONLY === 'true' ? '' : 'storybook-blocks-' +}controls-boolean--false`; + +export const Id: Story = { + args: { + id: blocksAwareId, + }, +}; + +export const Name: Story = { + args: { + name: 'True', + }, +}; + +export const SimpleSizeTest: Story = { + render: () => { + return ( +
+

+ This story does nothing. Its only purpose is to show how its size renders in different + conditions (inline/iframe/fixed height) when used in a {''} block. +

+

+ It has a fixed height of 1000px and a fixed width{' '} + of 800px +

+
+ ); + }, +}; + +export const Inline: Story = { + args: { + of: SimpleSizeTest, + inline: true, + }, +}; +export const InlineWithHeight: Story = { + ...Inline, + args: { + of: SimpleSizeTest, + inline: true, + height: '300px', + }, +}; +export const Iframe: Story = { + ...Inline, + args: { + of: SimpleSizeTest, + inline: false, + }, +}; +export const IframeWithHeight: Story = { + ...Inline, + args: { + of: SimpleSizeTest, + inline: false, + height: '300px', + }, +}; + +export const WithDefaultInteractions: Story = { + args: { + of: BooleanStories.Toggling, + }, +}; +export const WithInteractionsAutoplayInStory: Story = { + args: { + of: BooleanStories.TogglingInDocs, + }, +}; + +// TODO: types suggest that can take ProjectAnnotations, but it doesn't seem to do anything with them +// Such as parameters, decorators, etc. +// they seem to be taken from the story itself, and not from the call diff --git a/code/ui/blocks/src/blocks/Story.tsx b/code/ui/blocks/src/blocks/Story.tsx index daca709e6b2..53f29b73853 100644 --- a/code/ui/blocks/src/blocks/Story.tsx +++ b/code/ui/blocks/src/blocks/Story.tsx @@ -1,4 +1,4 @@ -import type { FC, ReactNode, ElementType, ComponentProps } from 'react'; +import type { FC, ComponentProps } from 'react'; import React, { useContext, useRef, useEffect, useState } from 'react'; import type { Renderer, @@ -25,7 +25,6 @@ type CommonProps = StoryAnnotations & { type StoryDefProps = { name: string; - children: ReactNode; }; type StoryRefProps = { @@ -36,7 +35,6 @@ type StoryRefProps = { type StoryImportProps = { name: string; - story: ElementType; }; export type StoryProps = (StoryDefProps | StoryRefProps | StoryImportProps) & CommonProps; @@ -49,8 +47,7 @@ export const getStoryId = (props: StoryProps, context: DocsContextProps): StoryI } const { name } = props as StoryDefProps; - const inputId = id; - return inputId || context.storyIdByName(name); + return id || context.storyIdByName(name); }; export const getStoryProps = ( @@ -87,14 +84,16 @@ const Story: FC = (props) => { const [showLoader, setShowLoader] = useState(true); useEffect(() => { - let cleanup: () => void; - if (story && storyRef.current) { - const element = storyRef.current as HTMLElement; - const { autoplay } = story.parameters.docs || {}; - cleanup = context.renderStoryToElement(story, element, { autoplay }); - setShowLoader(false); + if (!(story && storyRef.current)) { + return () => {}; } - return () => cleanup && cleanup(); + const element = storyRef.current as HTMLElement; + const { autoplay } = story.parameters.docs || {}; + const cleanup = context.renderStoryToElement(story, element, { autoplay }); + setShowLoader(false); + return () => { + cleanup(); + }; }, [context, story]); if (!story) { @@ -115,7 +114,7 @@ const Story: FC = (props) => { return (
{height ? ( - + ) : null} {showLoader && }
= (props) => { ); }; -Story.defaultProps = { - children: null, - name: null, -}; - export { Story }; diff --git a/code/ui/blocks/src/controls/Boolean.stories.tsx b/code/ui/blocks/src/controls/Boolean.stories.tsx index 726ab58e9b3..82458c745a4 100644 --- a/code/ui/blocks/src/controls/Boolean.stories.tsx +++ b/code/ui/blocks/src/controls/Boolean.stories.tsx @@ -1,10 +1,20 @@ +import { expect } from '@storybook/jest'; import type { Meta, StoryObj } from '@storybook/react'; +import { within, fireEvent } from '@storybook/testing-library'; +import { addons } from '@storybook/addons'; +import { RESET_STORY_ARGS, STORY_ARGS_UPDATED } from '@storybook/core-events'; import { BooleanControl } from './Boolean'; const meta = { component: BooleanControl, tags: ['docsPage'], - parameters: { withRawArg: 'value', controls: { include: ['value'] } }, + parameters: { + withRawArg: 'value', + controls: { include: ['value'] }, + notes: 'These are notes for the Boolean control stories', + info: 'This is info for the Boolean control stories', + jsx: { useBooleanShorthandSyntax: false }, + }, args: { name: 'boolean' }, } as Meta; @@ -26,3 +36,45 @@ export const Undefined: StoryObj = { value: undefined, }, }; + +export const Toggling: StoryObj = { + args: { + value: undefined, + }, + play: async ({ canvasElement, id }) => { + const channel = addons.getChannel(); + + channel.emit(RESET_STORY_ARGS, { storyId: id }); + await new Promise((resolve) => { + channel.once(STORY_ARGS_UPDATED, resolve); + }); + + const canvas = within(canvasElement); + + // from Undefined to False + const setBooleanControl = canvas.getByText('Set boolean'); + await fireEvent.click(setBooleanControl); + + let toggle = await canvas.findByTitle('Change to true'); + expect(toggle).toBeInTheDocument(); + + // from False to True + await fireEvent.click(toggle); + toggle = await canvas.findByTitle('Change to false'); + expect(toggle).toBeInTheDocument(); + + // from True to False + await fireEvent.click(toggle); + toggle = await canvas.findByTitle('Change to true'); + expect(toggle).toBeInTheDocument(); + }, +}; + +export const TogglingInDocs: StoryObj = { + ...Toggling, + parameters: { + docs: { + autoplay: true, + }, + }, +}; diff --git a/code/ui/blocks/src/controls/Boolean.tsx b/code/ui/blocks/src/controls/Boolean.tsx index 8abec1e3490..556701febca 100644 --- a/code/ui/blocks/src/controls/Boolean.tsx +++ b/code/ui/blocks/src/controls/Boolean.tsx @@ -86,6 +86,11 @@ const Label = styled.label(({ theme }) => ({ const parse = (value: string | null): boolean => value === 'true'; export type BooleanProps = ControlProps & BooleanConfig; +/** + * # Boolean control + * Renders a switch toggle with "True" or "False". + * or if the value is `undefined`, renders a button to set the boolean. + */ export const BooleanControl: FC = ({ name, value, onChange, onBlur, onFocus }) => { const onSetFalse = useCallback(() => onChange(false), [onChange]); if (value === undefined) { @@ -95,13 +100,14 @@ export const BooleanControl: FC = ({ name, value, onChange, onBlur ); } + const controlId = getControlId(name); const parsedValue = typeof value === 'string' ? parse(value) : value; return ( -