Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 0 additions & 45 deletions .storybook/docs-root.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
25 changes: 7 additions & 18 deletions .storybook/preview.js
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -25,7 +27,7 @@ window.__setCurrentStory = function (categorization, story) {
/** @type {NonNullable<import('@storybook/react').Story['decorators']>} */
export const decorators = [withLinks];

/** @type {import('@storybook/addons').Parameters} */
/** @type {Parameters} */
export const parameters = {
viewMode: 'docs',
controls: {
Expand All @@ -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(
<FluentProvider theme={webLightTheme}>
<Example />
</FluentProvider>,
document.getElementById('root'),
);`,
},
};
Original file line number Diff line number Diff line change
Expand Up @@ -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!
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getDependencies } from './getDepdencies';
import { getDependencies } from './getDependencies';

describe('getDependencies', () => {
it('should find all dependencies in a file', () => {
Expand Down Expand Up @@ -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';
Expand All @@ -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';
`;
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { withCodeSandboxButton } from '../withCodeSandboxButton';
import { withSandboxButton } from '../withCodeSandboxButton';

export const decorators = [withCodeSandboxButton];
export const decorators = [withSandboxButton];
Original file line number Diff line number Diff line change
@@ -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<string, string>;
requiredDependencies?: Record<string, string>;
provider: 'codesandbox-cloud' | 'codesandbox-browser' | 'stackblitz-cloud';
bundler: 'vite' | 'cra';
}

export interface ParametersExtension {
exportToSandbox?: ParametersConfig;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { ParametersExtension, StoryContext } from './types';
import { addDemoActionButton } from './sandbox-factory';
describe(`sandbox-factory`, () => {
describe(`#addDemoActionButton`, () => {
let submitSpy: ReturnType<typeof jest.fn>;
beforeEach(() => {
// https://github.com/jsdom/jsdom/issues/1937
submitSpy = window.HTMLFormElement.prototype.submit = jest.fn();
});

afterEach(() => {
jest.restoreAllMocks();
});

function setup(config: Pick<NonNullable<ParametersExtension['exportToSandbox']>, '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 = () => <Text>This is an example of the Text component's usage.</Text>;
`,
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 = `
<div id="anchor--${id}">
<div class="docs-story">
<div class="css-x12m3">
<button class="docblock-code-toggle">Show Code</button>
</div>
</div>
</div>
`;

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: `<button class="docblock-code-toggle with-code-sandbox-button">Open in CodeSandbox</button>`,
},
{
bundler: 'cra',
provider: 'codesandbox-cloud',
expected: `<button class="docblock-code-toggle with-code-sandbox-button">Open in CodeSandbox</button>`,
},
{
bundler: 'vite',
provider: 'codesandbox-cloud',
expected: `<button class="docblock-code-toggle with-code-sandbox-button">Open in CodeSandbox</button>`,
},
{
bundler: 'cra',
provider: 'stackblitz-cloud',
expected: `<button class="docblock-code-toggle with-code-sandbox-button">Open in Stackblitz</button>`,
},
{
bundler: 'vite',
provider: 'stackblitz-cloud',
expected: `<button class="docblock-code-toggle with-code-sandbox-button">Open in Stackblitz</button>`,
},
] 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();
});
});
});
Original file line number Diff line number Diff line change
@@ -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<string, string>, config: Data) => openCodeSandbox({ files, ...config }),
},
'codesandbox-browser': {
label: 'CodeSandbox',
factory: (files: Record<string, string>, config: Data) => openCodeSandbox({ files, ...config }),
},
'stackblitz-cloud': {
label: 'Stackblitz',
factory: (files: Record<string, string>, 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<string, string> } & 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<string, string> } & Data) {
const normalizedFilesApi = Object.entries(files).reduce((acc, current) => {
acc[current[0]] = { isBinary: false, content: current[1] };
return acc;
}, {} as Record<string, { content: string; isBinary: boolean }>);

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);
}
Loading