diff --git a/.storybook/docs-root.css b/.storybook/docs-root.css index 5d0ddbe07e558d..e63734059f187a 100644 --- a/.storybook/docs-root.css +++ b/.storybook/docs-root.css @@ -139,51 +139,6 @@ display: none; } -/* Remove z-index from "show code" button container */ -/* https://github.com/microsoft/fluentui/issues/22773 */ -.docs-story > div:nth-child(2) { - z-index: auto; -} - -#docs-root .docblock-code-toggle, -.docs-story .with-code-sandbox-button { - font-family: 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', - sans-serif; - min-width: 91px; - font-size: 14px !important; - font-weight: 600 !important; - margin-right: 32px !important; - line-height: 150% !important; - color: #201f1e !important; - text-align: center !important; - justify-content: center !important; - letter-spacing: -0.01em !important; - background: #f8f8f8 !important; - border: none !important; - /* box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25) !important; */ - border-radius: 5px 5px 0px 0px !important; -} - -/* Reduce font size of CodeSandbox and Show Code button when zoomed or small window width*/ -/* https://github.com/microsoft/fluentui/issues/22764 */ - -@media screen and (max-width: 380px) { - #docs-root .docblock-code-toggle, - .docs-story .with-code-sandbox-button { - font-size: 10px !important; - } -} - -/* Make storybook codesandbox export button match Figma design */ -.docs-story .with-code-sandbox-button { - right: 105px !important; -} - -.docs-story .with-code-sandbox-button:focus { - outline: none; - box-shadow: #1ea7fd 0 -3px 0 0 inset; -} - #docs-root span + .sbdocs .docblock-argstable tbody tr td button { color: #0078d4; color: red; diff --git a/.storybook/preview.js b/.storybook/preview.js index 18cf20e64464c4..7547509906f6ce 100644 --- a/.storybook/preview.js +++ b/.storybook/preview.js @@ -1,8 +1,10 @@ import 'cypress-storybook/react'; -import * as dedent from 'dedent'; import './docs-root.css'; +import '../packages/react-components/react-storybook-addon-codesandbox/src/styles.css'; import { withLinks } from '@storybook/addon-links'; +/** @typedef {import('../packages/react-components/react-storybook-addon-codesandbox/src/public-types').ParametersExtension & import('@storybook/addons').Parameters} Parameters */ + // This patches globals set up by cypress-storybook to work around its usage of the deprecated // forceReRender API that no longer works with storyStoreV7 // https://github.com/NicholasBoll/cypress-storybook/issues/46 @@ -25,7 +27,7 @@ window.__setCurrentStory = function (categorization, story) { /** @type {NonNullable} */ export const decorators = [withLinks]; -/** @type {import('@storybook/addons').Parameters} */ +/** @type {Parameters} */ export const parameters = { viewMode: 'docs', controls: { @@ -41,31 +43,18 @@ export const parameters = { // (@fluentui/babel-preset-storybook-full-source). transformSource: (snippet, story) => story.parameters.fullSource, }, - exportToCodeSandbox: { + exportToSandbox: { + provider: 'codesandbox-browser', + bundler: 'cra', requiredDependencies: { // for React react: '^17', 'react-dom': '^17', - // necessary when using typescript in CodeSandbox - 'react-scripts': 'latest', // necessary for FluentProvider: '@fluentui/react-components': '^9.0.0', }, optionalDependencies: { '@fluentui/react-icons': 'latest', }, - indexTsx: dedent` - import * as ReactDOM from 'react-dom'; - import { FluentProvider, webLightTheme } from '@fluentui/react-components'; - import { STORY_NAME as Example } from './example'; - // - // You can edit this example in "example.tsx". - // - ReactDOM.render( - - - , - document.getElementById('root'), - );`, }, }; diff --git a/packages/react-components/react-storybook-addon-codesandbox/README.md b/packages/react-components/react-storybook-addon-codesandbox/README.md index ce8be8f00e7ee6..c34d963b557282 100644 --- a/packages/react-components/react-storybook-addon-codesandbox/README.md +++ b/packages/react-components/react-storybook-addon-codesandbox/README.md @@ -2,6 +2,6 @@ **React Storybook Addon Codesandbox for [Fluent UI React](https://developer.microsoft.com/en-us/fluentui)** -Storybook addon that enables codesandbox exports for stories +Storybook addon that enables codesandbox or stackblitz exports for stories TODO: Add more details! diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/getDependencies.test.ts b/packages/react-components/react-storybook-addon-codesandbox/src/getDependencies.test.ts index e4d000aaf17ea6..e1480b431a063f 100644 --- a/packages/react-components/react-storybook-addon-codesandbox/src/getDependencies.test.ts +++ b/packages/react-components/react-storybook-addon-codesandbox/src/getDependencies.test.ts @@ -1,4 +1,4 @@ -import { getDependencies } from './getDepdencies'; +import { getDependencies } from './getDependencies'; describe('getDependencies', () => { it('should find all dependencies in a file', () => { @@ -39,6 +39,16 @@ describe('getDependencies', () => { }); }); + it('versions in optionalDependencies should not be included if code doesnt use them', () => { + const code = ` + import { stuff } from 'dependency-other'; + `; + const deps = getDependencies(code, {}, { dependency: '1.0.0' }); + + expect(deps).toEqual({ + ['dependency-other']: 'latest', + }); + }); it('versions in optionalDependencies should win ', () => { const code = ` import { stuff } from 'dependency'; @@ -61,7 +71,7 @@ describe('getDependencies', () => { }); }); - it('versions in requiredDependencies should win ', () => { + it('versions in requiredDependencies should be added if not present in config', () => { const code = ` import { stuff } from 'dependency'; `; diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/getDepdencies.ts b/packages/react-components/react-storybook-addon-codesandbox/src/getDependencies.ts similarity index 96% rename from packages/react-components/react-storybook-addon-codesandbox/src/getDepdencies.ts rename to packages/react-components/react-storybook-addon-codesandbox/src/getDependencies.ts index e8ee49a82399e7..9ed659643e3a8b 100644 --- a/packages/react-components/react-storybook-addon-codesandbox/src/getDepdencies.ts +++ b/packages/react-components/react-storybook-addon-codesandbox/src/getDependencies.ts @@ -1,4 +1,4 @@ -export type PackageDependencies = { [dependencyName: string]: string }; +type PackageDependencies = { [dependencyName: string]: string }; function matchAll(str: string, re: RegExp) { const regexp = new RegExp(re, 'g'); diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/preset/preview.ts b/packages/react-components/react-storybook-addon-codesandbox/src/preset/preview.ts index 3ef9d7127ad6de..c74d5c71138fdd 100644 --- a/packages/react-components/react-storybook-addon-codesandbox/src/preset/preview.ts +++ b/packages/react-components/react-storybook-addon-codesandbox/src/preset/preview.ts @@ -1,3 +1,3 @@ -import { withCodeSandboxButton } from '../withCodeSandboxButton'; +import { withSandboxButton } from '../withCodeSandboxButton'; -export const decorators = [withCodeSandboxButton]; +export const decorators = [withSandboxButton]; diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/public-types.ts b/packages/react-components/react-storybook-addon-codesandbox/src/public-types.ts new file mode 100644 index 00000000000000..744d744d917fca --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/public-types.ts @@ -0,0 +1,16 @@ +/** + * NOTE: + * Don't import anything from source code in this file !! + * + * only pure API definitions of addon are allowed to live here, that are used both internal and for external storybook `Parameter` type extensions + */ +interface ParametersConfig { + optionalDependencies?: Record; + requiredDependencies?: Record; + provider: 'codesandbox-cloud' | 'codesandbox-browser' | 'stackblitz-cloud'; + bundler: 'vite' | 'cra'; +} + +export interface ParametersExtension { + exportToSandbox?: ParametersConfig; +} diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-factory.spec.ts b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-factory.spec.ts new file mode 100644 index 00000000000000..2ddab3b178bc3b --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-factory.spec.ts @@ -0,0 +1,117 @@ +import { ParametersExtension, StoryContext } from './types'; +import { addDemoActionButton } from './sandbox-factory'; +describe(`sandbox-factory`, () => { + describe(`#addDemoActionButton`, () => { + let submitSpy: ReturnType; + beforeEach(() => { + // https://github.com/jsdom/jsdom/issues/1937 + submitSpy = window.HTMLFormElement.prototype.submit = jest.fn(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + function setup(config: Pick, 'bundler' | 'provider'>) { + const context = { + story: 'Showcase', + componentId: '', + name: 'Showcase', + title: 'DefaultTitle', + kind: '', + id: 'default', + originalStoryFn: { + name: 'DefaultTitle', + }, + viewMode: 'docs', + parameters: { + fullSource: ` + import * as React from 'react'; + import { Text } from '@proj/react-components'; + + export const Default = () => This is an example of the Text component's usage.; + `, + exportToSandbox: { + ...config, + requiredDependencies: {}, + }, + }, + } as unknown as StoryContext; + const canvas = createCanvas(context.id); + + return { canvas, context }; + + function createCanvas(id: string) { + const root = document.createElement('div'); + root.id = 'docs-root'; + + const content = ` +
+
+
+ +
+
+
+ `; + + root.innerHTML = content; + + document.body.appendChild(root); + + return { + getActionButton: () => document.querySelector('.with-code-sandbox-button') as HTMLButtonElement, + getActionButtonsContainer: () => document.querySelector('.css-x12m3'), + cleanup: () => root.remove(), + }; + } + } + + it.each([ + { + bundler: 'cra', + provider: 'codesandbox-browser', + expected: ``, + }, + { + bundler: 'cra', + provider: 'codesandbox-cloud', + expected: ``, + }, + { + bundler: 'vite', + provider: 'codesandbox-cloud', + expected: ``, + }, + { + bundler: 'cra', + provider: 'stackblitz-cloud', + expected: ``, + }, + { + bundler: 'vite', + provider: 'stackblitz-cloud', + expected: ``, + }, + ] as const)(`should add action button based on configuration ($bundler, $provider)`, ({ expected, ...config }) => { + const { canvas, context } = setup(config); + addDemoActionButton(context); + + expect(canvas.getActionButtonsContainer()?.innerHTML).toEqual(expect.stringContaining(expected)); + + canvas.cleanup(); + }); + + it(`should submit form on click`, () => { + const { canvas, context } = setup({ bundler: 'cra', provider: 'codesandbox-browser' }); + addDemoActionButton(context); + + const actionButton = canvas.getActionButton(); + actionButton.click(); + + expect(submitSpy).toHaveBeenCalledTimes(1); + + canvas.cleanup(); + }); + }); +}); diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-factory.ts b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-factory.ts new file mode 100644 index 00000000000000..4f1bbe46736bdd --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-factory.ts @@ -0,0 +1,97 @@ +import { getParameters } from 'codesandbox-import-utils/lib/api/define'; + +import { scaffold } from './sandbox-scaffold'; +import { addHiddenInput, prepareData, prepareSandboxContainer, type Data } from './sandbox-utils'; +import { StoryContext } from './types'; + +const defaultFileToPreview = encodeURIComponent('src/example.tsx'); + +const actionConfig = { + 'codesandbox-cloud': { + label: 'CodeSandbox', + factory: (files: Record, config: Data) => openCodeSandbox({ files, ...config }), + }, + 'codesandbox-browser': { + label: 'CodeSandbox', + factory: (files: Record, config: Data) => openCodeSandbox({ files, ...config }), + }, + 'stackblitz-cloud': { + label: 'Stackblitz', + factory: (files: Record, config: Data) => openStackblitz({ files, ...config }), + }, +}; + +export function addDemoActionButton(context: StoryContext) { + const { container, cssClasses } = prepareSandboxContainer(context); + const config = prepareData(context); + if (!config) { + throw new Error('issues with data'); + } + + addActionButton(container, config, cssClasses); +} + +function addActionButton(container: HTMLElement, config: Data, classList: string[]) { + const files = scaffold[config.bundler](config); + const action = actionConfig[config.provider]; + + const button = document.createElement('button'); + button.classList.add(...classList); + button.textContent = `Open in ${action.label}`; + + container?.prepend(button); + + button.addEventListener('click', _ev => { + action.factory(files, config); + }); +} + +/** + * + * @see https://developer.stackblitz.com/docs/platform/post-api/ + */ +function openStackblitz(data: { files: Record } & Data) { + const { files, description, title } = data; + const form = document.createElement('form'); + form.method = 'post'; + form.target = '_blank'; + form.action = `https://stackblitz.com/run?file=${defaultFileToPreview}`; + // node template - boots web-container + addHiddenInput(form, 'project[template]', 'node'); + addHiddenInput(form, 'project[title]', title); + addHiddenInput(form, 'project[description]', `# ${description}`); + + Object.keys(files).forEach(key => { + const value = files[key]; + addHiddenInput(form, `project[files][${key}]`, value); + }); + + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); +} + +/** + * + * @see https://codesandbox.io/docs/learn/sandboxes/cli-api#define-api + */ +function openCodeSandbox({ files, provider }: { files: Record } & Data) { + const normalizedFilesApi = Object.entries(files).reduce((acc, current) => { + acc[current[0]] = { isBinary: false, content: current[1] }; + return acc; + }, {} as Record); + + const env = provider === 'codesandbox-cloud' ? 'server' : 'browser'; + const parameters = getParameters({ files: normalizedFilesApi }); + + const form = document.createElement('form'); + form.method = 'POST'; + form.target = '_blank'; + form.action = `https://codesandbox.io/api/v1/sandboxes/define?environment=${env}`; + + addHiddenInput(form, 'parameters', parameters); + addHiddenInput(form, 'query', `file=${defaultFileToPreview}`); + document.body.appendChild(form); + form.submit(); + document.body.removeChild(form); +} diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-scaffold.spec.ts b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-scaffold.spec.ts new file mode 100644 index 00000000000000..ca0408122683d8 --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-scaffold.spec.ts @@ -0,0 +1,587 @@ +import { scaffold } from './sandbox-scaffold'; + +describe(`sabdbox-scaffold`, () => { + const config = { + dependencies: {}, + storyExportToken: `DefaultTitle`, + storyFile: ` + import * as React from 'react'; + import { Text } from '@proj/react-components'; + + export const Default = () => This is an example of the Text component's usage.; + `, + description: 'react sandbox demo', + title: 'react sandbox', + }; + describe(`cra`, () => { + it(`should generate scaffold for codesandbox-browser`, () => { + const actual = scaffold.cra({ + bundler: 'cra', + provider: 'codesandbox-browser', + ...config, + }); + + expect(actual).toMatchInlineSnapshot(` + Object { + "package.json": "{ + \\"main\\": \\"src/index.tsx\\", + \\"dependencies\\": {}, + \\"devDependencies\\": { + \\"@types/react\\": \\"^17\\", + \\"@types/react-dom\\": \\"^17\\", + \\"typescript\\": \\"~4.7.0\\", + \\"react-scripts\\": \\"^5.0.0\\", + \\"@babel/plugin-proposal-private-property-in-object\\": \\"latest\\" + }, + \\"scripts\\": { + \\"start\\": \\"react-scripts start\\", + \\"build\\": \\"react-scripts build\\", + \\"test\\": \\"react-scripts test --env=jsdom\\", + \\"eject\\": \\"react-scripts eject\\" + }, + \\"browserslist\\": [ + \\">0.2%\\", + \\"not dead\\", + \\"not ie <= 11\\", + \\"not op_mini all\\" + ] + }", + "public/index.html": "
", + "src/App.tsx": "import { FluentProvider, webLightTheme } from '@fluentui/react-components'; + import { DefaultTitle as Example } from './example'; + + const App = () => { + return ( + + + + ); + }; + + export default App;", + "src/example.tsx": " + import * as React from 'react'; + import { Text } from '@proj/react-components'; + + export const Default = () => This is an example of the Text component's usage.; + ", + "src/index.tsx": "import * as React from 'react'; + import * as ReactDOM from 'react-dom'; + import App from './App'; + + ReactDOM.render( + + + , + document.getElementById('root') as HTMLElement + );", + "tsconfig.json": "{ + \\"include\\": [ + \\"./src/**/*\\" + ], + \\"compilerOptions\\": { + \\"strict\\": true, + \\"esModuleInterop\\": true, + \\"lib\\": [ + \\"dom\\", + \\"es2015\\" + ], + \\"jsx\\": \\"react-jsx\\" + } + }", + } + `); + }); + + it(`should generate scaffold for codesandbox-cloud`, () => { + const actual = scaffold.cra({ + bundler: 'cra', + provider: 'codesandbox-cloud', + ...config, + }); + + expect(actual).toMatchInlineSnapshot(` + Object { + ".codesandbox/tasks.json": "{ + \\"setupTasks\\": [ + { + \\"name\\": \\"Install Dependencies\\", + \\"command\\": \\"yarn install\\" + } + ], + \\"tasks\\": { + \\"dev\\": { + \\"name\\": \\"dev\\", + \\"runAtStart\\": true, + \\"command\\": \\"yarn start\\", + \\"preview\\": { + \\"port\\": 3000 + } + }, + \\"build\\": { + \\"name\\": \\"build\\", + \\"command\\": \\"yarn build\\", + \\"runAtStart\\": false + }, + \\"preview\\": { + \\"name\\": \\"preview\\", + \\"command\\": \\"yarn preview\\", + \\"runAtStart\\": false + } + } + }", + ".devcontainer/Dockerfile": "FROM node:16-bullseye", + ".devcontainer/devcontainer.json": "{ + \\"name\\": \\"Devcontainer\\", + \\"build\\": { + \\"dockerfile\\": \\"./Dockerfile\\" + } + }", + "package.json": "{ + \\"main\\": \\"src/index.tsx\\", + \\"dependencies\\": {}, + \\"devDependencies\\": { + \\"@types/react\\": \\"^17\\", + \\"@types/react-dom\\": \\"^17\\", + \\"typescript\\": \\"~4.7.0\\", + \\"react-scripts\\": \\"^5.0.0\\", + \\"@babel/plugin-proposal-private-property-in-object\\": \\"latest\\" + }, + \\"scripts\\": { + \\"start\\": \\"react-scripts start\\", + \\"build\\": \\"react-scripts build\\", + \\"test\\": \\"react-scripts test --env=jsdom\\", + \\"eject\\": \\"react-scripts eject\\" + }, + \\"browserslist\\": [ + \\">0.2%\\", + \\"not dead\\", + \\"not ie <= 11\\", + \\"not op_mini all\\" + ] + }", + "public/index.html": "
", + "src/App.tsx": "import { FluentProvider, webLightTheme } from '@fluentui/react-components'; + import { DefaultTitle as Example } from './example'; + + const App = () => { + return ( + + + + ); + }; + + export default App;", + "src/example.tsx": " + import * as React from 'react'; + import { Text } from '@proj/react-components'; + + export const Default = () => This is an example of the Text component's usage.; + ", + "src/index.tsx": "import * as React from 'react'; + import * as ReactDOM from 'react-dom'; + import App from './App'; + + ReactDOM.render( + + + , + document.getElementById('root') as HTMLElement + );", + "tsconfig.json": "{ + \\"include\\": [ + \\"./src/**/*\\" + ], + \\"compilerOptions\\": { + \\"strict\\": true, + \\"esModuleInterop\\": true, + \\"lib\\": [ + \\"dom\\", + \\"es2015\\" + ], + \\"jsx\\": \\"react-jsx\\" + } + }", + } + `); + }); + + it(`should generate scaffold for stackblitz-cloud`, () => { + const actual = scaffold.cra({ + bundler: 'cra', + provider: 'stackblitz-cloud', + ...config, + }); + + expect(actual).toMatchInlineSnapshot(` + Object { + ".stackblitzrc": "{}", + "package.json": "{ + \\"main\\": \\"src/index.tsx\\", + \\"dependencies\\": {}, + \\"devDependencies\\": { + \\"@types/react\\": \\"^17\\", + \\"@types/react-dom\\": \\"^17\\", + \\"typescript\\": \\"~4.7.0\\", + \\"react-scripts\\": \\"^5.0.0\\", + \\"@babel/plugin-proposal-private-property-in-object\\": \\"latest\\" + }, + \\"scripts\\": { + \\"start\\": \\"react-scripts start\\", + \\"build\\": \\"react-scripts build\\", + \\"test\\": \\"react-scripts test --env=jsdom\\", + \\"eject\\": \\"react-scripts eject\\" + }, + \\"browserslist\\": [ + \\">0.2%\\", + \\"not dead\\", + \\"not ie <= 11\\", + \\"not op_mini all\\" + ] + }", + "public/index.html": "
", + "src/App.tsx": "import { FluentProvider, webLightTheme } from '@fluentui/react-components'; + import { DefaultTitle as Example } from './example'; + + const App = () => { + return ( + + + + ); + }; + + export default App;", + "src/example.tsx": " + import * as React from 'react'; + import { Text } from '@proj/react-components'; + + export const Default = () => This is an example of the Text component's usage.; + ", + "src/index.tsx": "import * as React from 'react'; + import * as ReactDOM from 'react-dom'; + import App from './App'; + + ReactDOM.render( + + + , + document.getElementById('root') as HTMLElement + );", + "tsconfig.json": "{ + \\"include\\": [ + \\"./src/**/*\\" + ], + \\"compilerOptions\\": { + \\"strict\\": true, + \\"esModuleInterop\\": true, + \\"lib\\": [ + \\"dom\\", + \\"es2015\\" + ], + \\"jsx\\": \\"react-jsx\\" + } + }", + } + `); + }); + }); + + describe(`vite`, () => { + it(`should throw error if used with 'codesandbox-browser'`, () => { + const actual = () => + scaffold.vite({ + provider: 'codesandbox-browser', + bundler: 'vite', + ...config, + }); + + expect(actual).toThrowErrorMatchingInlineSnapshot(`"vite is not supported on codesandbox-browser"`); + }); + + it(`should generate scaffold for codesandbox-cloud`, () => { + const actual = scaffold.vite({ + provider: 'codesandbox-cloud', + bundler: 'vite', + ...config, + }); + + expect(actual).toMatchInlineSnapshot(` + Object { + ".codesandbox/tasks.json": "{ + \\"setupTasks\\": [ + { + \\"name\\": \\"Install Dependencies\\", + \\"command\\": \\"yarn install\\" + } + ], + \\"tasks\\": { + \\"dev\\": { + \\"name\\": \\"dev\\", + \\"runAtStart\\": true, + \\"command\\": \\"yarn dev\\", + \\"preview\\": { + \\"port\\": 5173 + } + }, + \\"build\\": { + \\"name\\": \\"build\\", + \\"command\\": \\"yarn build\\", + \\"runAtStart\\": false + }, + \\"preview\\": { + \\"name\\": \\"preview\\", + \\"command\\": \\"yarn preview\\", + \\"runAtStart\\": false + } + } + }", + ".devcontainer/Dockerfile": "FROM node:16-bullseye", + ".devcontainer/devcontainer.json": "{ + \\"name\\": \\"Devcontainer\\", + \\"build\\": { + \\"dockerfile\\": \\"./Dockerfile\\" + } + }", + "index.html": " + + + + + + Vite + React + TS + + +
+ + + ", + "package.json": "{ + \\"name\\": \\"vite-react-typescript-starter\\", + \\"private\\": true, + \\"version\\": \\"0.0.0\\", + \\"type\\": \\"module\\", + \\"scripts\\": { + \\"dev\\": \\"vite\\", + \\"build\\": \\"tsc && vite build\\", + \\"lint\\": \\"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0\\", + \\"preview\\": \\"vite preview\\" + }, + \\"dependencies\\": {}, + \\"devDependencies\\": { + \\"@types/react\\": \\"^17\\", + \\"@types/react-dom\\": \\"^17\\", + \\"typescript\\": \\"~4.7.0\\", + \\"@vitejs/plugin-react\\": \\"^4.1.0\\", + \\"vite\\": \\"^4.0.0\\" + } + }", + "src/App.tsx": "import { FluentProvider, webLightTheme } from '@fluentui/react-components'; + import { DefaultTitle as Example } from './example'; + + const App = () => { + return ( + + + + ); + }; + + export default App;", + "src/example.tsx": " + import * as React from 'react'; + import { Text } from '@proj/react-components'; + + export const Default = () => This is an example of the Text component's usage.; + ", + "src/index.tsx": "import * as React from 'react'; + import * as ReactDOM from 'react-dom'; + import App from './App'; + + ReactDOM.render( + + + , + document.getElementById('root') as HTMLElement + );", + "tsconfig.json": "{ + \\"compilerOptions\\": { + \\"target\\": \\"ES2020\\", + \\"useDefineForClassFields\\": true, + \\"lib\\": [ + \\"ES2020\\", + \\"DOM\\", + \\"DOM.Iterable\\" + ], + \\"module\\": \\"ESNext\\", + \\"skipLibCheck\\": true, + \\"moduleResolution\\": \\"node\\", + \\"allowImportingTsExtensions\\": true, + \\"resolveJsonModule\\": true, + \\"isolatedModules\\": true, + \\"noEmit\\": true, + \\"jsx\\": \\"react-jsx\\", + \\"strict\\": true, + \\"noUnusedLocals\\": true, + \\"noUnusedParameters\\": true, + \\"noFallthroughCasesInSwitch\\": true + }, + \\"include\\": [ + \\"src\\" + ], + \\"references\\": [ + { + \\"path\\": \\"./tsconfig.node.json\\" + } + ] + }", + "tsconfig.node.json": "{ + \\"compilerOptions\\": { + \\"composite\\": true, + \\"skipLibCheck\\": true, + \\"module\\": \\"ESNext\\", + \\"moduleResolution\\": \\"bundler\\", + \\"allowSyntheticDefaultImports\\": true + }, + \\"include\\": [ + \\"vite.config.ts\\" + ] + }", + "vite.config.ts": "import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vitejs.dev/config/ + export default defineConfig({ + plugins: [react()], + })", + } + `); + }); + + it(`should generate scaffold for stackblitz-cloud`, () => { + const actual = scaffold.vite({ + provider: 'stackblitz-cloud', + bundler: 'vite', + ...config, + }); + + expect(actual).toMatchInlineSnapshot(` + Object { + ".stackblitzrc": "{}", + "index.html": " + + + + + + Vite + React + TS + + +
+ + + ", + "package.json": "{ + \\"name\\": \\"vite-react-typescript-starter\\", + \\"private\\": true, + \\"version\\": \\"0.0.0\\", + \\"type\\": \\"module\\", + \\"scripts\\": { + \\"dev\\": \\"vite\\", + \\"build\\": \\"tsc && vite build\\", + \\"lint\\": \\"eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0\\", + \\"preview\\": \\"vite preview\\" + }, + \\"dependencies\\": {}, + \\"devDependencies\\": { + \\"@types/react\\": \\"^17\\", + \\"@types/react-dom\\": \\"^17\\", + \\"typescript\\": \\"~4.7.0\\", + \\"@vitejs/plugin-react\\": \\"^4.1.0\\", + \\"vite\\": \\"^4.0.0\\" + } + }", + "src/App.tsx": "import { FluentProvider, webLightTheme } from '@fluentui/react-components'; + import { DefaultTitle as Example } from './example'; + + const App = () => { + return ( + + + + ); + }; + + export default App;", + "src/example.tsx": " + import * as React from 'react'; + import { Text } from '@proj/react-components'; + + export const Default = () => This is an example of the Text component's usage.; + ", + "src/index.tsx": "import * as React from 'react'; + import * as ReactDOM from 'react-dom'; + import App from './App'; + + ReactDOM.render( + + + , + document.getElementById('root') as HTMLElement + );", + "tsconfig.json": "{ + \\"compilerOptions\\": { + \\"target\\": \\"ES2020\\", + \\"useDefineForClassFields\\": true, + \\"lib\\": [ + \\"ES2020\\", + \\"DOM\\", + \\"DOM.Iterable\\" + ], + \\"module\\": \\"ESNext\\", + \\"skipLibCheck\\": true, + \\"moduleResolution\\": \\"node\\", + \\"allowImportingTsExtensions\\": true, + \\"resolveJsonModule\\": true, + \\"isolatedModules\\": true, + \\"noEmit\\": true, + \\"jsx\\": \\"react-jsx\\", + \\"strict\\": true, + \\"noUnusedLocals\\": true, + \\"noUnusedParameters\\": true, + \\"noFallthroughCasesInSwitch\\": true + }, + \\"include\\": [ + \\"src\\" + ], + \\"references\\": [ + { + \\"path\\": \\"./tsconfig.node.json\\" + } + ] + }", + "tsconfig.node.json": "{ + \\"compilerOptions\\": { + \\"composite\\": true, + \\"skipLibCheck\\": true, + \\"module\\": \\"ESNext\\", + \\"moduleResolution\\": \\"bundler\\", + \\"allowSyntheticDefaultImports\\": true + }, + \\"include\\": [ + \\"vite.config.ts\\" + ] + }", + "vite.config.ts": "import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vitejs.dev/config/ + export default defineConfig({ + plugins: [react()], + })", + } + `); + }); + }); +}); diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-scaffold.ts b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-scaffold.ts new file mode 100644 index 00000000000000..d5b9f88b48b0d2 --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-scaffold.ts @@ -0,0 +1,269 @@ +import * as dedent from 'dedent'; + +import type { Data } from './sandbox-utils'; +import { serializeJson } from './utils'; + +const commonDevDeps = { '@types/react': '^17', '@types/react-dom': '^17', typescript: '~4.7.0' }; + +export const scaffold = { + vite: (data: Data): Record => { + if (data.provider === 'codesandbox-browser') { + throw new Error('vite is not supported on codesandbox-browser'); + } + + const base = { + 'index.html': Vite.getHTML(), + 'src/App.tsx': Vite.getApp(data), + 'src/index.tsx': Vite.getRootIndex(), + 'src/example.tsx': Vite.getExample(data), + 'tsconfig.json': Vite.getTsconfig(), + 'tsconfig.node.json': Vite.getTsconfigNode(), + 'vite.config.ts': Vite.getViteCfg(), + 'package.json': Vite.getPkgJson(data), + }; + if (data.provider === 'stackblitz-cloud') { + Object.assign(base, getStackblitzConfig()); + } + if (data.provider === 'codesandbox-cloud') { + Object.assign(base, getCodesandboxConfig('vite')); + } + return base; + }, + cra: (data: Data): Record => { + const base = { + 'public/index.html': CRA.getHTML(), + 'src/App.tsx': CRA.getApp(data), + 'src/index.tsx': CRA.getRootIndex(), + 'src/example.tsx': CRA.getExample(data), + 'tsconfig.json': CRA.getTsconfig(), + 'package.json': CRA.getPkgJson(data), + }; + if (data.provider === 'stackblitz-cloud') { + Object.assign(base, getStackblitzConfig()); + } + if (data.provider === 'codesandbox-cloud') { + Object.assign(base, getCodesandboxConfig('cra')); + } + + return base; + }, +}; + +const Vite = { + getHTML: () => dedent` + + + + + + + Vite + React + TS + + +
+ + + + `, + getRootIndex: getIndex, + getExample, + getApp, + getViteCfg: () => { + return dedent` + import { defineConfig } from 'vite' + import react from '@vitejs/plugin-react' + + // https://vitejs.dev/config/ + export default defineConfig({ + plugins: [react()], + }) + `; + }, + getTsconfigNode: () => { + return serializeJson({ + compilerOptions: { + composite: true, + skipLibCheck: true, + module: 'ESNext', + moduleResolution: 'bundler', + allowSyntheticDefaultImports: true, + }, + include: ['vite.config.ts'], + }); + }, + getTsconfig: () => { + return serializeJson({ + compilerOptions: { + target: 'ES2020', + useDefineForClassFields: true, + lib: ['ES2020', 'DOM', 'DOM.Iterable'], + module: 'ESNext', + skipLibCheck: true, + + /* Bundler mode */ + moduleResolution: 'node', + // moduleResolution: 'bundler', + allowImportingTsExtensions: true, + resolveJsonModule: true, + isolatedModules: true, + noEmit: true, + jsx: 'react-jsx', + + /* Linting */ + strict: true, + noUnusedLocals: true, + noUnusedParameters: true, + noFallthroughCasesInSwitch: true, + }, + include: ['src'], + references: [{ path: './tsconfig.node.json' }], + }); + }, + getPkgJson: (data: Data) => { + return serializeJson({ + name: 'vite-react-typescript-starter', + private: true, + version: '0.0.0', + type: 'module', + scripts: { + dev: 'vite', + build: 'tsc && vite build', + lint: 'eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0', + preview: 'vite preview', + }, + dependencies: { + ...data.dependencies, + }, + devDependencies: { + ...commonDevDeps, + '@vitejs/plugin-react': '^4.1.0', + vite: '^4.0.0', + }, + }); + }, +}; + +const CRA = { + getHTML: () => `
`, + getRootIndex: getIndex, + getExample, + getApp, + getTsconfig: () => + serializeJson({ + include: ['./src/**/*'], + compilerOptions: { + strict: true, + esModuleInterop: true, + lib: ['dom', 'es2015'], + jsx: 'react-jsx', + }, + }), + getPkgJson: (data: Data) => { + return serializeJson({ + main: 'src/index.tsx', + dependencies: { + ...data.dependencies, + }, + devDependencies: { + ...commonDevDeps, + 'react-scripts': '^5.0.0', + '@babel/plugin-proposal-private-property-in-object': 'latest', + }, + scripts: { + start: 'react-scripts start', + build: 'react-scripts build', + test: 'react-scripts test --env=jsdom', + eject: 'react-scripts eject', + }, + browserslist: ['>0.2%', 'not dead', 'not ie <= 11', 'not op_mini all'], + }); + }, +}; + +function getCodesandboxConfig(kind: 'cra' | 'vite') { + const startConfig = { + cra: { command: 'yarn start', preview: { port: 3000 } }, + vite: { command: 'yarn dev', preview: { port: 5173 } }, + }; + return { + '.devcontainer/devcontainer.json': serializeJson({ + name: 'Devcontainer', + build: { + dockerfile: './Dockerfile', + }, + }), + '.devcontainer/Dockerfile': `FROM node:16-bullseye`, + '.codesandbox/tasks.json': serializeJson({ + // These tasks will run in order when initializing your CodeSandbox project. + setupTasks: [ + { + name: 'Install Dependencies', + command: 'yarn install', + }, + ], + + // These tasks can be run from CodeSandbox. Running one will open a log in the app. + tasks: { + dev: { + name: 'dev', + runAtStart: true, + ...startConfig[kind], + }, + build: { + name: 'build', + command: 'yarn build', + runAtStart: false, + }, + preview: { + name: 'preview', + command: 'yarn preview', + runAtStart: false, + }, + }, + }), + }; +} + +function getStackblitzConfig() { + return { + '.stackblitzrc': serializeJson({}), + }; +} + +function getIndex() { + return dedent` + import * as React from 'react'; + import * as ReactDOM from 'react-dom'; + import App from './App'; + + ReactDOM.render( + + + , + document.getElementById('root') as HTMLElement + ); + `; +} + +function getExample(demoData: Data) { + return demoData.storyFile; +} + +function getApp(data: Data) { + const code = dedent` + import { FluentProvider, webLightTheme } from '@fluentui/react-components'; + import { ${data.storyExportToken} as Example } from './example'; + + const App = () => { + return ( + + + + ); + }; + + export default App; + `; + + return code; +} diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-utils.spec.ts b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-utils.spec.ts new file mode 100644 index 00000000000000..9f13e2203a01df --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-utils.spec.ts @@ -0,0 +1,67 @@ +import { StoryContext } from './types'; +import { prepareData } from './sandbox-utils'; + +// eslint-disable-next-line @typescript-eslint/no-empty-function +const noop = () => {}; + +describe(`sabdbox-utils`, () => { + const context = { + story: 'Showcase', + componentId: '', + name: 'Showcase', + title: 'DefaultTitle', + kind: '', + id: '', + originalStoryFn: { + name: 'DefaultTitle', + }, + parameters: { + fullSource: ` + import * as React from 'react'; + import { Text } from '@proj/react-components'; + + export const Default = () => This is an example of the Text component's usage.; + `, + exportToSandbox: { + bundler: 'vite', + provider: 'sandbox', + requiredDependencies: {}, + }, + }, + } as unknown as StoryContext; + + describe(`#prepareData`, () => { + it(`should throw error when parameters.exportToSandbox is missing`, () => { + const actual = () => prepareData({ ...context, parameters: {} }); + + expect(actual).toThrowErrorMatchingInlineSnapshot(`"exportToSandbox config parameter cannot be empty"`); + }); + + it(`should throw error when parameters.fullsource is missing`, () => { + const consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation(noop); + const actual = prepareData({ ...context, parameters: { exportToSandbox: context.parameters.exportToSandbox } }); + + expect(actual).toBe(null); + expect(consoleErrorSpy.mock.calls.flat()).toMatchInlineSnapshot(` + Array [ + "Export to CodeSandbox: Couldn't find source for story Showcase. Did you install the babel plugin?", + ] + `); + }); + it(`should prepare data from SB context`, () => { + const actual = prepareData(context); + expect(actual).toEqual({ + bundler: 'vite', + dependencies: { + '@proj/react-components': 'latest', + react: 'latest', + }, + description: 'Story demo: DefaultTitle - Showcase', + provider: 'sandbox', + storyExportToken: 'DefaultTitle', + storyFile: context.parameters.fullSource, + title: 'FluentUI React v9', + }); + }); + }); +}); diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-utils.ts b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-utils.ts new file mode 100644 index 00000000000000..6e15a9400c3f4f --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/sandbox-utils.ts @@ -0,0 +1,107 @@ +import * as dedent from 'dedent'; + +import { getDependencies } from './getDependencies'; +import { StoryContext, ParametersExtension } from './types'; + +type ParametersConfig = NonNullable; + +export function addHiddenInput(form: HTMLFormElement, name: string, value: string) { + const input = document.createElement('input'); + input.type = 'hidden'; + input.name = name; + input.value = value; + form.appendChild(input); +} + +export function prepareSandboxContainer(context: StoryContext) { + const docsSelector = `#anchor--${context.id} .docs-story`; + const rootElement = document.querySelector(docsSelector); + + if (!rootElement) { + throw new Error(`css selector: ${docsSelector}, doesn't exist `); + } + + const showCodeButton = rootElement.querySelector('.docblock-code-toggle'); + const container = showCodeButton?.parentElement; + + if (!container) { + throw new Error(`css selector: '.docblock-code-toggle', doesn't exist `); + } + + const classList = (showCodeButton.classList.value + ' with-code-sandbox-button').split(' '); + + // this is needed in dev loop as every hot reload will add new buttons + if (process.env.NODE_ENV !== 'production') { + const ourButtons = container.querySelectorAll(`.with-code-sandbox-button`); + ourButtons.forEach(node => node.remove()); + } + + return { + container, + cssClasses: classList, + }; +} + +const addonConfigDefaults = { requiredDependencies: {}, optionalDependencies: {} }; +export type Data = Pick, 'provider' | 'bundler'> & { + storyFile: string; + // use originalStoryFn because users can override the `storyName` property. + // We need the name of the exported function, not the actual story + // https://github.com/microsoft/fluentui-storybook-addons/issues/12 + // originalStoryFn.name someties looks like this: ProgressBarDefault_stories_Default + // just get the "Default" + // @TODO - im not sure this is still needed, wasn't able to repro. Can we remove it ? + storyExportToken: string; + dependencies: Record; + title: string; + description: string; +}; + +export function prepareData(context: StoryContext): Data | null { + if (!context.parameters.exportToSandbox) { + throw new Error('exportToSandbox config parameter cannot be empty'); + } + + const addonConfig: Required = { + ...addonConfigDefaults, + ...context.parameters.exportToSandbox, + }; + const { provider, bundler } = addonConfig; + const storyFile = context.parameters?.fullSource; + + if (!storyFile) { + console.error( + dedent`Export to CodeSandbox: Couldn't find source for story ${context.story}. Did you install the babel plugin?`, + ); + return null; + } + + const { requiredDependencies, optionalDependencies } = addonConfig; + const dependencies = getDependencies(storyFile, requiredDependencies, optionalDependencies); + + const title = 'FluentUI React v9'; + const description = `Story demo: ${context.title} - ${context.name}`; + + // use originalStoryFn because users can override the `storyName` property. + // We need the name of the exported function, not the actual story + // https://github.com/microsoft/fluentui-storybook-addons/issues/12 + // originalStoryFn.name someties looks like this: ProgressBarDefault_stories_Default + // just get the "Default" + // @TODO - im not sure this is still needed, wasn't able to repro. Can we remove it ? + const storyExportToken = context.originalStoryFn.name.split('_stories_').slice(-1).pop(); + if (!storyExportToken) { + throw new Error('issues processing story export token'); + } + + const demoData = { + storyFile, + storyExportToken, + provider, + bundler, + dependencies, + title, + description, + }; + + return demoData; +} diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/styles.css b/packages/react-components/react-storybook-addon-codesandbox/src/styles.css new file mode 100644 index 00000000000000..275289ec078139 --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/styles.css @@ -0,0 +1,47 @@ +/* Remove z-index from "show code" button container */ +/* https://github.com/microsoft/fluentui/issues/22773 */ +.docs-story > div:nth-child(2) { + z-index: auto; +} + +#docs-root .docblock-code-toggle, +.docs-story .with-code-sandbox-button { + font-family: 'Segoe UI', 'Segoe UI Web (West European)', -apple-system, BlinkMacSystemFont, Roboto, 'Helvetica Neue', + sans-serif; + min-width: 91px; + font-size: 14px !important; + font-weight: 600 !important; + line-height: 150% !important; + color: #201f1e !important; + text-align: center !important; + justify-content: center !important; + letter-spacing: -0.01em !important; + background: #f8f8f8 !important; + border: none !important; + /* box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.25) !important; */ + border-radius: 5px 5px 0px 0px !important; +} + +.docs-story .with-code-sandbox-button { + margin-right: 32px; +} + +/* Reduce font size of CodeSandbox and Show Code button when zoomed or small window width*/ +/* https://github.com/microsoft/fluentui/issues/22764 */ + +@media screen and (max-width: 380px) { + #docs-root .docblock-code-toggle, + .docs-story .with-code-sandbox-button { + font-size: 10px !important; + } +} + +/* Make storybook codesandbox export button match Figma design */ +.docs-story .with-code-sandbox-button { + right: 105px !important; +} + +.docs-story .with-code-sandbox-button:focus { + outline: none; + box-shadow: #1ea7fd 0 -3px 0 0 inset; +} diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/types.ts b/packages/react-components/react-storybook-addon-codesandbox/src/types.ts new file mode 100644 index 00000000000000..dc41cad559b259 --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/types.ts @@ -0,0 +1,8 @@ +import type { StoryContext as StoryContextOrigin, Parameters } from '@storybook/addons'; +import type { ParametersExtension } from './public-types'; + +export interface StoryContext extends StoryContextOrigin { + parameters: Parameters & ParametersExtension; +} + +export type { ParametersExtension }; diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/utils.ts b/packages/react-components/react-storybook-addon-codesandbox/src/utils.ts new file mode 100644 index 00000000000000..366b47ca158366 --- /dev/null +++ b/packages/react-components/react-storybook-addon-codesandbox/src/utils.ts @@ -0,0 +1,3 @@ +export function serializeJson(value: object) { + return JSON.stringify(value, null, 2); +} diff --git a/packages/react-components/react-storybook-addon-codesandbox/src/withCodeSandboxButton.ts b/packages/react-components/react-storybook-addon-codesandbox/src/withCodeSandboxButton.ts index c9e4ce26b45c2f..ae6b35e8513c80 100644 --- a/packages/react-components/react-storybook-addon-codesandbox/src/withCodeSandboxButton.ts +++ b/packages/react-components/react-storybook-addon-codesandbox/src/withCodeSandboxButton.ts @@ -1,102 +1,14 @@ -import { StoryContext, useEffect } from '@storybook/addons'; -import { getParameters } from 'codesandbox-import-utils/lib/api/define'; -import * as dedent from 'dedent'; -import { getDependencies } from './getDepdencies'; -import type { PackageDependencies } from './getDepdencies'; +import { useEffect } from '@storybook/addons'; +import { addDemoActionButton } from './sandbox-factory'; -export const withCodeSandboxButton = (storyFn: (context: StoryContext) => JSX.Element, context: StoryContext) => { +import { StoryContext } from './types'; + +export const withSandboxButton = (storyFn: (context: StoryContext) => JSX.Element, context: StoryContext) => { useEffect(() => { if (context.viewMode === 'docs') { - displayToolState(`#anchor--${context.id} .docs-story`, context); + addDemoActionButton(context); } }, [context]); return storyFn(context); }; - -const displayToolState = (selector: string, context: StoryContext) => { - const exportLink = document.createElement('a'); - exportLink.className = 'with-code-sandbox-button'; - exportLink.style.setProperty('position', 'absolute'); - exportLink.style.setProperty('bottom', '0'); - exportLink.style.setProperty('right', '90px'); - exportLink.style.setProperty('border', '1px solid rgba(0,0,0,.1)'); - exportLink.style.setProperty('border-bottom', 'none'); - exportLink.style.setProperty('border-radius', '4px 4px 0 0'); - exportLink.style.setProperty('padding', '4px 10px'); - exportLink.style.setProperty('background', 'white'); - exportLink.style.setProperty( - 'font-family', - '"Nunito Sans",-apple-system,".SFNSText-Regular","San Francisco",BlinkMacSystemFont,"Segoe UI","Helvetica Neue",Helvetica,Arial,sans-serif', - ); - exportLink.style.setProperty('font-weight', '700'); - exportLink.style.setProperty('font-size', '12px'); - exportLink.style.setProperty('text-decoration', 'none'); - exportLink.style.setProperty('line-height', '16px'); - exportLink.setAttribute('target', '_blank'); - - // set to error state by default, overwritten later - exportLink.style.setProperty('color', 'darkred'); - exportLink.innerText = `CodeSandbox Error: See console`; - - const rootElement = document.querySelector(selector); - rootElement?.appendChild(exportLink); - - const storyFile = context.parameters?.fullSource; - - if (!storyFile) { - console.error( - `Export to CodeSandbox: Couldn’t find source for story ${context.story}. Did you install the babel plugin?`, - ); - return false; - } - - const requiredDependencies: PackageDependencies = context.parameters?.exportToCodeSandbox?.requiredDependencies ?? {}; - const optionalDependencies: PackageDependencies = context.parameters?.exportToCodeSandbox?.optionalDependencies ?? {}; - - const dependencies = getDependencies(storyFile, requiredDependencies, optionalDependencies); - - const indexTsx = context.parameters?.exportToCodeSandbox?.indexTsx; - if (indexTsx === null) { - console.error( - dedent`Export to CodeSandbox: Please set parameters.exportToCodeSandbox.indexTsx - to the desired content of index.tsx file.`, - ); - return false; - } - console.log(context); - - const defaultFileToPreview = encodeURIComponent('/example.tsx'); - const codeSandboxParameters = getParameters({ - files: { - 'example.tsx': { - isBinary: false, - content: storyFile, - }, - 'index.html': { - isBinary: false, - content: '
', - }, - 'index.tsx': { - isBinary: false, - // use originalStoryFn because users can override the `storyName` property. - // We need the name of the exported function, not the actual story - // https://github.com/microsoft/fluentui-storybook-addons/issues/12 - // originalStoryFn.name someties looks like this: ProgressBarDefault_stories_Default - // just get the "Default" - content: indexTsx.replace('STORY_NAME', context.originalStoryFn.name.split('_stories_').slice(-1).pop()), - }, - 'package.json': { - isBinary: false, - content: JSON.stringify({ dependencies }), - }, - }, - }); - - exportLink.setAttribute( - 'href', - `https://codesandbox.io/api/v1/sandboxes/define?parameters=${codeSandboxParameters}&query=file%3D${defaultFileToPreview}`, - ); - exportLink.style.setProperty('color', '#333333'); - exportLink.innerText = `Open in CodeSandbox`; -}; diff --git a/typings/storybook__addons/index.d.ts b/typings/storybook__addons/index.d.ts index 3f37163394817a..fd135ac9a8127d 100644 --- a/typings/storybook__addons/index.d.ts +++ b/typings/storybook__addons/index.d.ts @@ -57,25 +57,6 @@ declare module '@storybook/addons' { */ inlineStories?: boolean; }; - /** - * @see https://github.com/microsoft/fluentui-storybook-addons - */ - exportToCodeSandbox?: AddonExportToCodesandboxParameters; - } - - interface AddonExportToCodesandboxParameters { - /** - * Dependencies that should be included with every story - */ - requiredDependencies?: Record; - /** - * Dependencies that should be included if the story code contains it - */ - optionalDependencies?: Record; - /** - * Content of index.tsx in CodeSandbox - */ - indexTsx?: string; } interface ControlsParameters {