Merge pull request #14874 from storybookjs/14831-extract-addon-knobs

Extract addon-knobs from monorepo
This commit is contained in:
Michael Shilman 2021-05-10 21:04:53 +08:00 committed by GitHub
commit 6fe7f9c49d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
104 changed files with 19 additions and 5739 deletions

View File

@ -1,536 +0,0 @@
# Storybook Addon Knobs
Storybook Addon Knobs allow you to edit props dynamically using the Storybook UI.
You can also use Knobs as a dynamic variable inside stories in [Storybook](https://storybook.js.org).
[Framework Support](https://github.com/storybookjs/storybook/blob/master/ADDONS_SUPPORT.md).
This is what Knobs looks like:
[![Storybook Knobs Demo](docs/storybook-knobs-example.png)](https://storybooks-official.netlify.com/?knob-Dollars=12.5&knob-Name=Storyteller&knob-Years%20in%20NY=9&knob-background=%23ffff00&knob-Age=70&knob-Items%5B0%5D=Laptop&knob-Items%5B1%5D=Book&knob-Items%5B2%5D=Whiskey&knob-Other%20Fruit=lime&knob-Birthday=1484870400000&knob-Nice=true&knob-Styles=%7B%22border%22%3A%223px%20solid%20%23ff00ff%22%2C%22padding%22%3A%2210px%22%7D&knob-Fruit=apple&selectedKind=Addons%7CKnobs.withKnobs&selectedStory=tweaks%20static%20values&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybooks%2Fstorybook-addon-knobs)
> Checkout the above [Live Storybook](https://storybooks-official.netlify.com/?knob-Dollars=12.5&knob-Name=Storyteller&knob-Years%20in%20NY=9&knob-background=%23ffff00&knob-Age=70&knob-Items%5B0%5D=Laptop&knob-Items%5B1%5D=Book&knob-Items%5B2%5D=Whiskey&knob-Other%20Fruit=lime&knob-Birthday=1484870400000&knob-Nice=true&knob-Styles=%7B%22border%22%3A%223px%20solid%20%23ff00ff%22%2C%22padding%22%3A%2210px%22%7D&knob-Fruit=apple&selectedKind=Addons%7CKnobs.withKnobs&selectedStory=tweaks%20static%20values&full=0&addons=1&stories=1&panelRight=0&addonPanel=storybooks%2Fstorybook-addon-knobs) or [watch this video](https://www.youtube.com/watch?v=kopW6vzs9dg&feature=youtu.be).
## Getting Started
First of all, you need to install Knobs into your project as a dev dependency.
```sh
yarn add @storybook/addon-knobs --dev
```
within `.storybook/main.js`:
```js
module.exports = {
addons: ['@storybook/addon-knobs'],
};
```
Now, write your stories with Knobs.
### With React
```js
import React from 'react';
import { withKnobs, text, boolean, number } from '@storybook/addon-knobs';
export default {
title: 'Storybook Knobs',
decorators: [withKnobs],
};
// Add the `withKnobs` decorator to add knobs support to your stories.
// You can also configure `withKnobs` as a global decorator.
// Knobs for React props
export const withAButton = () => (
<button disabled={boolean('Disabled', false)}>{text('Label', 'Hello Storybook')}</button>
);
// Knobs as dynamic variables.
export const asDynamicVariables = () => {
const name = text('Name', 'James');
const age = number('Age', 35);
const content = `I am ${name} and I'm ${age} years old.`;
return <div>{content}</div>;
};
```
### With Vue.js
MyButton.story.js:
```js
import { storiesOf } from '@storybook/vue';
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
import MyButton from './MyButton.vue';
export default {
title: 'Storybook Knobs',
decorators: [withKnobs],
};
// Assign `props` to the story's component, calling
// knob methods within the `default` property of each prop,
// then pass the story's prop data to the components prop in
// the template with `v-bind:` or by placing the prop within
// the components slot.
export const exampleWithKnobs = () => ({
components: { MyButton },
props: {
isDisabled: {
default: boolean('Disabled', false),
},
text: {
default: text('Text', 'Hello Storybook'),
},
},
template: `<MyButton :isDisabled="isDisabled">{{ text }}</MyButton>`,
});
```
MyButton.vue:
```vue
<template>
<button :disabled="isDisabled">
<slot></slot>
</button>
</template>
<script>
export default {
props: {
isDisabled: {
type: Boolean,
default: false,
},
},
};
</script>
```
### With Angular
```js
import { storiesOf } from '@storybook/angular';
import { boolean, number, text, withKnobs } from '@storybook/addon-knobs';
import { Button } from '@storybook/angular/demo';
export default {
title: 'Storybook Knobs',
decorators: [withKnobs],
};
export const withKnobs = () => ({
component: Button,
props: {
text: text('text', 'Hello Storybook'), // The first param of the knob function has to be exactly the same as the component input.
},
});
```
### With Ember
```js
import { withKnobs, text, boolean } from '@storybook/addon-knobs';
import { hbs } from 'ember-cli-htmlbars';
export default {
title: 'StoryBook with Knobs',
decorators: [withKnobs],
};
export const button = () => ({
template: hbs`
<button disabled={{disabled}}>{{label}}</button>
`,
context: {
label: text('label', 'Hello Storybook'),
disabled: boolean('disabled', false),
},
});
```
## Categorization
Categorize your Knobs by assigning them a `groupId`. When a `groupId` exists, tabs will appear in the Knobs storybook panel to filter between the groups. Knobs without a `groupId` are automatically categorized into the `ALL` group.
```js
export const inGroups = () => {
const personalGroupId = 'personal info';
const generalGroupId = 'general info';
const name = text('Name', 'James', personalGroupId);
const age = number('Age', 35, { min: 0, max: 99 }, personalGroupId);
const message = text('Hello!', 35, generalGroupId);
const content = `
I am ${name} and I'm ${age} years old.
${message}
`;
return <div>{content}</div>;
};
```
You can see your Knobs in a Storybook panel as shown below.
![](docs/demo.png)
## Available Knobs
These are the Knobs available for you to use. You can import these Knobs from the `@storybook/addon-knobs` module.
Here's how to import the **text** Knob.
```js
import { text } from '@storybook/addon-knobs';
```
Just like that, you can import any other following Knobs:
### text
Allows you to get some text from the user.
```js
import { text } from '@storybook/addon-knobs';
const label = 'Your Name';
const defaultValue = 'James';
const groupId = 'GROUP-ID1';
const value = text(label, defaultValue, groupId);
```
### boolean
Allows you to get a boolean value from the user.
```js
import { boolean } from '@storybook/addon-knobs';
const label = 'Agree?';
const defaultValue = false;
const groupId = 'GROUP-ID1';
const value = boolean(label, defaultValue, groupId);
```
### number
Allows you to get a number from the user.
```js
import { number } from '@storybook/addon-knobs';
const label = 'Age';
const defaultValue = 78;
const groupId = 'GROUP-ID1';
const value = number(label, defaultValue);
```
For use with `groupId`, pass the default `options` as the third argument.
```js
const value = number(label, defaultValue, {}, groupId);
```
### number bound by range
Allows you to get a number from the user using a range slider.
```js
import { number } from '@storybook/addon-knobs';
const label = 'Temperature';
const defaultValue = 73;
const options = {
range: true,
min: 60,
max: 90,
step: 1,
};
const groupId = 'GROUP-ID1';
const value = number(label, defaultValue, options, groupId);
```
### color
Allows you to get a colour from the user.
```js
import { color } from '@storybook/addon-knobs';
const label = 'Color';
const defaultValue = '#ff00ff';
const groupId = 'GROUP-ID1';
const value = color(label, defaultValue, groupId);
```
### object
Allows you to get a JSON object or array from the user.
```js
import { object } from '@storybook/addon-knobs';
const label = 'Styles';
const defaultValue = {
backgroundColor: 'red',
};
const groupId = 'GROUP-ID1';
const value = object(label, defaultValue, groupId);
```
> Make sure to enter valid JSON syntax while editing values inside the knob.
### array
Allows you to get an array of strings from the user.
```js
import { array } from '@storybook/addon-knobs';
const label = 'Styles';
const defaultValue = ['Red'];
const groupId = 'GROUP-ID1';
const value = array(label, defaultValue);
```
> While editing values inside the knob, you will need to use a separator.
> By default it is a comma, but this can be overridden by passing a separator variable.
>
> ```js
> import { array } from '@storybook/addon-knobs';
>
> const label = 'Styles';
> const defaultValue = ['Red'];
> const separator = ':';
> const value = array(label, defaultValue, separator);
> ```
For use with `groupId`, pass the default `separator` as the third argument.
```js
const value = array(label, defaultValue, ',', groupId);
```
### select
It allows you to get a value from a select box from the user.
```js
import { select } from '@storybook/addon-knobs';
const label = 'Colors';
const options = {
Red: 'red',
Blue: 'blue',
Yellow: 'yellow',
Rainbow: ['red', 'orange', 'etc'],
None: null,
};
const defaultValue = 'red';
const groupId = 'GROUP-ID1';
const value = select(label, options, defaultValue, groupId);
```
Options can also be an array:
```js
import { select } from '@storybook/addon-knobs';
const label = 'Cats';
const options = ['linus', 'eleanor', 'lover'];
const defaultValue = 'eleanor';
const groupId = 'GROUP-ID2';
const value = select(label, options, defaultValue, groupId);
```
Options can also be an array OF objects:
```js
const label = 'Dogs';
const arrayOfObjects = [
{
label: 'Sparky',
dogParent: 'Matthew',
location: 'Austin',
},
{
label: 'Juniper',
dogParent: 'Joshua',
location: 'Austin',
},
];
const defaultValue = arrayOfObjects[0];
const groupId = 'GROUP-ID3';
const value = select(label, arrayOfObjects, defaultValue, groupId);
```
### radio buttons
It allows you to get a value from a list of radio buttons from the user.
```js
import { radios } from '@storybook/addon-knobs';
const label = 'Fruits';
const options = {
Kiwi: 'kiwi',
Guava: 'guava',
Watermelon: 'watermelon',
};
const defaultValue = 'kiwi';
const groupId = 'GROUP-ID1';
const value = radios(label, options, defaultValue, groupId);
```
### options
Configurable UI for selecting a value from a set of options.
```js
import { optionsKnob } from '@storybook/addon-knobs';
const label = 'Fruits';
const valuesObj = {
Kiwi: 'kiwi',
Guava: 'guava',
Watermelon: 'watermelon',
};
const defaultValue = 'kiwi';
const optionsObj = {
display: 'inline-radio',
};
const groupId = 'GROUP-ID1';
const value = optionsKnob(label, valuesObj, defaultValue, optionsObj, groupId);
```
Alternatively you can use this import:
```
import { optionsKnob as options } from '@storybook/addon-knobs';
...
const value = options(label, valuesObj, defaultValue, optionsObj, groupId);
```
> The display property for `optionsObj` accepts:
>
> - `radio`
> - `inline-radio`
> - `check`
> - `inline-check`
> - `select`
> - `multi-select`
### files
It allows you to get a value from a file input from the user.
```js
import { files } from '@storybook/addon-knobs';
const label = 'Images';
const accept = '.xlsx, .pdf';
const defaultValue = [];
const groupId = 'GROUP-ID1';
const value = files(label, accept, defaultValue, groupId);
```
> You can optionally specify a [list of file types](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file#accept) which the file input should accept.
> Multiple files can be selected, and will be returned as an array of [Data URLs](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs).
### date
Allows you to get date (and time) from the user.
```js
import { date } from '@storybook/addon-knobs';
const label = 'Event Date';
const defaultValue = new Date('Jan 20 2017');
const groupId = 'GROUP-ID1';
const value = date(label, defaultValue, groupId);
```
> Note: the default value must not change - e.g., do not do `date('Label', new Date())` or `date('Label')`.
The `date` knob returns the selected date as stringified Unix timestamp (e.g. `"1510913096516"`).
If your component needs the date in a different form you can wrap the `date` function:
```js
function myDateKnob(name, defaultValue) {
const stringTimestamp = date(name, defaultValue);
return new Date(stringTimestamp);
}
```
### button
It allows you to include a button and associated handler.
```js
import { button } from '@storybook/addon-knobs';
const label = 'Do Something';
const handler = () => doSomething('foobar');
const groupId = 'GROUP-ID1';
button(label, handler, groupId);
```
Button knobs cause the story to re-render after the handler fires.
You can prevent this by having the handler return `false`.
### withKnobs options
withKnobs also accepts two optional options as story parameters.
Usage:
```js
import { withKnobs } from '@storybook/addon-knobs';
export default {
title: 'Storybook Knobs',
decorators: [withKnobs],
};
export const defaultView = () => <div />;
defaultView.parameters = {
knobs: {
// Doesn't emit events while user is typing.
timestamps: true,
// Escapes strings to be safe for inserting as innerHTML. This option is true by default. It's safe to set it to `false` with frameworks like React which do escaping on their side.
// You can still set it to false, but it's strongly discouraged to set to true in cases when you host your storybook on some route of your main site or web app.
escapeHTML: true,
},
};
```
## Typescript
If you are using Typescript, make sure you have the type definitions installed for the following libs:
- node
- react
You can install them using: (_assuming you are using Typescript >2.0._)
```sh
yarn add @types/node @types/react --dev
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 699 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 85 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 157 KiB

View File

@ -1,94 +0,0 @@
{
"name": "@storybook/addon-knobs",
"version": "6.3.0-alpha.21",
"description": "Storybook addon prop editor component",
"keywords": [
"addon",
"storybook",
"test"
],
"homepage": "https://github.com/storybookjs/storybook/tree/master/addons/knobs",
"bugs": {
"url": "https://github.com/storybookjs/storybook/issues"
},
"repository": {
"type": "git",
"url": "https://github.com/storybookjs/storybook.git",
"directory": "addons/knobs"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/storybook"
},
"license": "MIT",
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"types": "dist/ts3.9/index.d.ts",
"typesVersions": {
"<3.8": {
"*": [
"dist/ts3.4/*"
]
}
},
"files": [
"dist/**/*",
"README.md",
"*.js",
"*.d.ts"
],
"scripts": {
"prepare": "node ../../scripts/prepare.js"
},
"dependencies": {
"@storybook/addons": "6.3.0-alpha.21",
"@storybook/api": "6.3.0-alpha.21",
"@storybook/channels": "6.3.0-alpha.21",
"@storybook/client-api": "6.3.0-alpha.21",
"@storybook/components": "6.3.0-alpha.21",
"@storybook/core-events": "6.3.0-alpha.21",
"@storybook/theming": "6.3.0-alpha.21",
"copy-to-clipboard": "^3.3.1",
"core-js": "^3.8.2",
"escape-html": "^1.0.3",
"fast-deep-equal": "^3.1.3",
"global": "^4.4.0",
"lodash": "^4.17.20",
"prop-types": "^15.7.2",
"qs": "^6.10.0",
"react-colorful": "^5.1.2",
"react-lifecycles-compat": "^3.0.4",
"react-select": "^3.2.0",
"regenerator-runtime": "^0.13.7"
},
"devDependencies": {
"@types/enzyme": "^3.10.8",
"@types/escape-html": "1.0.0",
"@types/react-lifecycles-compat": "^3.0.1",
"@types/react-select": "^3.1.2",
"@types/webpack-env": "^1.16.0",
"enzyme": "^3.11.0"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0",
"react-dom": "^16.8.0 || ^17.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"react-dom": {
"optional": true
}
},
"publishConfig": {
"access": "public"
},
"gitHead": "c3ffd75d5ae104f678f2f0bea2042b017373aa4a",
"storybook": {
"displayName": "Knobs",
"unsupportedFrameworks": [
"react-native"
]
}
}

View File

@ -1,13 +0,0 @@
function managerEntries(entry = [], options) {
return [...entry, require.resolve('./dist/esm/register')];
}
function config(entry = [], { addDecorator = true } = {}) {
const knobsConfig = [];
if (addDecorator) {
knobsConfig.push(require.resolve('./dist/esm/preset/addDecorator'));
}
return [...entry, ...knobsConfig];
}
module.exports = { managerEntries, config };

View File

@ -1 +0,0 @@
require('./dist/esm/register');

View File

@ -1,113 +0,0 @@
import KnobManager from './KnobManager';
jest.mock('global', () => ({
navigator: { userAgent: 'browser', platform: '' },
window: {
__STORYBOOK_CLIENT_API__: undefined,
addEventListener: jest.fn(),
location: { search: '' },
history: { replaceState: jest.fn() },
},
document: {
addEventListener: jest.fn(),
getElementById: jest.fn().mockReturnValue({}),
body: { classList: { add: jest.fn(), remove: jest.fn() } },
documentElement: {},
location: { search: '?id=kind--story' },
},
}));
describe('KnobManager', () => {
describe('knob()', () => {
describe('when the knob is present in the knobStore', () => {
const testManager = new KnobManager();
beforeEach(() => {
testManager.knobStore = {
set: jest.fn(),
update: jest.fn(),
get: () =>
({
defaultValue: 'default value',
name: 'foo',
type: 'string',
value: 'current value',
} as any),
} as any;
});
it('should return the existing knob value when types match', () => {
const defaultKnob = {
name: 'foo',
type: 'string',
value: 'default value',
} as any;
const knob = testManager.knob('foo', defaultKnob);
expect(knob).toEqual('current value');
expect(testManager.knobStore.set).not.toHaveBeenCalled();
});
it('should update the existing knob options when types match', () => {
const defaultKnob = {
name: 'foo',
type: 'string',
value: 'default value',
foo: 'foo',
} as any;
testManager.knob('foo', defaultKnob);
expect(testManager.knobStore.update).toHaveBeenCalledWith(
'foo',
expect.objectContaining({ foo: 'foo' })
);
});
it('should return the new default knob value when type has changed', () => {
const defaultKnob = {
name: 'foo',
value: true,
type: 'boolean',
} as any;
testManager.knob('foo', defaultKnob);
const newKnob = {
...defaultKnob,
label: 'foo',
defaultValue: defaultKnob.value,
};
expect(testManager.knobStore.set).toHaveBeenCalledWith('foo', newKnob);
});
});
describe('when the knob is not present in the knobStore', () => {
const testManager = new KnobManager();
beforeEach(() => {
testManager.knobStore = {
set: jest.fn(),
get: jest.fn(),
} as any;
(testManager.knobStore as any).get
.mockImplementationOnce(() => undefined)
.mockImplementationOnce(() => 'normal value');
});
it('should return the new default knob value when default has changed', () => {
const defaultKnob = {
name: 'foo',
value: 'normal value',
} as any;
testManager.knob('foo', defaultKnob);
const newKnob = {
...defaultKnob,
label: 'foo',
defaultValue: defaultKnob.value,
};
expect(testManager.knobStore.set).toHaveBeenCalledWith('foo', newKnob);
});
});
});
});

View File

@ -1,147 +0,0 @@
/* eslint no-underscore-dangle: 0 */
import { navigator } from 'global';
import escape from 'escape-html';
import { getQueryParams } from '@storybook/client-api';
import { Channel } from '@storybook/channels';
import KnobStore, { KnobStoreKnob } from './KnobStore';
import { Knob, KnobType, Mutable } from './type-defs';
import { SET } from './shared';
import { deserializers } from './converters';
const knobValuesFromUrl: Record<string, string> = Object.entries(getQueryParams()).reduce(
(acc, [k, v]) => {
if (k.includes('knob-')) {
return { ...acc, [k.replace('knob-', '')]: v };
}
return acc;
},
{}
);
// This is used by _mayCallChannel to determine how long to wait to before triggering a panel update
const PANEL_UPDATE_INTERVAL = 400;
function escapeStrings(obj: { [key: string]: string }): { [key: string]: string };
function escapeStrings(obj: (string | string[])[]): (string | string[])[];
function escapeStrings(obj: string): string;
function escapeStrings(obj: any): any {
if (typeof obj === 'string') {
return escape(obj);
}
if (obj == null || typeof obj !== 'object') {
return obj;
}
if (Array.isArray(obj)) {
const newArray = obj.map(escapeStrings);
const didChange = newArray.some((newValue, key) => newValue !== obj[key]);
return didChange ? newArray : obj;
}
return Object.entries<{ [key: string]: string }>(obj).reduce((acc, [key, oldValue]) => {
const newValue = escapeStrings(oldValue);
return newValue === oldValue ? acc : { ...acc, [key]: newValue };
}, obj);
}
interface KnobManagerOptions {
escapeHTML?: boolean;
disableDebounce?: boolean;
disableForceUpdate?: boolean;
}
export default class KnobManager {
knobStore = new KnobStore();
channel: Channel | undefined;
options: KnobManagerOptions = {};
calling = false;
setChannel(channel: Channel) {
this.channel = channel;
}
setOptions(options: KnobManagerOptions) {
this.options = options;
}
getKnobValue({ value }: Knob) {
return this.options.escapeHTML ? escapeStrings(value) : value;
}
knob<T extends KnobType = any>(name: string, options: Knob<T>): Mutable<Knob<T>['value']> {
this._mayCallChannel();
const knobName = options.groupId ? `${name}_${options.groupId}` : name;
const { knobStore } = this;
const existingKnob = knobStore.get(knobName);
// We need to return the value set by the knob editor via this.
// Normally the knobs are reset and so re-use is safe as long as the types match
// when in storyshots, though the change event isn't called and so the knobs aren't reset, making this code fail
// so always create a new knob when in storyshots
if (
existingKnob &&
options.type === existingKnob.type &&
navigator &&
// userAgent is not set in react-native
(!navigator.userAgent || !navigator.userAgent.includes('jsdom'))
) {
const { value, ...restOptions } = options;
knobStore.update(knobName, restOptions);
return this.getKnobValue(existingKnob);
}
const knobInfo: Knob<T> & { name: string; label: string; defaultValue?: any } = {
...options,
name: knobName,
label: name,
};
if (knobValuesFromUrl[knobName]) {
const value = deserializers[options.type](knobValuesFromUrl[knobName]);
knobInfo.defaultValue = value;
knobInfo.value = value;
delete knobValuesFromUrl[knobName];
} else {
knobInfo.defaultValue = options.value;
}
knobStore.set(knobName, knobInfo as KnobStoreKnob);
return this.getKnobValue(knobStore.get(knobName));
}
_mayCallChannel() {
// Re rendering of the story may cause changes to the knobStore. Some new knobs maybe added and
// Some knobs may go unused. So we need to update the panel accordingly. For example remove the
// unused knobs from the panel. This function sends the `setKnobs` message to the channel
// triggering a panel re-render.
if (!this.channel) {
// to prevent call to undefined channel and therefore throwing TypeError
return;
}
if (this.calling) {
// If a call to channel has already registered ignore this call.
// Once the previous call is completed all the changes to knobStore including the one that
// triggered this, will be added to the panel.
// This avoids emitting to the channel within very short periods of time.
return;
}
this.calling = true;
const timestamp = +new Date();
setTimeout(() => {
this.calling = false;
// emit to the channel and trigger a panel re-render
if (this.channel) this.channel.emit(SET, { knobs: this.knobStore.getAll(), timestamp });
}, PANEL_UPDATE_INTERVAL);
}
}

View File

@ -1,79 +0,0 @@
import { Knob } from './type-defs';
type Callback = () => any;
export type KnobStoreKnob = Knob & {
name: string;
label: string;
used?: boolean;
defaultValue?: any;
hideLabel?: boolean;
callback?: () => any;
};
const callArg = (fn: Callback) => fn();
const callAll = (fns: Callback[]) => fns.forEach(callArg);
export default class KnobStore {
store: Record<string, KnobStoreKnob> = {};
callbacks: Callback[] = [];
timer: number | undefined;
has(key: string) {
return this.store[key] !== undefined;
}
set(key: string, value: KnobStoreKnob) {
this.store[key] = {
...value,
used: true,
groupId: value.groupId,
};
// debounce the execution of the callbacks for 50 milliseconds
if (this.timer) {
clearTimeout(this.timer);
}
this.timer = setTimeout(callAll, 50, this.callbacks) as number;
}
update(key: string, options: Partial<KnobStoreKnob>) {
this.store[key] = {
...this.store[key],
...options,
} as KnobStoreKnob;
}
get(key: string) {
const knob = this.store[key];
if (knob) {
knob.used = true;
}
return knob;
}
getAll() {
return this.store;
}
reset() {
this.store = {};
}
markAllUnused() {
Object.keys(this.store).forEach((knobName) => {
this.store[knobName].used = false;
});
}
subscribe(cb: Callback) {
this.callbacks.push(cb);
}
unsubscribe(cb: Callback) {
const index = this.callbacks.indexOf(cb);
this.callbacks.splice(index, 1);
}
}

View File

@ -1,339 +0,0 @@
import {
number,
color,
files,
object,
boolean,
text,
select,
date,
array,
button,
knob,
radios,
optionsKnob as options,
} from '../index';
// Note: this is a helper to batch test return types and avoid "declared but never read" errors
function expectKnobOfType<T>(..._: T[]) {}
const groupId = 'GROUP-ID1';
/** Text knob */
expectKnobOfType<string>(
text('text simple', 'Batman'),
text('text with group', 'default', groupId)
);
/** Date knob */
expectKnobOfType<number>(
date('date simple', new Date('January 20 1887')),
date('date with group', new Date(), groupId)
);
/** Boolean knob */
expectKnobOfType<boolean>(
boolean('boolean simple', false),
boolean('boolean with group', true, groupId)
);
/** Color knob */
expectKnobOfType<string>(
color('color simple', 'black'),
color('color with group', '#ffffff', groupId)
);
/** Number knob */
expectKnobOfType<number>(
number('number basic', 42),
number('number with options', 72, { range: true, min: 60, max: 90, step: 1 }),
number('number with group', 1, {}, groupId)
);
/** Radios knob */
expectKnobOfType<string>(
radios(
'radio with string values',
{
1100: '1100',
2200: '2200',
3300: '3300',
},
'2200'
)
);
expectKnobOfType<number>(radios('radio with number values', { 3: 3, 7: 7, 23: 23 }, 3));
expectKnobOfType<string | number | null>(
radios(
'radio with mixed value',
{
1100: '1100',
2200: 2200,
3300: '3300',
},
null,
groupId
)
);
/** Select knob */
enum SomeEnum {
Type1 = 1,
Type2,
}
enum ButtonVariant {
primary = 'primary',
secondary = 'secondary',
}
const stringLiteralArray: ('Apple' | 'Banana' | 'Grapes')[] = ['Apple', 'Banana', 'Grapes'];
expectKnobOfType<string>(
select(
'select with string options',
{
None: 'none',
Underline: 'underline',
'Line-through': 'line-through',
},
'none'
),
select<string>('select with string array', ['yes', 'no'], 'yes'),
select('select with string literal array', stringLiteralArray, stringLiteralArray[0]),
select('select with readonly array', ['red', 'blue'] as const, 'red'),
select<ButtonVariant>('select with string enum options', ButtonVariant, ButtonVariant.primary)
);
expectKnobOfType<string | undefined | null | boolean>(
select(
'select with an undefined in array',
['Apple', 'Banana', 'Grapes', undefined, null, false] as const,
undefined
)
);
expectKnobOfType<string | null>(
select('select with null option', { a: 'Option', b: null }, null, groupId)
);
expectKnobOfType<number>(
select('select with number options', { 'type a': 1, 'type b': 2 }, 1),
select<SomeEnum>(
'select with numeric enum options',
{ 'type a': SomeEnum.Type1, 'type b': SomeEnum.Type2 },
SomeEnum.Type2
),
select<number>('select with number array', [1, 2, 3, 4], 1),
select('select with readonly number array', [1, 2] as const, 1)
);
expectKnobOfType<number | null>(
select('select with null option', { a: 1, b: null }, null, groupId)
);
expectKnobOfType<string | string[]>(
select(
'select with string and string array options',
{
Red: 'red',
Blue: 'blue',
Yellow: 'yellow',
Rainbow: ['red', 'orange', 'etc'],
None: 'transparent',
},
'red'
)
);
expectKnobOfType<number | number[]>(
select(
'select with number and number array options',
{
Red: 1,
Blue: 2,
Yellow: 3,
Rainbow: [4, 5, 6],
None: 7,
},
7
)
);
expectKnobOfType<string | string[] | null>(
select(
'select with string, string array, and null options',
{
Red: 'red',
Blue: 'blue',
Yellow: 'yellow',
Rainbow: ['red', 'orange', 'etc'],
None: null,
},
null
)
);
expectKnobOfType<number[]>(
select(
'select with number array options',
{
ones: [1],
twos: [2, 2],
threes: [3, 3, 3],
},
[1]
)
);
/** Object knob */
expectKnobOfType(
object('object simple', {
fontFamily: 'Arial',
padding: 20,
}),
object('object with group', {}, groupId)
);
/** Options knob */
type Tool = 'hammer' | 'saw' | 'drill';
const visibleToolOptions: Record<string, Tool> = {
hammer: 'hammer',
saw: 'saw',
drill: 'drill',
};
expectKnobOfType(
options<Tool>('options with single selection', visibleToolOptions, 'hammer', {
display: 'check',
}),
options<Tool>('options with multi selection', visibleToolOptions, ['hammer', 'saw'], {
display: 'inline-check',
}),
options<Tool>('options with readonly multi selection', visibleToolOptions, ['hammer'] as const, {
display: 'radio',
}),
options('options with group', {}, '', { display: 'check' })
);
expectKnobOfType<number | null>(
options('select with null option', { a: 1, b: null }, null, { display: 'check' })
);
expectKnobOfType<string | string[]>(
options(
'options with string and string array options',
{
Red: 'red',
Blue: 'blue',
Yellow: 'yellow',
Rainbow: ['red', 'orange', 'etc'],
None: 'transparent',
},
'red',
{ display: 'check' }
)
);
expectKnobOfType<number | number[]>(
options(
'select with number and number array options',
{
Red: 1,
Blue: 2,
Yellow: 3,
Rainbow: [4, 5, 6],
None: 7,
},
7,
{ display: 'check' }
)
);
expectKnobOfType<string | string[] | null>(
options(
'select with string, string array, and null options',
{
Red: 'red',
Blue: 'blue',
Yellow: 'yellow',
Rainbow: ['red', 'orange', 'etc'],
None: null,
},
null,
{ display: 'check' }
)
);
expectKnobOfType<number[]>(
options(
'select with number array options',
{
ones: [1],
twos: [2, 2],
threes: [3, 3, 3],
},
[1],
{ display: 'check' }
)
);
/** Array knob */
const arrayReadonly = array('array as readonly', ['hi', 'there'] as const);
expectKnobOfType<string[]>(
array('array simple', ['red', 'green', 'blue']),
arrayReadonly,
array('array with group', [], ',', groupId)
);
// Should return a mutable array despite the readonly input
arrayReadonly.push('Make sure that the output is still mutable although the input need not be!');
/** Button knob */
expectKnobOfType(
button('button simple', () => {}),
button('button with group', () => undefined, groupId)
);
/** Files knob */
expectKnobOfType<string[]>(
files('files simple', 'image/*', []),
files('files with group', 'image/*', ['img.jpg'], groupId)
);
/** Generic knob */
expectKnobOfType<string>(
knob('generic knob as text', { type: 'text', value: 'a' }),
knob('generic knob as color', { type: 'color', value: 'black' }),
knob<'select', string>('generic knob as string select', {
type: 'select',
value: 'yes',
options: ['yes', 'no'],
selectV2: true,
})
);
expectKnobOfType<number>(
knob('generic knob as number', { type: 'number', value: 42 }),
knob('generic knob as select', { type: 'radios', value: 3, options: { 3: 3, 7: 7, 23: 23 } }),
knob('generic knob as number select', {
type: 'select',
value: 1,
options: [1, 2],
selectV2: true,
})
);

View File

@ -1,305 +0,0 @@
import React, { PureComponent, Fragment, Validator } from 'react';
import PropTypes from 'prop-types';
import qs from 'qs';
import { document } from 'global';
import { styled } from '@storybook/theming';
import copy from 'copy-to-clipboard';
import { STORY_CHANGED } from '@storybook/core-events';
import {
Placeholder,
TabWrapper,
TabsState,
ActionBar,
Link,
ScrollArea,
} from '@storybook/components';
import { API } from '@storybook/api';
import { RESET, SET, CHANGE, SET_OPTIONS, CLICK } from '../shared';
import { getKnobControl } from './types';
import PropForm from './PropForm';
import { KnobStoreKnob } from '../KnobStore';
const getTimestamp = () => +new Date();
export const DEFAULT_GROUP_ID = 'Other';
const PanelWrapper = styled(({ children, className }) => (
<ScrollArea horizontal vertical className={className}>
{children}
</ScrollArea>
))({
height: '100%',
width: '100%',
});
interface PanelKnobGroups {
title: string;
render: (knob: any) => any;
}
interface KnobPanelProps {
active: boolean;
onReset?: object;
api: Pick<API, 'on' | 'off' | 'emit' | 'getQueryParam' | 'setQueryParams'>;
}
interface KnobPanelState {
knobs: Record<string, KnobStoreKnob>;
}
interface KnobPanelOptions {
timestamps?: boolean;
}
export default class KnobPanel extends PureComponent<KnobPanelProps> {
static propTypes = {
active: PropTypes.bool.isRequired as Validator<KnobPanelProps['active']>,
onReset: PropTypes.object as Validator<KnobPanelProps['onReset']>, // eslint-disable-line
api: PropTypes.shape({
on: PropTypes.func,
off: PropTypes.func,
emit: PropTypes.func,
getQueryParam: PropTypes.func,
setQueryParams: PropTypes.func,
}).isRequired as Validator<KnobPanelProps['api']>,
};
static defaultProps: KnobPanelProps = {
active: true,
api: {
on: () => () => {},
off: () => {},
emit: () => {},
getQueryParam: () => undefined,
setQueryParams: () => {},
},
};
state: KnobPanelState = {
knobs: {},
};
options: KnobPanelOptions = {};
lastEdit: number = getTimestamp();
loadedFromUrl = false;
mounted = false;
componentDidMount() {
this.mounted = true;
const { api } = this.props;
api.on(SET, this.setKnobs);
api.on(SET_OPTIONS, this.setOptions);
this.stopListeningOnStory = api.on(STORY_CHANGED, () => {
if (this.mounted) {
this.setKnobs({ knobs: {} });
}
this.setKnobs({ knobs: {} });
});
}
componentWillUnmount() {
this.mounted = false;
const { api } = this.props;
api.off(SET, this.setKnobs);
this.stopListeningOnStory();
}
setOptions = (options: KnobPanelOptions = { timestamps: false }) => {
this.options = options;
};
setKnobs = ({
knobs,
timestamp,
}: {
knobs: Record<string, KnobStoreKnob>;
timestamp?: number;
}) => {
const queryParams: Record<string, any> = {};
const { api } = this.props;
if (!this.options.timestamps || !timestamp || this.lastEdit <= timestamp) {
Object.keys(knobs).forEach((name) => {
const knob = knobs[name];
// For the first time, get values from the URL and set them.
if (!this.loadedFromUrl) {
const urlValue = api.getQueryParam(`knob-${name}`);
// If the knob value present in url
if (urlValue !== undefined) {
const value = getKnobControl(knob.type).deserialize(urlValue);
knob.value = value;
queryParams[`knob-${name}`] = getKnobControl(knob.type).serialize(value);
api.emit(CHANGE, knob);
}
}
});
api.setQueryParams(queryParams);
this.setState({ knobs });
this.loadedFromUrl = true;
}
};
reset = () => {
const { api } = this.props;
api.emit(RESET);
};
copy = () => {
const { location } = document;
const query = qs.parse(location.search, { ignoreQueryPrefix: true });
const { knobs } = this.state;
Object.entries(knobs).forEach(([name, knob]) => {
query[`knob-${name}`] = getKnobControl(knob.type).serialize(knob.value);
});
copy(`${location.origin + location.pathname}?${qs.stringify(query, { encode: false })}`);
// TODO: show some notification of this
};
emitChange = (changedKnob: KnobStoreKnob) => {
const { api } = this.props;
api.emit(CHANGE, changedKnob);
};
handleChange = (changedKnob: KnobStoreKnob) => {
this.lastEdit = getTimestamp();
const { api } = this.props;
const { knobs } = this.state;
const { name } = changedKnob;
const newKnobs = { ...knobs };
newKnobs[name] = {
...newKnobs[name],
...changedKnob,
};
this.setState({ knobs: newKnobs }, () => {
this.emitChange(changedKnob);
const queryParams: { [key: string]: any } = {};
Object.keys(newKnobs).forEach((n) => {
const knob = newKnobs[n];
queryParams[`knob-${n}`] = getKnobControl(knob.type).serialize(knob.value);
});
api.setQueryParams(queryParams);
});
};
handleClick = (knob: KnobStoreKnob) => {
const { api } = this.props;
api.emit(CLICK, knob);
};
stopListeningOnStory!: Function;
render() {
const { knobs } = this.state;
const { active: panelActive } = this.props;
if (!panelActive) {
return null;
}
const groups: Record<string, PanelKnobGroups> = {};
const groupIds: string[] = [];
const knobKeysArray = Object.keys(knobs).filter((key) => knobs[key].used);
knobKeysArray.forEach((key) => {
const knobKeyGroupId = knobs[key].groupId || DEFAULT_GROUP_ID;
groupIds.push(knobKeyGroupId);
groups[knobKeyGroupId] = {
render: ({ active }) => (
<TabWrapper key={knobKeyGroupId} active={active}>
<PropForm
knobs={knobsArray.filter(
(knob) => (knob.groupId || DEFAULT_GROUP_ID) === knobKeyGroupId
)}
onFieldChange={this.handleChange}
onFieldClick={this.handleClick}
/>
</TabWrapper>
),
title: knobKeyGroupId,
};
});
const knobsArray = knobKeysArray.map((key) => knobs[key]);
if (knobsArray.length === 0) {
return (
<Placeholder>
<Fragment>No knobs found</Fragment>
<Fragment>
Learn how to&nbsp;
<Link
href="https://github.com/storybookjs/storybook/tree/master/addons/knobs"
target="_blank"
withArrow
cancel={false}
>
dynamically interact with components
</Link>
</Fragment>
</Placeholder>
);
}
// Always sort DEFAULT_GROUP_ID (ungrouped) tab last without changing the remaining tabs
const sortEntries = (g: Record<string, PanelKnobGroups>): [string, PanelKnobGroups][] => {
const unsortedKeys = Object.keys(g);
if (unsortedKeys.includes(DEFAULT_GROUP_ID)) {
const sortedKeys = unsortedKeys.filter((key) => key !== DEFAULT_GROUP_ID);
sortedKeys.push(DEFAULT_GROUP_ID);
return sortedKeys.map<[string, PanelKnobGroups]>((key) => [key, g[key]]);
}
return Object.entries(g);
};
const entries = sortEntries(groups);
return (
<Fragment>
<PanelWrapper>
{entries.length > 1 ? (
<TabsState>
{entries.map(([k, v]) => (
<div id={k} key={k} title={v.title}>
{v.render}
</div>
))}
</TabsState>
) : (
<PropForm
knobs={knobsArray}
onFieldChange={this.handleChange}
onFieldClick={this.handleClick}
/>
)}
</PanelWrapper>
<ActionBar
actionItems={[
{ title: 'Copy', onClick: this.copy },
{ title: 'Reset', onClick: this.reset },
]}
/>
</Fragment>
);
}
}

View File

@ -1,63 +0,0 @@
import React, { Component, ComponentType, Validator } from 'react';
import PropTypes from 'prop-types';
import { Form } from '@storybook/components';
import { getKnobControl } from './types';
import { KnobStoreKnob } from '../KnobStore';
interface PropFormProps {
knobs: KnobStoreKnob[];
onFieldChange: (changedKnob: KnobStoreKnob) => void;
onFieldClick: (knob: KnobStoreKnob) => void;
}
const InvalidType = () => <span>Invalid Type</span>;
export default class PropForm extends Component<PropFormProps> {
static displayName = 'PropForm';
static defaultProps = {
knobs: [] as KnobStoreKnob[],
onFieldChange: () => {},
onFieldClick: () => {},
};
static propTypes = {
knobs: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string,
value: PropTypes.any,
})
).isRequired as Validator<PropFormProps['knobs']>,
onFieldChange: PropTypes.func.isRequired as Validator<PropFormProps['onFieldChange']>,
onFieldClick: PropTypes.func.isRequired as Validator<PropFormProps['onFieldClick']>,
};
makeChangeHandler(name: string, type: string) {
const { onFieldChange } = this.props;
return (value = '') => {
const change: KnobStoreKnob = { name, type, value } as any;
onFieldChange(change);
};
}
render() {
const { knobs, onFieldClick } = this.props;
return (
<Form>
{knobs.map((knob) => {
const changeHandler = this.makeChangeHandler(knob.name, knob.type);
const InputType: ComponentType<any> = getKnobControl(knob.type) || InvalidType;
return (
<Form.Field key={knob.name} label={!knob.hideLabel && `${knob.label || knob.name}`}>
<InputType knob={knob} onChange={changeHandler} onClick={onFieldClick} />
</Form.Field>
);
})}
</Form>
);
}
}

View File

@ -1,130 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import ReactSelect from 'react-select';
import OptionsType from '../types/Options';
const mockOn = jest.fn();
describe('Options', () => {
let knob;
let wrapper;
let firstLabel;
let firstInput;
let lastInput;
describe('renders checkbox input', () => {
beforeEach(() => {
knob = {
name: 'Color',
defaultValue: ['#0ff'],
options: {
Red: '#f00',
Green: '#090',
Blue: '#0ff',
},
optionsObj: {
display: 'check',
},
};
wrapper = mount(<OptionsType knob={knob} onChange={mockOn} />);
firstLabel = wrapper.find('label').first();
firstInput = wrapper.find('input').first();
lastInput = wrapper.find('input').last();
});
it('correctly renders label', () => {
expect(firstLabel.text()).toEqual('Red');
});
it('correctly sets checkbox value', () => {
expect(firstInput.prop('value')).toEqual('#f00');
});
it('marks the correct default checkbox as checked', () => {
expect(firstInput.prop('checked')).toEqual(false);
expect(lastInput.prop('checked')).toEqual(true);
});
it('updates on change event', () => {
expect(wrapper.props().knob.defaultValue).toEqual(['#0ff']);
firstInput.simulate('change');
expect(mockOn).toHaveBeenCalled();
expect(wrapper.props().knob.defaultValue).toEqual(['#0ff', '#f00']);
});
});
describe('renders radio input', () => {
beforeEach(() => {
knob = {
name: 'Color',
value: '#0ff',
options: {
Red: '#f00',
Green: '#090',
Blue: '#0ff',
},
optionsObj: {
display: 'radio',
},
};
wrapper = mount(<OptionsType knob={knob} onChange={mockOn} />);
firstLabel = wrapper.find('label').first();
firstInput = wrapper.find('input').first();
lastInput = wrapper.find('input').last();
});
it('correctly renders label', () => {
expect(firstLabel.text()).toEqual('Red');
});
it('correctly sets radio input value', () => {
expect(firstInput.prop('value')).toEqual('#f00');
});
it('marks the correct default radio input as checked', () => {
expect(firstInput.prop('checked')).toEqual(false);
expect(lastInput.prop('checked')).toEqual(true);
});
it('updates on change event', () => {
firstInput.simulate('change');
expect(mockOn).toHaveBeenCalled();
});
});
describe('renders select input', () => {
let selectInput;
beforeEach(() => {
knob = {
name: 'Color',
value: '#0ff',
options: {
Red: '#f00',
Green: '#090',
Blue: '#0ff',
},
optionsObj: {
display: 'select',
},
};
wrapper = mount(<OptionsType knob={knob} onChange={mockOn} />);
selectInput = wrapper.find(ReactSelect).find('input');
});
it('updates when dropdown is opened and first option selected', () => {
// Simulate the arrow down event to open the dropdown menu.
selectInput.simulate('keyDown', { key: 'ArrowDown', keyCode: 40 });
// Simulate the enter key to select the first option.
selectInput.simulate('keyDown', { key: 'Enter', keyCode: 13 });
// selectInput.simulate('change');
expect(mockOn).toHaveBeenCalled();
});
});
});

View File

@ -1,236 +0,0 @@
import React from 'react';
import { shallow, mount } from 'enzyme';
import { STORY_CHANGED } from '@storybook/core-events';
import { TabsState } from '@storybook/components';
import { ThemeProvider, themes, convert } from '@storybook/theming';
import Panel, { DEFAULT_GROUP_ID } from '../Panel';
import { CHANGE, SET } from '../../shared';
import PropForm from '../PropForm';
const createTestApi = () => ({
on: jest.fn(),
emit: jest.fn(),
});
// React.memo in Tabs is causing problems with enzyme, probably
// due to https://github.com/airbnb/enzyme/issues/1875, so this
// is a workaround
jest.mock('react', () => {
const r = jest.requireActual('react');
return { ...r, memo: (x) => x };
});
describe('Panel', () => {
it('should subscribe to setKnobs event of channel', () => {
const testApi = createTestApi();
shallow(<Panel api={testApi} active />);
expect(testApi.on).toHaveBeenCalledWith(SET, expect.any(Function));
});
it('should subscribe to STORY_CHANGE event', () => {
const testApi = createTestApi();
shallow(<Panel api={testApi} active />);
expect(testApi.on.mock.calls).toContainEqual([STORY_CHANGED, expect.any(Function)]);
expect(testApi.on).toHaveBeenCalledWith(SET, expect.any(Function));
});
describe('setKnobs handler', () => {
it('should read url params and set values for existing knobs', () => {
const handlers = {};
const testQueryParams = {
'knob-foo': 'test string',
bar: 'some other string',
};
const testApi = {
on: (e, handler) => {
handlers[e] = handler;
},
emit: jest.fn(),
getQueryParam: (key) => testQueryParams[key],
setQueryParams: jest.fn(),
};
shallow(<Panel api={testApi} active />);
const setKnobsHandler = handlers[SET];
const knobs = {
foo: {
name: 'foo',
value: 'default string',
type: 'text',
},
baz: {
name: 'baz',
value: 'another knob value',
type: 'text',
},
};
setKnobsHandler({ knobs, timestamp: +new Date() });
const knobFromUrl = {
name: 'foo',
value: testQueryParams['knob-foo'],
type: 'text',
};
const e = CHANGE;
expect(testApi.emit).toHaveBeenCalledWith(e, knobFromUrl);
});
});
describe('handleChange()', () => {
it('should set queryParams and emit knobChange event', () => {
const testApi = {
getQueryParam: jest.fn(),
setQueryParams: jest.fn(),
on: jest.fn(),
emit: jest.fn(),
};
const wrapper = shallow(<Panel api={testApi} active />);
const testChangedKnob = {
name: 'foo',
value: 'changed text',
type: 'text',
};
wrapper.instance().handleChange(testChangedKnob);
expect(testApi.emit).toHaveBeenCalledWith(CHANGE, testChangedKnob);
// const paramsChange = { 'knob-foo': 'changed text' };
// expect(testApi.setQueryParams).toHaveBeenCalledWith(paramsChange);
});
});
describe('groups', () => {
const testApi = {
off: jest.fn(),
emit: jest.fn(),
getQueryParam: jest.fn(),
setQueryParams: jest.fn(),
on: jest.fn(() => () => {}),
};
it('should have no tabs when there are no groupIds', () => {
// Unfortunately, a shallow render will not invoke the render() function of the groups --
// it thinks they are unnamed function components (what they effectively are anyway).
//
// We have to do a full mount.
const root = mount(
<ThemeProvider theme={convert(themes.light)}>
<Panel api={testApi} active />
</ThemeProvider>
);
testApi.on.mock.calls[0][1]({
knobs: {
foo: {
name: 'foo',
defaultValue: 'test',
used: true,
// no groupId
},
bar: {
name: 'bar',
defaultValue: 'test2',
used: true,
// no groupId
},
},
});
const wrapper = root.update().find(Panel);
const formWrapper = wrapper.find(PropForm);
const knobs = formWrapper.map((formInstanceWrapper) => formInstanceWrapper.prop('knobs'));
expect(knobs).toMatchSnapshot();
root.unmount();
});
it('should have one tab per groupId when all are defined', () => {
const root = mount(
<ThemeProvider theme={convert(themes.light)}>
<Panel api={testApi} active />
</ThemeProvider>
);
testApi.on.mock.calls[0][1]({
knobs: {
foo: {
name: 'foo',
defaultValue: 'test',
used: true,
groupId: 'foo',
},
bar: {
name: 'bar',
defaultValue: 'test2',
used: true,
groupId: 'bar',
},
},
});
const wrapper = root.update().find(Panel);
const titles = wrapper
.find(TabsState)
.find('button')
.map((child) => child.prop('children'));
expect(titles).toEqual(['foo', 'bar']);
const knobs = wrapper.find(PropForm);
// but it should not have its own PropForm in this case
expect(knobs.length).toEqual(titles.length);
expect(knobs).toMatchSnapshot();
root.unmount();
});
it(`the ${DEFAULT_GROUP_ID} tab should have its own additional content when there are knobs both with and without a groupId`, () => {
const root = mount(
<ThemeProvider theme={convert(themes.light)}>
<Panel api={testApi} active />
</ThemeProvider>
);
testApi.on.mock.calls[0][1]({
knobs: {
bar: {
name: 'bar',
defaultValue: 'test2',
used: true,
// no groupId
},
foo: {
name: 'foo',
defaultValue: 'test',
used: true,
groupId: 'foo',
},
},
});
const wrapper = root.update().find(Panel);
const titles = wrapper
.find(TabsState)
.find('button')
.map((child) => child.prop('children'));
expect(titles).toEqual(['foo', DEFAULT_GROUP_ID]);
const knobs = wrapper.find(PropForm).map((propForm) => propForm.prop('knobs'));
// there are props with no groupId so Other should also have its own PropForm
expect(knobs.length).toEqual(titles.length);
expect(knobs).toMatchSnapshot();
root.unmount();
});
});
});

View File

@ -1,44 +0,0 @@
import React from 'react';
import { mount } from 'enzyme';
import RadioType from '../types/Radio';
describe('Radio', () => {
let knob;
beforeEach(() => {
knob = {
name: 'Color',
value: '#319C16',
options: {
Green: '#319C16',
Red: '#FF2B2B',
},
};
});
describe('displays value of button input', () => {
it('correctly renders labels', () => {
const wrapper = mount(<RadioType knob={knob} />);
const greenLabel = wrapper.find('label').first();
expect(greenLabel.text()).toEqual('Green');
});
it('sets value on the radio buttons', () => {
const wrapper = mount(<RadioType knob={knob} />);
const greenInput = wrapper.find('input').first();
expect(greenInput.prop('value')).toEqual('#319C16');
});
it('marks the correct checkbox as checked', () => {
const wrapper = mount(<RadioType knob={knob} />);
const greenInput = wrapper.find('input').first();
const redInput = wrapper.find('input').last();
expect(greenInput.prop('checked')).toEqual(true);
expect(redInput.prop('checked')).toEqual(false);
});
});
});

View File

@ -1,61 +0,0 @@
import React from 'react';
import { shallow } from 'enzyme';
import SelectType from '../types/Select';
describe('Select', () => {
let knob;
describe('Object values', () => {
beforeEach(() => {
knob = {
name: 'Colors',
value: '#00ff00',
options: {
Green: '#00ff00',
Red: '#ff0000',
},
};
});
it('correctly maps option keys and values', () => {
const wrapper = shallow(<SelectType knob={knob} />);
const green = wrapper.find('option').first();
expect(green.text()).toEqual('Green');
expect(green.prop('value')).toEqual('Green');
});
it('should set the default value for array-values correctly', () => {
knob = {
name: 'Array values',
options: {
'100 x 100': [100, 100],
'200 x 200': [200, 200],
},
value: [200, 200],
};
const wrapper = shallow(<SelectType knob={knob} />);
const value = wrapper.prop('value');
expect(value).toEqual('200 x 200');
});
});
describe('Array values', () => {
beforeEach(() => {
knob = {
name: 'Colors',
value: 'green',
options: ['green', 'red'],
};
});
it('correctly maps option keys and values', () => {
const wrapper = shallow(<SelectType knob={knob} />);
const green = wrapper.find('option').first();
expect(green.text()).toEqual('green');
expect(green.prop('value')).toEqual('green');
});
});
});

View File

@ -1,233 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Panel groups should have no tabs when there are no groupIds 1`] = `
Array [
Array [
Object {
"defaultValue": "test",
"name": "foo",
"used": true,
},
Object {
"defaultValue": "test2",
"name": "bar",
"used": true,
},
],
]
`;
exports[`Panel groups should have one tab per groupId when all are defined 1`] = `
Array [
.emotion-2 {
box-sizing: border-box;
width: 100%;
}
.emotion-1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
border-bottom: 1px solid rgba(0,0,0,.1);
margin: 0 15px;
padding: 8px 0;
}
.emotion-1:last-child {
margin-bottom: 3rem;
}
.emotion-0 {
min-width: 100px;
font-weight: 700;
margin-right: 15px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
line-height: 16px;
}
<PropForm
knobs={
Array [
Object {
"defaultValue": "test",
"groupId": "foo",
"name": "foo",
"used": true,
},
]
}
onFieldChange={[Function]}
onFieldClick={[Function]}
>
<Styled(form)>
<form
className="emotion-2"
>
<Field
key="foo"
label="foo"
>
<Styled(label)>
<label
className="emotion-1"
>
<Styled(span)>
<span
className="emotion-0"
>
<span>
foo
</span>
</span>
</Styled(span)>
<InvalidType
knob={
Object {
"defaultValue": "test",
"groupId": "foo",
"name": "foo",
"used": true,
}
}
onChange={[Function]}
onClick={[Function]}
>
<span>
Invalid Type
</span>
</InvalidType>
</label>
</Styled(label)>
</Field>
</form>
</Styled(form)>
</PropForm>,
.emotion-2 {
box-sizing: border-box;
width: 100%;
}
.emotion-1 {
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
border-bottom: 1px solid rgba(0,0,0,.1);
margin: 0 15px;
padding: 8px 0;
}
.emotion-1:last-child {
margin-bottom: 3rem;
}
.emotion-0 {
min-width: 100px;
font-weight: 700;
margin-right: 15px;
display: -webkit-box;
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
-webkit-box-pack: start;
-webkit-justify-content: flex-start;
-ms-flex-pack: start;
justify-content: flex-start;
-webkit-align-items: center;
-webkit-box-align: center;
-ms-flex-align: center;
align-items: center;
line-height: 16px;
}
<PropForm
knobs={
Array [
Object {
"defaultValue": "test2",
"groupId": "bar",
"name": "bar",
"used": true,
},
]
}
onFieldChange={[Function]}
onFieldClick={[Function]}
>
<Styled(form)>
<form
className="emotion-2"
>
<Field
key="bar"
label="bar"
>
<Styled(label)>
<label
className="emotion-1"
>
<Styled(span)>
<span
className="emotion-0"
>
<span>
bar
</span>
</span>
</Styled(span)>
<InvalidType
knob={
Object {
"defaultValue": "test2",
"groupId": "bar",
"name": "bar",
"used": true,
}
}
onChange={[Function]}
onClick={[Function]}
>
<span>
Invalid Type
</span>
</InvalidType>
</label>
</Styled(label)>
</Field>
</form>
</Styled(form)>
</PropForm>,
]
`;
exports[`Panel groups the Other tab should have its own additional content when there are knobs both with and without a groupId 1`] = `
Array [
Array [
Object {
"defaultValue": "test",
"groupId": "foo",
"name": "foo",
"used": true,
},
],
Array [
Object {
"defaultValue": "test2",
"name": "bar",
"used": true,
},
],
]
`;

View File

@ -1,54 +0,0 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { ThemeProvider, themes, convert } from '@storybook/theming';
import ArrayType from './Array';
describe('Array', () => {
it('should subscribe to setKnobs event of channel', () => {
const onChange = jest.fn();
render(
<ThemeProvider theme={convert(themes.light)}>
<ArrayType
onChange={onChange}
knob={{ name: 'passions', value: ['Fishing', 'Skiing'], separator: ',' }}
/>{' '}
</ThemeProvider>
);
const input = screen.getByRole('textbox');
expect(input).toHaveValue('Fishing,Skiing');
userEvent.type(input, ',');
expect(onChange).toHaveBeenLastCalledWith(['Fishing', 'Skiing', '']);
});
it('deserializes an Array to an Array', () => {
const array = ['a', 'b', 'c'];
const deserialized = ArrayType.deserialize(array);
expect(deserialized).toEqual(['a', 'b', 'c']);
});
it('deserializes an Object to an Array', () => {
const object = { 1: 'one', 0: 'zero', 2: 'two' };
const deserialized = ArrayType.deserialize(object);
expect(deserialized).toEqual(['zero', 'one', 'two']);
});
it('should change to an empty array when emptied', () => {
const onChange = jest.fn();
render(
<ThemeProvider theme={convert(themes.light)}>
<ArrayType
onChange={onChange}
knob={{ name: 'passions', value: ['Fishing', 'Skiing'], separator: ',' }}
/>{' '}
</ThemeProvider>
);
const input = screen.getByRole('textbox');
expect(input).toHaveValue('Fishing,Skiing');
userEvent.clear(input);
expect(onChange).toHaveBeenLastCalledWith([]);
});
});

View File

@ -1,77 +0,0 @@
import PropTypes from 'prop-types';
import React, { ChangeEvent, Component, Validator } from 'react';
import { Form } from '@storybook/components';
import { KnobControlConfig, KnobControlProps } from './types';
export type ArrayTypeKnobValue = string[] | readonly string[];
export interface ArrayTypeKnob extends KnobControlConfig<ArrayTypeKnobValue> {
separator: string;
}
interface ArrayTypeProps extends KnobControlProps<ArrayTypeKnobValue> {
knob: ArrayTypeKnob;
}
function formatArray(value: string, separator: string) {
if (value === '') {
return [];
}
return value.split(separator);
}
export default class ArrayType extends Component<ArrayTypeProps> {
static defaultProps: Partial<ArrayTypeProps> = {
knob: {} as any,
onChange: (value: ArrayTypeKnobValue) => value,
};
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.array,
separator: PropTypes.string,
}) as Validator<ArrayTypeProps['knob']>,
onChange: PropTypes.func as Validator<ArrayTypeProps['onChange']>,
};
static serialize = (value: ArrayTypeKnobValue) => value;
static deserialize = (value: string[] | Record<string, string>) => {
if (Array.isArray(value)) return value;
return Object.keys(value)
.sort()
.reduce((array, key) => [...array, value[key]], [] as string[]);
};
shouldComponentUpdate(nextProps: Readonly<ArrayTypeProps>) {
const { knob } = this.props;
return nextProps.knob.value !== knob.value;
}
handleChange = (e: ChangeEvent<HTMLTextAreaElement>): void => {
const { knob, onChange } = this.props;
const { value } = e.target as HTMLTextAreaElement;
const newVal = formatArray(value, knob.separator);
onChange(newVal);
};
render() {
const { knob } = this.props;
const value = knob.value && knob.value.join(knob.separator);
return (
<Form.Textarea
id={knob.name}
name={knob.name}
value={value}
onChange={this.handleChange}
size="flex"
/>
);
}
}

View File

@ -1,58 +0,0 @@
import PropTypes from 'prop-types';
import React, { FunctionComponent, Validator } from 'react';
import { styled } from '@storybook/theming';
import { KnobControlConfig, KnobControlProps } from './types';
type BooleanTypeKnobValue = boolean;
export type BooleanTypeKnob = KnobControlConfig<BooleanTypeKnobValue>;
export interface BooleanTypeProps extends KnobControlProps<BooleanTypeKnobValue> {
knob: BooleanTypeKnob;
}
const Input = styled.input({
display: 'table-cell',
boxSizing: 'border-box',
verticalAlign: 'top',
height: 21,
outline: 'none',
border: '1px solid #ececec',
fontSize: '12px',
color: '#555',
});
const serialize = (value: BooleanTypeKnobValue): string | null => (value ? String(value) : null);
const deserialize = (value: string | null) => value === 'true';
const BooleanType: FunctionComponent<BooleanTypeProps> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = ({ knob, onChange }) => (
<Input
id={knob.name}
name={knob.name}
type="checkbox"
onChange={(e) => onChange(e.target.checked)}
checked={knob.value || false}
/>
);
BooleanType.defaultProps = {
knob: {} as any,
onChange: (value) => value,
};
BooleanType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.bool,
}) as Validator<BooleanTypeProps['knob']>,
onChange: PropTypes.func as Validator<BooleanTypeProps['onChange']>,
};
BooleanType.serialize = serialize;
BooleanType.deserialize = deserialize;
export default BooleanType;

View File

@ -1,43 +0,0 @@
import PropTypes from 'prop-types';
import React, { FunctionComponent, Validator } from 'react';
import { Form } from '@storybook/components';
import { KnobControlConfig, KnobControlProps } from './types';
export type ButtonTypeKnob = KnobControlConfig<never>;
export interface ButtonTypeProps extends KnobControlProps<never> {
knob: ButtonTypeKnob;
onClick: ButtonTypeOnClickProp;
}
export type ButtonTypeOnClickProp = (knob: ButtonTypeKnob) => any;
const serialize = (): undefined => undefined;
const deserialize = (): undefined => undefined;
const ButtonType: FunctionComponent<ButtonTypeProps> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = ({ knob, onClick }) => (
<Form.Button type="button" name={knob.name} onClick={() => onClick(knob)}>
{knob.name}
</Form.Button>
);
ButtonType.defaultProps = {
knob: {} as any,
onClick: () => {},
};
ButtonType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
}).isRequired as Validator<ButtonTypeProps['knob']>,
onClick: PropTypes.func.isRequired as Validator<ButtonTypeProps['onClick']>,
};
ButtonType.serialize = serialize;
ButtonType.deserialize = deserialize;
export default ButtonType;

View File

@ -1,130 +0,0 @@
import React, { Component, ChangeEvent, Validator } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { KnobControlConfig, KnobControlProps } from './types';
type CheckboxesTypeKnobValue = string[];
export interface CheckboxesTypeKnob extends KnobControlConfig<CheckboxesTypeKnobValue> {
options: Record<string, string>;
}
interface CheckboxesTypeProps
extends KnobControlProps<CheckboxesTypeKnobValue>,
CheckboxesWrapperProps {
knob: CheckboxesTypeKnob;
}
interface CheckboxesTypeState {
values: CheckboxesTypeKnobValue;
}
interface CheckboxesWrapperProps {
isInline: boolean;
}
const CheckboxesWrapper = styled.div<CheckboxesWrapperProps>(({ isInline }) =>
isInline
? {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
'> * + *': {
marginLeft: 10,
},
}
: {}
);
const CheckboxFieldset = styled.fieldset({
border: 0,
padding: 0,
margin: 0,
});
const CheckboxLabel = styled.label({
padding: '3px 0 3px 5px',
lineHeight: '18px',
display: 'inline-block',
});
export default class CheckboxesType extends Component<CheckboxesTypeProps, CheckboxesTypeState> {
static defaultProps: CheckboxesTypeProps = {
knob: {} as any,
onChange: (value) => value,
isInline: false,
};
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.array,
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}) as Validator<CheckboxesTypeProps['knob']>,
onChange: PropTypes.func as Validator<CheckboxesTypeProps['onChange']>,
isInline: PropTypes.bool as Validator<CheckboxesTypeProps['isInline']>,
};
static serialize = (value: CheckboxesTypeKnobValue) => value;
static deserialize = (value: CheckboxesTypeKnobValue) => value;
constructor(props: CheckboxesTypeProps) {
super(props);
const { knob } = props;
this.state = {
values: knob.defaultValue || [],
};
}
private handleChange = (e: ChangeEvent<HTMLInputElement>) => {
const { onChange } = this.props;
const currentValue = (e.target as HTMLInputElement).value;
const { values } = this.state;
if (values.includes(currentValue)) {
values.splice(values.indexOf(currentValue), 1);
} else {
values.push(currentValue);
}
this.setState({ values });
onChange(values);
};
private renderCheckboxList = ({ options }: CheckboxesTypeKnob) =>
Object.keys(options).map((key) => this.renderCheckbox(key, options[key]));
private renderCheckbox = (label: string, value: string) => {
const { knob } = this.props;
const { name } = knob;
const id = `${name}-${value}`;
const { values } = this.state;
return (
<div key={id}>
<input
type="checkbox"
id={id}
name={name}
value={value}
onChange={this.handleChange}
checked={values.includes(value)}
/>
<CheckboxLabel htmlFor={id}>{label}</CheckboxLabel>
</div>
);
};
render() {
const { knob, isInline } = this.props;
return (
<CheckboxFieldset>
<CheckboxesWrapper isInline={isInline}>{this.renderCheckboxList(knob)}</CheckboxesWrapper>
</CheckboxFieldset>
);
}
}

View File

@ -1,142 +0,0 @@
import { document } from 'global';
import PropTypes from 'prop-types';
import React, { Component, Validator } from 'react';
import { RgbaStringColorPicker } from 'react-colorful';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
import { KnobControlConfig, KnobControlProps } from './types';
type ColorTypeKnobValue = string;
export type ColorTypeKnob = KnobControlConfig<ColorTypeKnobValue>;
type ColorTypeProps = KnobControlProps<ColorTypeKnobValue>;
interface ColorTypeState {
displayColorPicker: boolean;
}
interface ColorButtonProps {
name: string;
type: string;
size: string;
active: boolean;
onClick: () => any;
}
const { Button } = Form;
const Swatch = styled.div<{}>(({ theme }) => ({
position: 'absolute',
top: '50%',
transform: 'translateY(-50%)',
left: 6,
width: 16,
height: 16,
boxShadow: `${theme.appBorderColor} 0 0 0 1px inset`,
borderRadius: '1rem',
}));
const ColorButton = styled(Button)<ColorButtonProps>(({ active }) => ({
zIndex: active ? 3 : 'unset',
}));
const Popover = styled.div({
position: 'absolute',
zIndex: 2,
});
export default class ColorType extends Component<ColorTypeProps, ColorTypeState> {
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
}) as Validator<ColorTypeProps['knob']>,
onChange: PropTypes.func as Validator<ColorTypeProps['onChange']>,
};
static defaultProps: ColorTypeProps = {
knob: {} as any,
onChange: (value) => value,
};
static serialize = (value: ColorTypeKnobValue) => value;
static deserialize = (value: ColorTypeKnobValue) => value;
state: ColorTypeState = {
displayColorPicker: false,
};
componentDidMount() {
document.addEventListener('mousedown', this.handleWindowMouseDown);
}
shouldComponentUpdate(nextProps: ColorTypeProps, nextState: ColorTypeState) {
const { knob } = this.props;
const { displayColorPicker } = this.state;
return (
nextProps.knob.value !== knob.value || nextState.displayColorPicker !== displayColorPicker
);
}
componentWillUnmount() {
document.removeEventListener('mousedown', this.handleWindowMouseDown);
}
handleWindowMouseDown = (e: MouseEvent) => {
const { displayColorPicker } = this.state;
if (!displayColorPicker || this.popover.contains(e.target as HTMLElement)) {
return;
}
this.setState({
displayColorPicker: false,
});
};
private handleClick = () => {
const { displayColorPicker } = this.state;
this.setState({
displayColorPicker: !displayColorPicker,
});
};
private handleChange = (color: string) => {
const { onChange } = this.props;
onChange(color);
};
popover!: HTMLDivElement;
render() {
const { knob } = this.props;
const { displayColorPicker } = this.state;
const colorStyle = {
background: knob.value,
};
return (
<ColorButton
active={displayColorPicker}
type="button"
name={knob.name}
onClick={this.handleClick}
size="flex"
>
{knob.value && knob.value.toUpperCase()}
<Swatch style={colorStyle} />
{displayColorPicker ? (
<Popover
ref={(e) => {
if (e) this.popover = e;
}}
>
<RgbaStringColorPicker color={knob.value} onChange={this.handleChange} />
</Popover>
) : null}
</ColorButton>
);
}
}

View File

@ -1,154 +0,0 @@
import React, { Component, ChangeEvent, Validator } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
import { KnobControlConfig, KnobControlProps } from './types';
type DateTypeKnobValue = number;
export type DateTypeKnob = KnobControlConfig<DateTypeKnobValue>;
type DateTypeProps = KnobControlProps<DateTypeKnobValue>;
interface DateTypeState {
valid: boolean | undefined;
}
const FlexSpaced = styled.div({
flex: 1,
display: 'flex',
'&& > *': {
marginLeft: 10,
},
'&& > *:first-of-type': {
marginLeft: 0,
},
});
const FlexInput = styled(Form.Input)({ flex: 1 });
const formatDate = (date: Date) => {
const year = `000${date.getFullYear()}`.slice(-4);
const month = `0${date.getMonth() + 1}`.slice(-2);
const day = `0${date.getDate()}`.slice(-2);
return `${year}-${month}-${day}`;
};
const formatTime = (date: Date) => {
const hours = `0${date.getHours()}`.slice(-2);
const minutes = `0${date.getMinutes()}`.slice(-2);
return `${hours}:${minutes}`;
};
export default class DateType extends Component<DateTypeProps, DateTypeState> {
static defaultProps: DateTypeProps = {
knob: {} as any,
onChange: (value) => value,
};
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.number,
}) as Validator<DateTypeProps['knob']>,
onChange: PropTypes.func as Validator<DateTypeProps['onChange']>,
};
static serialize = (value: DateTypeKnobValue) =>
new Date(value).getTime() || new Date().getTime();
static deserialize = (value: DateTypeKnobValue) =>
new Date(value).getTime() || new Date().getTime();
static getDerivedStateFromProps() {
return { valid: true };
}
state: DateTypeState = {
valid: undefined,
};
componentDidUpdate() {
const { knob } = this.props;
const { valid } = this.state;
const value = new Date(knob.value);
if (valid !== false) {
this.dateInput.value = formatDate(value);
this.timeInput.value = formatTime(value);
}
}
private onDateChange = (e: ChangeEvent<HTMLInputElement>) => {
const { knob, onChange } = this.props;
const { state } = this;
let valid = false;
const [year, month, day] = e.target.value.split('-');
const result = new Date(knob.value);
if (result.getTime()) {
result.setFullYear(parseInt(year, 10), parseInt(month, 10) - 1, parseInt(day, 10));
if (result.getTime()) {
valid = true;
onChange(result.getTime());
}
}
if (valid !== state.valid) {
this.setState({ valid });
}
};
private onTimeChange = (e: ChangeEvent<HTMLInputElement>) => {
const { knob, onChange } = this.props;
const { state } = this;
let valid = false;
const [hours, minutes] = e.target.value.split(':');
const result = new Date(knob.value);
if (result.getTime()) {
result.setHours(parseInt(hours, 10));
result.setMinutes(parseInt(minutes, 10));
if (result.getTime()) {
onChange(result.getTime());
valid = true;
}
}
if (valid !== state.valid) {
this.setState({ valid });
}
};
dateInput!: HTMLInputElement;
timeInput!: HTMLInputElement;
render() {
const { knob } = this.props;
const { name } = knob;
const { valid } = this.state;
return name ? (
<FlexSpaced style={{ display: 'flex' }}>
<FlexInput
type="date"
max="9999-12-31" // I do this because of a rendering bug in chrome
ref={(el: HTMLInputElement) => {
this.dateInput = el;
}}
id={`${name}date`}
name={`${name}date`}
onChange={this.onDateChange}
/>
<FlexInput
type="time"
id={`${name}time`}
name={`${name}time`}
ref={(el: HTMLInputElement) => {
this.timeInput = el;
}}
onChange={this.onTimeChange}
/>
{!valid ? <div>invalid</div> : null}
</FlexSpaced>
) : null;
}
}

View File

@ -1,67 +0,0 @@
import { FileReader } from 'global';
import PropTypes, { Validator } from 'prop-types';
import React, { ChangeEvent, FunctionComponent } from 'react';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
import { KnobControlConfig, KnobControlProps } from './types';
type DateTypeKnobValue = string[];
export interface FileTypeKnob extends KnobControlConfig<DateTypeKnobValue> {
accept: string;
}
export interface FilesTypeProps extends KnobControlProps<DateTypeKnobValue> {
knob: FileTypeKnob;
}
const FileInput = styled(Form.Input)({
paddingTop: 4,
});
function fileReaderPromise(file: File) {
return new Promise<string>((resolve) => {
const fileReader = new FileReader();
fileReader.onload = (e: Event) => resolve((e.currentTarget as FileReader).result as string);
fileReader.readAsDataURL(file);
});
}
const serialize = (): undefined => undefined;
const deserialize = (): undefined => undefined;
const FilesType: FunctionComponent<FilesTypeProps> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = ({ knob, onChange }) => (
<FileInput
type="file"
name={knob.name}
multiple
onChange={(e: ChangeEvent<HTMLInputElement>) => {
if (e.target.files) {
Promise.all(Array.from(e.target.files).map(fileReaderPromise)).then(onChange);
}
}}
accept={knob.accept}
size="flex"
/>
);
FilesType.defaultProps = {
knob: {} as any,
onChange: (value) => value,
};
FilesType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
}) as Validator<FilesTypeProps['knob']>,
onChange: PropTypes.func as Validator<FilesTypeProps['onChange']>,
};
FilesType.serialize = serialize;
FilesType.deserialize = deserialize;
export default FilesType;

View File

@ -1,126 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, ChangeEvent, Validator } from 'react';
import { styled } from '@storybook/theming';
import { Form } from '@storybook/components';
import { KnobControlConfig, KnobControlProps } from './types';
type NumberTypeKnobValue = number;
export interface NumberTypeKnobOptions {
range?: boolean;
min?: number;
max?: number;
step?: number;
}
export type NumberTypeKnob = KnobControlConfig<NumberTypeKnobValue> &
NumberTypeKnobOptions & { value?: NumberTypeKnobValue };
interface NumberTypeProps extends KnobControlProps<NumberTypeKnobValue | null> {
knob: NumberTypeKnob;
}
const RangeInput = styled.input(
{
boxSizing: 'border-box',
height: 25,
outline: 'none',
border: '1px solid #f7f4f4',
borderRadius: 2,
fontSize: 11,
padding: 5,
color: '#444',
},
{
display: 'table-cell',
flexGrow: 1,
}
);
const RangeLabel = styled.span({
paddingLeft: 5,
paddingRight: 5,
fontSize: 12,
whiteSpace: 'nowrap',
});
const RangeWrapper = styled.div({
display: 'flex',
alignItems: 'center',
width: '100%',
});
export default class NumberType extends Component<NumberTypeProps> {
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.string, PropTypes.number]),
range: PropTypes.bool,
min: PropTypes.number,
max: PropTypes.number,
step: PropTypes.number,
}).isRequired as Validator<NumberTypeProps['knob']>,
onChange: PropTypes.func.isRequired as Validator<NumberTypeProps['onChange']>,
};
static defaultProps: NumberTypeProps = {
knob: {} as any,
onChange: (value) => value,
};
static serialize = (value: NumberTypeKnobValue | null | undefined) =>
value === null || value === undefined ? '' : String(value);
static deserialize = (value: string) => (value === '' ? null : parseFloat(value));
shouldComponentUpdate(nextProps: NumberTypeProps) {
const { knob } = this.props;
return nextProps.knob.value !== knob.value;
}
private handleChange = (event: ChangeEvent<HTMLInputElement>) => {
const { onChange } = this.props;
const { value } = event.target;
let parsedValue: number | null = Number(value);
if (Number.isNaN(parsedValue) || value === '') {
parsedValue = null;
}
onChange(parsedValue);
};
render() {
const { knob } = this.props;
return knob.range ? (
<RangeWrapper>
<RangeLabel>{knob.min}</RangeLabel>
<RangeInput
value={knob.value}
type="range"
name={knob.name}
min={knob.min}
max={knob.max}
step={knob.step}
onChange={this.handleChange}
/>
<RangeLabel>{`${knob.value} / ${knob.max}`}</RangeLabel>
</RangeWrapper>
) : (
<Form.Input
value={knob.value}
type="number"
name={knob.name}
min={knob.min}
max={knob.max}
step={knob.step}
onChange={this.handleChange}
size="flex"
/>
);
}
}

View File

@ -1,100 +0,0 @@
import React, { Component, ChangeEvent, Validator } from 'react';
import PropTypes from 'prop-types';
import deepEqual from 'fast-deep-equal';
import { polyfill } from 'react-lifecycles-compat';
import { Form } from '@storybook/components';
import { KnobControlConfig, KnobControlProps } from './types';
export type ObjectTypeKnob<T> = KnobControlConfig<T>;
type ObjectTypeProps<T> = KnobControlProps<T>;
interface ObjectTypeState<T> {
value: string;
failed: boolean;
json?: T;
}
class ObjectType<T> extends Component<ObjectTypeProps<T>> {
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
}).isRequired as Validator<ObjectTypeProps<any>['knob']>,
onChange: PropTypes.func.isRequired as Validator<ObjectTypeProps<any>['onChange']>,
};
static defaultProps: ObjectTypeProps<any> = {
knob: {} as any,
onChange: (value) => value,
};
static serialize: { <T>(object: T): string } = (object) => JSON.stringify(object);
static deserialize: { <T>(value: string): T } = (value) => (value ? JSON.parse(value) : {});
static getDerivedStateFromProps<T>(
props: ObjectTypeProps<T>,
state: ObjectTypeState<T>
): ObjectTypeState<T> | null {
if (!deepEqual(props.knob.value, state.json)) {
try {
return {
value: JSON.stringify(props.knob.value, null, 2),
failed: false,
json: props.knob.value,
};
} catch (e) {
return { value: 'Object cannot be stringified', failed: true };
}
}
return null;
}
state: ObjectTypeState<T> = {
value: '',
failed: false,
json: {} as any,
};
handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
const { value } = e.target;
const { json: stateJson } = this.state;
const { knob, onChange } = this.props;
try {
const json = JSON.parse(value.trim());
this.setState({
value,
json,
failed: false,
});
if (deepEqual(knob.value, stateJson)) {
onChange(json);
}
} catch (err) {
this.setState({
value,
failed: true,
});
}
};
render() {
const { value, failed } = this.state;
const { knob } = this.props;
return (
<Form.Textarea
name={knob.name}
valid={failed ? 'error' : undefined}
value={value}
onChange={this.handleChange}
size="flex"
/>
);
}
}
polyfill(ObjectType as any);
export default ObjectType;

View File

@ -1,142 +0,0 @@
import React, { FunctionComponent, Validator } from 'react';
import PropTypes from 'prop-types';
import ReactSelect from 'react-select';
import { styled } from '@storybook/theming';
import { KnobControlConfig, KnobControlProps } from './types';
import RadiosType from './Radio';
import CheckboxesType from './Checkboxes';
// TODO: Apply the Storybook theme to react-select
export type OptionsTypeKnobSingleValue =
| string
| number
| null
| undefined
| string[]
| number[]
| (string | number)[];
export type OptionsTypeKnobValue<
T extends OptionsTypeKnobSingleValue = OptionsTypeKnobSingleValue
> = T | NonNullable<T>[] | readonly NonNullable<T>[];
export type OptionsKnobOptionsDisplay =
| 'radio'
| 'inline-radio'
| 'check'
| 'inline-check'
| 'select'
| 'multi-select';
export interface OptionsKnobOptions {
display: OptionsKnobOptionsDisplay;
}
export interface OptionsTypeKnob<T extends OptionsTypeKnobValue> extends KnobControlConfig<T> {
options: OptionsTypeOptionsProp<T>;
optionsObj: OptionsKnobOptions;
}
export interface OptionsTypeOptionsProp<T> {
[key: string]: T;
}
export interface OptionsTypeProps<T extends OptionsTypeKnobValue> extends KnobControlProps<T> {
knob: OptionsTypeKnob<T>;
display: OptionsKnobOptionsDisplay;
}
const OptionsSelect = styled(ReactSelect)({
width: '100%',
maxWidth: '300px',
color: 'black',
});
type ReactSelectOnChangeFn =
| { (v: OptionsSelectValueItem): void }
| { (v: OptionsSelectValueItem[]): void };
interface OptionsSelectValueItem {
value: any;
label: string;
}
const serialize: { <T>(value: T): T } = (value) => value;
const deserialize: { <T>(value: T): T } = (value) => value;
const OptionsType: FunctionComponent<OptionsTypeProps<any>> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = (props) => {
const { knob, onChange } = props;
const { display } = knob.optionsObj;
if (display === 'check' || display === 'inline-check') {
const isInline = display === 'inline-check';
return <CheckboxesType {...props} isInline={isInline} />;
}
if (display === 'radio' || display === 'inline-radio') {
const isInline = display === 'inline-radio';
return <RadiosType {...props} isInline={isInline} />;
}
if (display === 'select' || display === 'multi-select') {
const options: OptionsSelectValueItem[] = Object.keys(knob.options).map((key) => ({
value: knob.options[key],
label: key,
}));
const isMulti = display === 'multi-select';
const optionsIndex = options.findIndex((i) => i.value === knob.value);
let defaultValue: typeof options | typeof options[0] = options[optionsIndex];
let handleChange: ReactSelectOnChangeFn = (e: OptionsSelectValueItem) => onChange(e.value);
if (isMulti) {
defaultValue = options.filter((i) => knob.value.includes(i.value));
handleChange = (values: OptionsSelectValueItem[]) =>
onChange(values.map((item) => item.value));
}
return (
<OptionsSelect
value={defaultValue}
options={options}
isMulti={isMulti}
onChange={handleChange}
/>
);
}
return null;
};
OptionsType.defaultProps = {
knob: {} as any,
display: 'select',
onChange: (value) => value,
};
OptionsType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.oneOfType([PropTypes.array, PropTypes.string]),
options: PropTypes.object,
}) as Validator<OptionsTypeProps<any>['knob']>,
display: PropTypes.oneOf<OptionsKnobOptionsDisplay>([
'radio',
'inline-radio',
'check',
'inline-check',
'select',
'multi-select',
]) as Validator<OptionsTypeProps<any>['display']>,
onChange: PropTypes.func as Validator<OptionsTypeProps<any>['onChange']>,
};
OptionsType.serialize = serialize;
OptionsType.deserialize = deserialize;
export default OptionsType;

View File

@ -1,97 +0,0 @@
import React, { Component, Validator } from 'react';
import PropTypes from 'prop-types';
import { styled } from '@storybook/theming';
import { KnobControlConfig, KnobControlProps } from './types';
export type RadiosTypeKnobValue = string | number | null | undefined;
export type RadiosTypeOptionsProp<T extends RadiosTypeKnobValue> = Record<string | number, T>;
export interface RadiosTypeKnob extends KnobControlConfig<RadiosTypeKnobValue> {
options: RadiosTypeOptionsProp<RadiosTypeKnobValue>;
}
interface RadiosTypeProps extends KnobControlProps<RadiosTypeKnobValue>, RadiosWrapperProps {
knob: RadiosTypeKnob;
}
interface RadiosWrapperProps {
isInline: boolean;
}
const RadiosWrapper = styled.div<RadiosWrapperProps>(({ isInline }) =>
isInline
? {
display: 'flex',
flexWrap: 'wrap',
alignItems: 'center',
'> * + *': {
marginLeft: 10,
},
}
: {}
);
const RadioLabel = styled.label({
padding: '3px 0 3px 5px',
lineHeight: '18px',
display: 'inline-block',
});
class RadiosType extends Component<RadiosTypeProps> {
static defaultProps: RadiosTypeProps = {
knob: {} as any,
onChange: (value) => value,
isInline: false,
};
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}) as Validator<RadiosTypeProps['knob']>,
onChange: PropTypes.func as Validator<RadiosTypeProps['onChange']>,
isInline: PropTypes.bool as Validator<RadiosTypeProps['isInline']>,
};
static serialize = (value: RadiosTypeKnobValue) => value;
static deserialize = (value: RadiosTypeKnobValue) => value;
private renderRadioButtonList({ options }: RadiosTypeKnob) {
if (Array.isArray(options)) {
return options.map((val) => this.renderRadioButton(val, val));
}
return Object.keys(options).map((key) => this.renderRadioButton(key, options[key]));
}
private renderRadioButton(label: string, value: RadiosTypeKnobValue) {
const opts = { label, value };
const { onChange, knob } = this.props;
const { name } = knob;
const id = `${name}-${opts.value}`;
return (
<div key={id}>
<input
type="radio"
id={id}
name={name}
value={opts.value || undefined}
onChange={(e) => onChange(e.target.value)}
checked={value === knob.value}
/>
<RadioLabel htmlFor={id}>{label}</RadioLabel>
</div>
);
}
render() {
const { knob, isInline } = this.props;
return <RadiosWrapper isInline={isInline}>{this.renderRadioButtonList(knob)}</RadiosWrapper>;
}
}
export default RadiosType;

View File

@ -1,87 +0,0 @@
import React, { FunctionComponent, ChangeEvent, Validator } from 'react';
import PropTypes from 'prop-types';
import { Form } from '@storybook/components';
import { KnobControlConfig, KnobControlProps } from './types';
export type SelectTypeKnobValue = string | number | boolean | null | undefined | PropertyKey[];
export type SelectTypeOptionsProp<T extends SelectTypeKnobValue = SelectTypeKnobValue> =
| Record<PropertyKey, T>
| Record<Extract<T, PropertyKey>, T[keyof T]>
| T[]
| readonly T[];
export interface SelectTypeKnob<T extends SelectTypeKnobValue = SelectTypeKnobValue>
extends KnobControlConfig<T> {
options: SelectTypeOptionsProp<T>;
}
export interface SelectTypeProps<T extends SelectTypeKnobValue = SelectTypeKnobValue>
extends KnobControlProps<T> {
knob: SelectTypeKnob<T>;
}
const serialize = (value: SelectTypeKnobValue) => value;
const deserialize = (value: SelectTypeKnobValue) => value;
const SelectType: FunctionComponent<SelectTypeProps> & {
serialize: typeof serialize;
deserialize: typeof deserialize;
} = ({ knob, onChange }) => {
const { options } = knob;
const callbackReduceArrayOptions = (acc: any, option: any, i: number) => {
if (typeof option !== 'object' || option === null) return { ...acc, [option]: option };
const label = option.label || option.key || i;
return { ...acc, [label]: option };
};
const entries = Array.isArray(options) ? options.reduce(callbackReduceArrayOptions, {}) : options;
const selectedKey = Object.keys(entries).find((key) => {
const { value: knobVal } = knob;
const entryVal = entries[key];
if (Array.isArray(knobVal)) {
return JSON.stringify(entryVal) === JSON.stringify(knobVal);
}
return entryVal === knobVal;
});
return (
<Form.Select
value={selectedKey}
name={knob.name}
onChange={(e: ChangeEvent<HTMLSelectElement>) => {
onChange(entries[e.target.value]);
}}
size="flex"
>
{Object.entries(entries).map(([key]) => (
<option key={key} value={key}>
{key}
</option>
))}
</Form.Select>
);
};
SelectType.defaultProps = {
knob: {} as any,
onChange: (value) => value,
};
SelectType.propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.any,
options: PropTypes.oneOfType([PropTypes.array, PropTypes.object]),
}) as Validator<SelectTypeProps['knob']>,
onChange: PropTypes.func as Validator<SelectTypeProps['onChange']>,
};
SelectType.serialize = serialize;
SelectType.deserialize = deserialize;
export default SelectType;

View File

@ -1,55 +0,0 @@
import PropTypes from 'prop-types';
import React, { Component, ChangeEvent, Validator } from 'react';
import { Form } from '@storybook/components';
import { KnobControlConfig, KnobControlProps } from './types';
type TextTypeKnobValue = string;
export type TextTypeKnob = KnobControlConfig<TextTypeKnobValue> & { value?: TextTypeKnobValue };
type TextTypeProps = KnobControlProps<TextTypeKnobValue>;
export default class TextType extends Component<TextTypeProps> {
static defaultProps: TextTypeProps = {
knob: {} as any,
onChange: (value) => value,
};
static propTypes = {
knob: PropTypes.shape({
name: PropTypes.string,
value: PropTypes.string,
}) as Validator<TextTypeProps['knob']>,
onChange: PropTypes.func as Validator<TextTypeProps['onChange']>,
};
static serialize = (value: TextTypeKnobValue) => value;
static deserialize = (value: TextTypeKnobValue) => value;
shouldComponentUpdate(nextProps: TextTypeProps) {
const { knob } = this.props;
return nextProps.knob.value !== knob.value;
}
private handleChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
const { onChange } = this.props;
const { value } = event.target;
onChange(value);
};
render() {
const { knob } = this.props;
return (
<Form.Textarea
id={knob.name}
name={knob.name}
value={knob.value}
onChange={this.handleChange}
size="flex"
/>
);
}
}

View File

@ -1,59 +0,0 @@
import { ComponentType } from 'react';
import TextType from './Text';
import NumberType from './Number';
import ColorType from './Color';
import BooleanType from './Boolean';
import ObjectType from './Object';
import SelectType from './Select';
import RadiosType from './Radio';
import ArrayType from './Array';
import DateType from './Date';
import ButtonType from './Button';
import FilesType from './Files';
import OptionsType from './Options';
const KnobControls = {
text: TextType,
number: NumberType,
color: ColorType,
boolean: BooleanType,
object: ObjectType,
select: SelectType,
radios: RadiosType,
array: ArrayType,
date: DateType,
button: ButtonType,
files: FilesType,
options: OptionsType,
};
export default KnobControls;
export type KnobType = keyof typeof KnobControls;
export type KnobControlType = ComponentType<any> & {
serialize: (v: any) => any;
deserialize: (v: any) => any;
};
// Note: this is a utility function that helps in resolving types more orderly
export const getKnobControl = (type: KnobType) => KnobControls[type] as KnobControlType;
export type { TextTypeKnob } from './Text';
export type { NumberTypeKnob, NumberTypeKnobOptions } from './Number';
export type { ColorTypeKnob } from './Color';
export type { BooleanTypeKnob } from './Boolean';
export type { ObjectTypeKnob } from './Object';
export type { SelectTypeKnob, SelectTypeOptionsProp, SelectTypeKnobValue } from './Select';
export type { RadiosTypeKnob, RadiosTypeOptionsProp, RadiosTypeKnobValue } from './Radio';
export type { ArrayTypeKnob, ArrayTypeKnobValue } from './Array';
export type { DateTypeKnob } from './Date';
export type { ButtonTypeKnob, ButtonTypeOnClickProp } from './Button';
export type { FileTypeKnob } from './Files';
export type {
OptionsTypeKnob,
OptionsKnobOptions,
OptionsTypeOptionsProp,
OptionsTypeKnobSingleValue,
OptionsTypeKnobValue,
} from './Options';

View File

@ -1,10 +0,0 @@
export interface KnobControlConfig<T = never> {
name: string;
value: T;
defaultValue?: T;
}
export interface KnobControlProps<T> {
knob: KnobControlConfig<T>;
onChange: (value: T) => T;
}

View File

@ -1,52 +0,0 @@
const unconvertable = (): undefined => undefined;
export const converters = {
jsonParse: (value: any): any => JSON.parse(value),
jsonStringify: (value: any): string => JSON.stringify(value),
simple: (value: any): any => value,
stringifyIfSet: (value: any): string =>
value === null || value === undefined ? '' : String(value),
stringifyIfTruthy: (value: any): string | null => (value ? String(value) : null),
toArray: (value: any): any[] => {
if (Array.isArray(value)) {
return value;
}
return value.split(',');
},
toBoolean: (value: any): boolean => value === 'true',
toDate: (value: any): number => new Date(value).getTime() || new Date().getTime(),
toFloat: (value: any): number | null => (value === '' ? null : parseFloat(value)),
};
export const serializers = {
array: converters.simple,
boolean: converters.stringifyIfTruthy,
button: unconvertable,
checkbox: converters.simple,
color: converters.simple,
date: converters.toDate,
files: unconvertable,
number: converters.stringifyIfSet,
object: converters.jsonStringify,
options: converters.simple,
radios: converters.simple,
select: converters.simple,
text: converters.simple,
};
export const deserializers = {
array: converters.toArray,
boolean: converters.toBoolean,
button: unconvertable,
checkbox: converters.simple,
color: converters.simple,
date: converters.toDate,
files: unconvertable,
number: converters.toFloat,
object: converters.jsonParse,
options: converters.simple,
radios: converters.simple,
select: converters.simple,
text: converters.simple,
};

View File

@ -1,149 +0,0 @@
import addons, { makeDecorator } from '@storybook/addons';
import { SET_OPTIONS } from './shared';
import { manager, registerKnobs } from './registerKnobs';
import { Knob, KnobType, Mutable } from './type-defs';
import {
NumberTypeKnobOptions,
ButtonTypeOnClickProp,
RadiosTypeOptionsProp,
SelectTypeOptionsProp,
SelectTypeKnobValue,
OptionsTypeKnobValue,
OptionsTypeOptionsProp,
OptionsTypeKnobSingleValue,
OptionsKnobOptions,
RadiosTypeKnobValue,
ArrayTypeKnobValue,
} from './components/types';
export function knob<T extends KnobType, V = Mutable<Knob<T>['value']>>(
name: string,
options: Knob<T>
): V {
return manager.knob(name, options) as V;
}
export function text(name: string, value: string, groupId?: string) {
return manager.knob(name, { type: 'text', value, groupId });
}
export function boolean(name: string, value: boolean, groupId?: string) {
return manager.knob(name, { type: 'boolean', value, groupId });
}
export function number(
name: string,
value: number,
options: NumberTypeKnobOptions = {},
groupId?: string
) {
const rangeDefaults = {
min: 0,
max: 10,
step: 1,
};
const mergedOptions = options.range
? {
...rangeDefaults,
...options,
}
: options;
const finalOptions = {
type: 'number' as 'number',
...mergedOptions,
value,
groupId,
};
return manager.knob(name, finalOptions);
}
export function color(name: string, value: string, groupId?: string) {
return manager.knob(name, { type: 'color', value, groupId });
}
export function object<T>(name: string, value: T, groupId?: string): T {
return manager.knob(name, { type: 'object', value, groupId });
}
export function select<T extends SelectTypeKnobValue>(
name: string,
options: SelectTypeOptionsProp<T>,
value: T,
groupId?: string
): T {
return manager.knob(name, {
type: 'select',
selectV2: true,
options: options as SelectTypeOptionsProp,
value,
groupId,
}) as T;
}
export function radios<T extends RadiosTypeKnobValue>(
name: string,
options: RadiosTypeOptionsProp<T>,
value: T,
groupId?: string
): T {
return manager.knob(name, { type: 'radios', options, value, groupId }) as T;
}
export function array(name: string, value: ArrayTypeKnobValue, separator = ',', groupId?: string) {
return manager.knob(name, { type: 'array', value, separator, groupId });
}
export function date(name: string, value = new Date(), groupId?: string) {
const proxyValue = value ? value.getTime() : new Date().getTime();
return manager.knob(name, { type: 'date', value: proxyValue, groupId });
}
export function button(name: string, callback: ButtonTypeOnClickProp, groupId?: string) {
return manager.knob(name, { type: 'button', callback, hideLabel: true, groupId });
}
export function files(name: string, accept: string, value: string[] = [], groupId?: string) {
return manager.knob(name, { type: 'files', accept, value, groupId });
}
export function optionsKnob<T extends OptionsTypeKnobSingleValue>(
name: string,
valuesObj: OptionsTypeOptionsProp<T>,
value: OptionsTypeKnobValue<T>,
optionsObj: OptionsKnobOptions,
groupId?: string
): T {
return manager.knob(name, { type: 'options', options: valuesObj, value, optionsObj, groupId });
}
const defaultOptions = {
escapeHTML: true,
};
export const withKnobs = makeDecorator({
name: 'withKnobs',
parameterName: 'knobs',
skipIfNoParametersOrOptions: false,
wrapper: (getStory, context, { options, parameters }) => {
const storyOptions = parameters || options;
const allOptions = { ...defaultOptions, ...storyOptions };
const channel = addons.getChannel();
manager.setChannel(channel);
manager.setOptions(allOptions);
channel.emit(SET_OPTIONS, allOptions);
registerKnobs();
return getStory(context);
},
});
export * from './shared';
if (module && module.hot && module.hot.decline) {
module.hot.decline();
}

View File

@ -1,3 +0,0 @@
import { withKnobs } from '../index';
export const decorators = [withKnobs];

View File

@ -1,13 +0,0 @@
import * as React from 'react';
import addons from '@storybook/addons';
import Panel from './components/Panel';
import { ADDON_ID, PANEL_ID, PARAM_KEY } from './shared';
import { createTitleListener } from './title';
addons.register(ADDON_ID, (api) => {
addons.addPanel(PANEL_ID, {
title: createTitleListener(api),
render: ({ active, key }) => <Panel api={api} key={key} active={active} />,
paramKey: PARAM_KEY,
});
});

View File

@ -1,92 +0,0 @@
import addons from '@storybook/addons';
import { useEffect } from '@storybook/client-api';
import { STORY_CHANGED, FORCE_RE_RENDER } from '@storybook/core-events';
import debounce from 'lodash/debounce';
import KnobManager from './KnobManager';
import { CHANGE, CLICK, RESET, SET } from './shared';
import { KnobStoreKnob } from './KnobStore';
export const manager = new KnobManager();
const { knobStore } = manager;
const COMPONENT_FORCE_RENDER_DEBOUNCE_DELAY_MS = 325;
function forceReRender() {
addons.getChannel().emit(FORCE_RE_RENDER);
}
function setPaneKnobs(timestamp: boolean | number = +new Date()) {
const channel = addons.getChannel();
channel.emit(SET, { knobs: knobStore.getAll(), timestamp });
}
const resetAndForceUpdate = () => {
knobStore.markAllUnused();
forceReRender();
};
// Increase performance by reducing how frequently the story is recreated during knob changes
const debouncedResetAndForceUpdate = debounce(
resetAndForceUpdate,
COMPONENT_FORCE_RENDER_DEBOUNCE_DELAY_MS
);
function knobChanged(change: KnobStoreKnob) {
const { name } = change;
const { value } = change; // Update the related knob and it's value.
const knobOptions = knobStore.get(name);
knobOptions.value = value;
if (!manager.options.disableForceUpdate && !knobOptions.disableForceUpdate) {
if (!manager.options.disableDebounce && !knobOptions.disableDebounce) {
debouncedResetAndForceUpdate();
} else {
resetAndForceUpdate();
}
}
}
function knobClicked(clicked: KnobStoreKnob) {
const knobOptions = knobStore.get(clicked.name);
if (knobOptions.callback && knobOptions.callback() !== false) {
forceReRender();
}
}
function resetKnobs() {
knobStore.reset();
setPaneKnobs(false);
}
function resetKnobsAndForceReRender() {
knobStore.reset();
forceReRender();
setPaneKnobs(false);
}
function disconnectCallbacks() {
const channel = addons.getChannel();
channel.removeListener(CHANGE, knobChanged);
channel.removeListener(CLICK, knobClicked);
channel.removeListener(STORY_CHANGED, resetKnobs);
channel.removeListener(RESET, resetKnobsAndForceReRender);
knobStore.unsubscribe(setPaneKnobs);
}
function connectCallbacks() {
const channel = addons.getChannel();
channel.on(CHANGE, knobChanged);
channel.on(CLICK, knobClicked);
channel.on(STORY_CHANGED, resetKnobs);
channel.on(RESET, resetKnobsAndForceReRender);
knobStore.subscribe(setPaneKnobs);
return disconnectCallbacks;
}
export function registerKnobs() {
useEffect(connectCallbacks, []);
}

View File

@ -1,10 +0,0 @@
// addons, panels and events get unique names using a prefix
export const PARAM_KEY = 'knobs';
export const ADDON_ID = 'storybookjs/knobs';
export const PANEL_ID = `${ADDON_ID}/panel`;
export const RESET = `${ADDON_ID}/reset`;
export const SET = `${ADDON_ID}/set`;
export const CHANGE = `${ADDON_ID}/change`;
export const SET_OPTIONS = `${ADDON_ID}/set-options`;
export const CLICK = `${ADDON_ID}/click`;

View File

@ -1,13 +0,0 @@
import { API } from '@storybook/api';
import { SET } from './shared';
import { KnobStoreKnob } from './KnobStore';
export function createTitleListener(api: API): () => string {
let knobsCount = 0;
api.on(SET, ({ knobs }: { knobs: Record<string, KnobStoreKnob> }) => {
knobsCount = Object.keys(knobs).length;
});
return () => (knobsCount === 0 ? 'Knobs' : `Knobs (${knobsCount})`);
}

View File

@ -1,54 +0,0 @@
import {
TextTypeKnob,
NumberTypeKnob,
ColorTypeKnob,
BooleanTypeKnob,
ObjectTypeKnob,
SelectTypeKnob,
RadiosTypeKnob,
ArrayTypeKnob,
DateTypeKnob,
ButtonTypeOnClickProp,
FileTypeKnob,
OptionsTypeKnob,
KnobType,
} from './components/types';
export type Mutable<T> = {
-readonly [P in keyof T]: T[P] extends readonly (infer U)[] ? U[] : T[P];
};
type KnobPlus<T extends KnobType, K> = K & {
type: T;
groupId?: string;
disableDebounce?: boolean;
disableForceUpdate?: boolean;
};
export type Knob<T extends KnobType = any> = T extends 'text'
? KnobPlus<T, Pick<TextTypeKnob, 'value'>>
: T extends 'boolean'
? KnobPlus<T, Pick<BooleanTypeKnob, 'value'>>
: T extends 'number'
? KnobPlus<T, Pick<NumberTypeKnob, 'value' | 'range' | 'min' | 'max' | 'step'>>
: T extends 'color'
? KnobPlus<T, Pick<ColorTypeKnob, 'value'>>
: T extends 'object'
? KnobPlus<T, Pick<ObjectTypeKnob<any>, 'value'>>
: T extends 'select'
? KnobPlus<T, Pick<SelectTypeKnob, 'value' | 'options'> & { selectV2: true }>
: T extends 'radios'
? KnobPlus<T, Pick<RadiosTypeKnob, 'value' | 'options'>>
: T extends 'array'
? KnobPlus<T, Pick<ArrayTypeKnob, 'value' | 'separator'>>
: T extends 'date'
? KnobPlus<T, Pick<DateTypeKnob, 'value'>>
: T extends 'files'
? KnobPlus<T, Pick<FileTypeKnob, 'value' | 'accept'>>
: T extends 'button'
? KnobPlus<T, { value?: never; callback: ButtonTypeOnClickProp; hideLabel: true }>
: T extends 'options'
? KnobPlus<T, Pick<OptionsTypeKnob<any>, 'options' | 'value' | 'optionsObj'>>
: never;
export type { KnobType };

View File

@ -1 +0,0 @@
declare module 'global';

View File

@ -1,18 +0,0 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env", "jest", "@testing-library/jest-dom"],
"strict": true,
"noUnusedLocals": true
},
"include": ["src/**/*"],
"exclude": [
"src/**/*.test.*",
"src/**/tests/**/*",
"src/**/__tests__/**/*",
"src/**/*.stories.*",
"src/**/*.mockdata.*",
"src/**/__testfixtures__/**"
]
}

View File

@ -1,21 +0,0 @@
/* eslint-disable cypress/no-unnecessary-waiting */
import { clickAddon, visit } from '../helper';
describe('Knobs', () => {
beforeEach(() => {
visit('official-storybook/?path=/story/addons-knobs-withknobs--tweaks-static-values');
});
it('[text] it should change a string value', () => {
clickAddon('Knobs');
cy.get('#Name').clear().type('John Doe');
cy.getStoryElement()
.console('info')
.wait(3000)
.find('p')
.eq(0)
.should('contain.text', 'My name is John Doe');
});
});

View File

@ -7,7 +7,6 @@ module.exports = {
'@storybook/addon-storysource',
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addon-knobs',
'@storybook/addon-jest',
'@storybook/addon-backgrounds',
'@storybook/addon-a11y',

View File

@ -43,7 +43,6 @@
"@storybook/addon-controls": "6.3.0-alpha.21",
"@storybook/addon-docs": "6.3.0-alpha.21",
"@storybook/addon-jest": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-storyshots": "6.3.0-alpha.21",
"@storybook/addon-storysource": "6.3.0-alpha.21",

View File

@ -19,12 +19,12 @@ exports[`Storyshots Addons/Docs with text 1`] = `
<storybook-wrapper>
<storybook-button-component
_nghost-a-c8=""
ng-reflect-text="Hello Button"
ng-reflect-text="hello button"
>
<button
_ngcontent-a-c8=""
>
Hello Button
hello button
</button>
</storybook-button-component>
</storybook-wrapper>

View File

@ -2,7 +2,6 @@ import { moduleMetadata } from '@storybook/angular';
import { Story, Meta, ArgsTable } from '@storybook/addon-docs';
import { Welcome, Button } from '@storybook/angular/demo';
import { linkTo } from '@storybook/addon-links';
import { text, withKnobs } from '@storybook/addon-knobs';
import { DocButtonComponent } from './doc-button/doc-button.component';
# Storybook Docs for Angular
@ -21,7 +20,7 @@ How you like them apples?!
Just like in React, we first declare our component.
<Meta title="Addons/Docs" decorators={[withKnobs, moduleMetadata({ declarations: [Button] })]} />
<Meta title="Addons/Docs" decorators={[moduleMetadata({ declarations: [Button] })]} />
This declaration doesn't show up in the MDX output.
@ -55,7 +54,7 @@ Similarly, here's how we do it in the Docs MDX format. We've already added the d
{{
template: `<storybook-button-component [text]="text" (onClick)="onClick($event)"></storybook-button-component>`,
props: {
text: text('Button text', 'Hello Button'),
text: 'hello button',
onClick: () => {},
},
}}

View File

@ -1,82 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons/Knobs All knobs 1`] = `
<storybook-wrapper>
<storybook-simple-knobs-component
ng-reflect-border="deeppink"
ng-reflect-fruit="apples"
ng-reflect-items="Laptop,Book,Whiskey"
ng-reflect-name="Jane"
ng-reflect-nice="true"
ng-reflect-price="2.25"
ng-reflect-stock="20"
ng-reflect-today="1487548800000"
>
<div
ng-reflect-ng-style="[object Object]"
style="border: 2px dotted deeppink; border-radius: 8px;"
>
<h1>
My name is Jane,
</h1>
<h3>
today is Feb 20, 2017
</h3>
<p>
I have a stock of 20 apples, costing $ 2.25 each.
</p>
<p>
Sorry.
</p>
<p>
Also, I have:
</p>
<ul>
<li>
Laptop
</li>
<li>
Book
</li>
<li>
Whiskey
</li>
</ul>
<p>
Nice to meet you!
</p>
</div>
</storybook-simple-knobs-component>
</storybook-wrapper>
`;
exports[`Storyshots Addons/Knobs Simple 1`] = `
<storybook-wrapper>
<h1>
This is a template
</h1><storybook-simple-knobs-component
ng-reflect-age="0"
ng-reflect-name="John Doe"
ng-reflect-phone-number="555-55-55"
>
<div>
I am John Doe and I'm 0 years old.
</div>
<div>
Phone Number: 555-55-55
</div>
</storybook-simple-knobs-component>
</storybook-wrapper>
`;
exports[`Storyshots Addons/Knobs XSS safety 1`] = `
<storybook-wrapper>
&lt;img src=x onerror="alert('XSS Attack')" &gt;
</storybook-wrapper>
`;

View File

@ -1,110 +0,0 @@
import { action } from '@storybook/addon-actions';
import {
withKnobs,
text,
number,
boolean,
array,
select,
radios,
color,
date,
button,
} from '@storybook/addon-knobs';
import { SimpleKnobsComponent } from './knobs.component';
import { AllKnobsComponent } from './all-knobs.component';
export default {
title: 'Addons/Knobs',
decorators: [withKnobs],
parameters: {
knobs: {
disableDebounce: true,
},
},
};
export const Simple = () => {
const name = text('name', 'John Doe');
const age = number('age', 0);
const phoneNumber = text('phoneNumber', '555-55-55');
return {
moduleMetadata: {
entryComponents: [SimpleKnobsComponent],
declarations: [SimpleKnobsComponent],
},
template: `
<h1> This is a template </h1>
<storybook-simple-knobs-component
[age]="age"
[phoneNumber]="phoneNumber"
[name]="name"
>
</storybook-simple-knobs-component>
`,
props: {
name,
age,
phoneNumber,
},
};
};
Simple.storyName = 'Simple';
export const AllKnobs = () => {
const name = text('name', 'Jane');
const stock = number('stock', 20, {
range: true,
min: 0,
max: 30,
step: 5,
});
const fruits = {
Apple: 'apples',
Banana: 'bananas',
Cherry: 'cherries',
};
const fruit = select('fruit', fruits, 'apples');
const otherFruits = {
Kiwi: 'kiwi',
Guava: 'guava',
Watermelon: 'watermelon',
};
const otherFruit = radios('Other Fruit', otherFruits, 'watermelon');
const price = number('price', 2.25);
const border = color('border', 'deeppink');
const today = date('today', new Date(Date.UTC(2017, 1, 20)));
const items = array('items', ['Laptop', 'Book', 'Whiskey']);
const nice = boolean('nice', true);
button('Arbitrary action', action('You clicked it!'));
return {
props: {
name,
stock,
fruit,
otherFruit,
price,
border,
today,
items,
nice,
},
};
};
AllKnobs.storyName = 'All knobs';
AllKnobs.parameters = {
component: AllKnobsComponent,
};
export const XssSafety = () => ({
template: text('Rendered string', '<img src=x onerror="alert(\'XSS Attack\')" >'),
});
XssSafety.storyName = 'XSS safety';

View File

@ -1,60 +0,0 @@
/* eslint-disable no-useless-constructor */
import { Component, Input, OnInit, OnChanges, SimpleChanges } from '@angular/core';
@Component({
selector: 'storybook-simple-knobs-component',
template: `
<div
[ngStyle]="{ border: '2px dotted ' + border, 'padding.px': '8 22', 'border-radius.px': '8' }"
>
<h1>My name is {{ name }},</h1>
<h3>today is {{ today | date: 'MMM d, y':'UTC' }}</h3>
<p *ngIf="stock">I have a stock of {{ stock }} {{ fruit }}, costing $ {{ price }} each.</p>
<p *ngIf="!stock">I'm out of {{ fruit }}.</p>
<p *ngIf="stock && nice">Sorry.</p>
<p>Also, I have:</p>
<ul>
<li *ngFor="let item of items">{{ item }}</li>
</ul>
<p *ngIf="nice">Nice to meet you!</p>
<p *ngIf="!nice">Leave me alone!</p>
</div>
`,
})
export class AllKnobsComponent implements OnChanges, OnInit {
@Input()
price;
@Input()
border;
@Input()
fruit;
@Input()
name;
@Input()
items;
@Input()
today;
@Input()
stock;
@Input()
nice;
constructor() {
// logger.debug('constructor');
}
ngOnInit(): void {
// logger.debug('on init, user component');
}
ngOnChanges(changes: SimpleChanges): void {
// logger.debug(changes);
}
}

View File

@ -1,19 +0,0 @@
import { Component, Input } from '@angular/core';
@Component({
selector: 'storybook-simple-knobs-component',
template: `
<div>I am {{ name }} and I'm {{ age }} years old.</div>
<div>Phone Number: {{ phoneNumber }}</div>
`,
})
export class SimpleKnobsComponent {
@Input()
name;
@Input()
age;
@Input()
phoneNumber;
}

View File

@ -16,7 +16,6 @@ module.exports = {
},
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addon-knobs',
'@storybook/addon-backgrounds',
'@storybook/addon-a11y',
'@storybook/addon-jest',

View File

@ -26,7 +26,6 @@
"@storybook/addon-backgrounds": "6.3.0-alpha.21",
"@storybook/addon-docs": "6.3.0-alpha.21",
"@storybook/addon-jest": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-storyshots": "6.3.0-alpha.21",
"@storybook/addons": "6.3.0-alpha.21",

View File

@ -1,10 +1,6 @@
import { addParameters, addDecorator } from '@storybook/react';
import { withKnobs } from '@storybook/addon-knobs';
addDecorator(withKnobs);
addParameters({
export const parameters = {
options: {
brandTitle: 'CRA TypeScript Kitchen Sink',
brandUrl: 'https://github.com/storybookjs/storybook/tree/master/examples/cra-ts-kitchen-sink',
},
});
};

View File

@ -37,7 +37,6 @@
"@storybook/addon-a11y": "6.3.0-alpha.21",
"@storybook/addon-actions": "6.3.0-alpha.21",
"@storybook/addon-docs": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addons": "6.3.0-alpha.21",
"@storybook/builder-webpack4": "6.3.0-alpha.21",

View File

@ -1,6 +1,5 @@
import React from 'react';
import { action } from '@storybook/addon-actions';
import { radios } from '@storybook/addon-knobs';
import Button, { Type } from './Button';
export default {
@ -18,6 +17,4 @@ const typeOptions = {
Action: 'action',
};
export const WithType = () => (
<Button type={radios('Type', typeOptions, typeOptions.Default) as Type}>Label</Button>
);
export const WithType = () => <Button type={typeOptions.Default as Type}>Label</Button>;

View File

@ -14,7 +14,6 @@ module.exports = {
'@storybook/addon-docs',
'@storybook/addon-controls',
'@storybook/addon-links',
'@storybook/addon-knobs',
'@storybook/addon-viewport',
'@storybook/addon-backgrounds',
],

View File

@ -22,7 +22,6 @@
"@storybook/addon-backgrounds": "6.3.0-alpha.21",
"@storybook/addon-controls": "6.3.0-alpha.21",
"@storybook/addon-docs": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-storysource": "6.3.0-alpha.21",
"@storybook/addon-viewport": "6.3.0-alpha.21",

View File

@ -1,36 +0,0 @@
import { hbs } from 'ember-cli-htmlbars';
import { withKnobs, text, color, boolean } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
export default {
title: 'Addon/Knobs',
decorators: [withKnobs],
parameters: {
options: { selectedPanel: 'storybookjs/knobs/panel' },
},
};
export const WithText = () => ({
template: hbs`
{{welcome-banner
style=(if hidden "display: none")
backgroundColor=backgroundColor
titleColor=titleColor
subTitleColor=subTitleColor
title=title
subtitle=subtitle
click=(action onClick)
}}
`,
context: {
hidden: boolean('hidden', false),
backgroundColor: color('backgroundColor', '#FDF4E7'),
titleColor: color('titleColor', '#DF4D37'),
subTitleColor: color('subTitleColor', '#B8854F'),
title: text('title', 'Welcome to storybook'),
subtitle: text('subtitle', 'This environment is completely editable'),
onClick: action('clicked'),
},
});
WithText.storyName = 'with text';

View File

@ -9,7 +9,6 @@ module.exports = {
'@storybook/addon-backgrounds',
'@storybook/addon-controls',
'@storybook/addon-jest',
'@storybook/addon-knobs',
'@storybook/addon-links',
{
name: '@storybook/addon-postcss',

View File

@ -19,7 +19,6 @@
"@storybook/addon-controls": "6.3.0-alpha.21",
"@storybook/addon-docs": "6.3.0-alpha.21",
"@storybook/addon-jest": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-postcss": "^2.0.0",
"@storybook/addon-storyshots": "6.3.0-alpha.21",

View File

@ -1,70 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons/Knobs All knobs 1`] = `
<div
style="border: 2px dotted deeppink; padding: 8px 22px; border-radius: 8px"
>
<h1>
My name is Jane,
</h1>
<h3>
today is January 20, 2017
</h3>
<p>
I have a stock of 20 apples, costing $2.25 each.
</p>
<p>
Also, I have:
</p>
<ul>
<li>
Laptop
</li>
<li>
Book
</li>
<li>
Whiskey
</li>
</ul>
<p>
Nice to meet you!
</p>
</div>
`;
exports[`Storyshots Addons/Knobs CSS transitions 1`] = `
<p
style="transition: color 0.5s ease-out; color: orangered;"
>
John Doe
</p>
`;
exports[`Storyshots Addons/Knobs DOM 1`] = `
<p>
John Doe
</p>
`;
exports[`Storyshots Addons/Knobs Simple 1`] = `
<div>
I am John Doe and I'm 44 years old.
</div>
`;
exports[`Storyshots Addons/Knobs XSS safety 1`] = `&lt;img src=x onerror="alert('XSS Attack')" &gt;`;

View File

@ -1,91 +0,0 @@
import { action } from '@storybook/addon-actions';
import { document } from 'global';
import {
array,
boolean,
button,
color,
date,
select,
withKnobs,
text,
number,
} from '@storybook/addon-knobs';
const cachedContainer = document.createElement('p');
export default {
title: 'Addons/Knobs',
decorators: [withKnobs],
};
export const Simple = () => {
const name = text('Name', 'John Doe');
const age = number('Age', 44);
const content = `I am ${name} and I'm ${age} years old.`;
return `<div>${content}</div>`;
};
export const DOM = () => {
const name = text('Name', 'John Doe');
const container = document.createElement('p');
container.textContent = name;
return container;
};
export const Story3 = () => {
const name = text('Name', 'John Doe');
const textColor = color('Text color', 'orangered');
cachedContainer.textContent = name;
cachedContainer.style.transition = 'color 0.5s ease-out';
cachedContainer.style.color = textColor;
return cachedContainer;
};
Story3.storyName = 'CSS transitions';
export const Story4 = () => {
const name = text('Name', 'Jane');
const stock = number('Stock', 20, {
range: true,
min: 0,
max: 30,
step: 5,
});
const fruits = {
Apple: 'apples',
Banana: 'bananas',
Cherry: 'cherries',
};
const fruit = select('Fruit', fruits, 'apples');
const price = number('Price', 2.25);
const colour = color('Border', 'deeppink');
const today = date('Today', new Date('Jan 20 2017 GMT+0'));
const items = array('Items', ['Laptop', 'Book', 'Whiskey']);
const nice = boolean('Nice', true);
const stockMessage = stock
? `I have a stock of ${stock} ${fruit}, costing &dollar;${price} each.`
: `I'm out of ${fruit}${nice ? ', Sorry!' : '.'}`;
const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!';
const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' };
button('Arbitrary action', action('You clicked it!'));
const style = `border: 2px dotted ${colour}; padding: 8px 22px; border-radius: 8px`;
return `<div style="${style}">
<h1>My name is ${name},</h1>
<h3>today is ${new Date(today).toLocaleDateString('en-US', dateOptions)}</h3>
<p>${stockMessage}</p>
<p>Also, I have:</p>
<ul>${items.map((item) => `<li>${item}</li>`).join('')}</ul>
<p>${salutation}</p>
</div>
`;
};
Story4.storyName = 'All knobs';
export const Story5 = () => text('Rendered string', '<img src=x onerror="alert(\'XSS Attack\')" >');
Story5.storyName = 'XSS safety';

View File

@ -1,6 +1,5 @@
declare module 'global';
declare module 'format-json';
declare module '@storybook/addon-knobs';
declare module '*.json' {
const value: any;
export default value;

View File

@ -22,7 +22,6 @@ const config: StorybookConfig = {
{ name: '@storybook/addon-essentials' },
'@storybook/addon-storysource',
'@storybook/addon-links',
'@storybook/addon-knobs',
'@storybook/addon-a11y',
'@storybook/addon-jest',
'@storybook/addon-graphql',

View File

@ -20,7 +20,6 @@
"@storybook/addon-controls": "6.3.0-alpha.21",
"@storybook/addon-docs": "6.3.0-alpha.21",
"@storybook/addon-jest": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-storyshots": "6.3.0-alpha.21",
"@storybook/addon-storyshots-puppeteer": "6.3.0-alpha.21",

View File

@ -8,7 +8,6 @@ import {
ColorItem,
Meta,
} from '@storybook/addon-docs';
import { withKnobs, text } from '@storybook/addon-knobs';
import { action } from '@storybook/addon-actions';
import { Button } from '@storybook/react/demo';
import styled from 'styled-components';
@ -83,7 +82,7 @@ export const nonStory2 = () => <Button>Not a story</Button>; // another one
<Canvas>
<Story name="hello story">
<Button onClick={action('clicked')}>{text('caption', 'hello world')}</Button>
<Button onClick={action('clicked')}>hello world</Button>
</Story>
<Story name="goodbye">
<Button onClick={action('clicked')}>goodbye world</Button>

View File

@ -1,6 +1,5 @@
import * as React from 'react';
import { action } from '@storybook/addon-actions';
import { radios } from '@storybook/addon-knobs';
import Button, { Type } from '../../components/TsButton';
export default {
@ -23,6 +22,4 @@ const typeOptions = {
Action: 'action',
};
export const WithType = () => (
<Button type={radios('Type', typeOptions, typeOptions.Default) as Type}>Label</Button>
);
export const WithType = () => <Button type={typeOptions.Default as Type}>Label</Button>;

View File

@ -1,16 +0,0 @@
import { withKnobs, text } from '@storybook/addon-knobs';
export default {
title: 'Addons/Knobs/with decorators',
};
export const WithDecoratorCallingStoryFunctionMoreThanOnce = () => {
return text('Text', 'Hello');
};
WithDecoratorCallingStoryFunctionMoreThanOnce.decorators = [
withKnobs,
(storyFn) => {
storyFn();
return storyFn();
},
];

View File

@ -1,14 +0,0 @@
import React from 'react';
import { withKnobs, text } from '@storybook/addon-knobs';
export default {
title: 'Addons/Knobs/withKnobs using options',
decorators: [
withKnobs({
escapeHTML: false,
}),
],
};
export const AcceptsOptions = () => <div>{text('Rendered string', '<h1>Hello</h1>')}</div>;
AcceptsOptions.storyName = 'accepts options';

View File

@ -1,380 +0,0 @@
import React, { Fragment } from 'react';
import PropTypes from 'prop-types';
import {
withKnobs,
text,
number,
boolean,
color,
select,
radios,
array,
date,
button,
object,
files,
optionsKnob as options,
} from '@storybook/addon-knobs';
const ItemLoader = ({ isLoading, items }) => {
if (isLoading) {
return <p>Loading data</p>;
}
if (!items.length) {
return <p>No items loaded</p>;
}
return (
<ul>
{items.map((i) => (
<li key={i}>{i}</li>
))}
</ul>
);
};
ItemLoader.propTypes = {
isLoading: PropTypes.bool.isRequired,
items: PropTypes.arrayOf(PropTypes.string).isRequired,
};
let injectedItems = [];
let injectedIsLoading = false;
export default {
title: 'Addons/Knobs/withKnobs',
decorators: [withKnobs],
};
export const selectKnob = () => {
const value = select('value', [1, 2, 3, undefined, null], 1);
return <div>{JSON.stringify({ value: String(value) }, null, 2)}</div>;
};
export const TweaksStaticValues = () => {
const name = text('Name', 'Storyteller');
const age = number('Age', 70, { range: true, min: 0, max: 90, step: 5 });
const fruits = {
Apple: 'apple',
Banana: 'banana',
Cherry: 'cherry',
};
const fruit = select('Fruit', fruits, 'apple');
const otherFruits = {
Kiwi: 'kiwi',
Guava: 'guava',
Watermelon: 'watermelon',
};
const otherFruit = radios('Other Fruit', otherFruits, 'watermelon');
const dollars = number('Dollars', 12.5, { min: 0, max: 100, step: 0.01 });
const years = number('Years in NY', 9);
const backgroundColor = color('background', '#dedede');
const items = array('Items', ['Laptop', 'Book', 'Whiskey']);
const otherStyles = object('Styles', {
border: '2px dashed silver',
borderRadius: 10,
padding: 10,
});
const nice = boolean('Nice', true);
const images = files('Happy Picture', 'image/*', [
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAQAAAC1+jfqAAAABGdBTUEAALGPC/xhBQAAACBjSFJNAAB6JgAAgIQAAPoAAACA6AAAdTAAAOpgAAA6mAAAF3CculE8AAAAAmJLR0QA/4ePzL8AAAAHdElNRQfiARwMCyEWcOFPAAAAP0lEQVQoz8WQMQoAIAwDL/7/z3GwghSp4KDZyiUpBMCYUgd8rehtH16/l3XewgU2KAzapjXBbNFaPS6lDMlKB6OiDv3iAH1OAAAAJXRFWHRkYXRlOmNyZWF0ZQAyMDE4LTAxLTI4VDEyOjExOjMzLTA3OjAwlAHQBgAAACV0RVh0ZGF0ZTptb2RpZnkAMjAxOC0wMS0yOFQxMjoxMTozMy0wNzowMOVcaLoAAAAASUVORK5CYII=',
]);
// array of objects
const arrayOfObjects = [
{
label: 'Sparky',
dogParent: 'Matthew',
location: 'Austin',
},
{
label: 'Juniper',
dogParent: 'Joshua',
location: 'Austin',
},
];
const dog = select('Dogs', arrayOfObjects, arrayOfObjects[0]);
// NOTE: the default value must not change - e.g., do not do date('Label', new Date()) or date('Label')
const defaultBirthday = new Date('Jan 20 2017 GMT+0');
const birthday = date('Birthday', defaultBirthday);
const intro = `My name is ${name}, I'm ${age} years old, and my favorite fruit is ${fruit}. I also enjoy ${otherFruit}, and hanging out with my dog ${dog.label}`;
const style = { backgroundColor, ...otherStyles };
const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!';
const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' };
return (
<div style={style}>
<p>{intro}</p>
<p>My birthday is: {new Date(birthday).toLocaleDateString('en-US', dateOptions)}</p>
<p>I live in NY for {years} years.</p>
<p>My wallet contains: ${dollars.toFixed(2)}</p>
<p>In my backpack, I have:</p>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<p>{salutation}</p>
<p>
When I am happy I look like this: <img src={images[0]} alt="happy" />
</p>
</div>
);
};
TweaksStaticValues.storyName = 'tweaks static values';
export const TweaksStaticValuesOrganizedInGroups = () => {
const GROUP_IDS = {
DISPLAY: 'Display',
GENERAL: 'General',
FAVORITES: 'Favorites',
};
const fruits = {
Apple: 'apple',
Banana: 'banana',
Cherry: 'cherry',
};
const otherFruits = {
Kiwi: 'kiwi',
Guava: 'guava',
Watermelon: 'watermelon',
};
// NOTE: the default value must not change - e.g., do not do date('Label', new Date()) or date('Label')
const defaultBirthday = new Date('Jan 20 2017 GMT+0');
// Ungrouped
const ungrouped = text('Ungrouped', 'Mumble');
// General
const name = text('Name', 'Storyteller', GROUP_IDS.GENERAL);
const age = number('Age', 70, { range: true, min: 0, max: 90, step: 5 }, GROUP_IDS.GENERAL);
const birthday = date('Birthday', defaultBirthday, GROUP_IDS.GENERAL);
const dollars = number(
'Account Balance',
12.5,
{ min: 0, max: 100, step: 0.01 },
GROUP_IDS.GENERAL
);
const years = number('Years in NY', 9, {}, GROUP_IDS.GENERAL);
// Favorites
const nice = boolean('Nice', true, GROUP_IDS.FAVORITES);
const fruit = select('Fruit', fruits, 'apple', GROUP_IDS.FAVORITES);
const otherFruit = radios('Other Fruit', otherFruits, 'watermelon', GROUP_IDS.FAVORITES);
const items = array('Items', ['Laptop', 'Book', 'Whiskey'], ',', GROUP_IDS.FAVORITES);
// Display
const backgroundColor = color('Color', 'rgba(126, 211, 33, 0.22)', GROUP_IDS.DISPLAY);
const otherStyles = object(
'Styles',
{
border: '2px dashed silver',
borderRadius: 10,
padding: 10,
},
GROUP_IDS.DISPLAY
);
const style = { backgroundColor, ...otherStyles };
const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!';
const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' };
return (
<div style={style}>
<h1>General Information</h1>
<p>Name: {name}</p>
<p>Age: {age}</p>
<p>Birthday: {new Date(birthday).toLocaleDateString('en-US', dateOptions)}</p>
<p>Account Balance: {dollars}</p>
<p>Years in NY: {years}</p>
<hr />
<h1>Favorites</h1>
<p>Catchphrase: {salutation}</p>
<p>Fruit: {fruit}</p>
<p>Other Fruit: {otherFruit}</p>
<p>Items:</p>
<ul>
{items.map((item) => (
<li key={`${item}`}>{item}</li>
))}
</ul>
<p>When I'm by myself, I say: "{ungrouped}"</p>
</div>
);
};
TweaksStaticValuesOrganizedInGroups.storyName = 'tweaks static values organized in groups';
export const DynamicKnobs = () => {
const showOptional = select('Show optional', ['yes', 'no'], 'yes');
return (
<Fragment>
<div>{text('compulsory', 'I must be here')}</div>
{showOptional === 'yes' ? <div>{text('optional', 'I can disappear')}</div> : null}
</Fragment>
);
};
DynamicKnobs.storyName = 'dynamic knobs';
export const ComplexSelect = () => {
const m = select(
'complex',
{
number: 1,
string: 'string',
object: {},
array: [1, 2, 3],
function: () => {},
},
'string'
);
const value = m.toString();
const type = Array.isArray(m) ? 'array' : typeof m;
return (
<pre>
the type of {JSON.stringify(value, null, 2)} = {type}
</pre>
);
};
ComplexSelect.storyName = 'complex select';
export const OptionsKnob = () => {
const valuesRadio = {
Monday: 'Monday',
Tuesday: 'Tuesday',
Wednesday: 'Wednesday',
};
const optionRadio = options('Radio', valuesRadio, 'Tuesday', { display: 'radio' });
const valuesInlineRadio = {
Saturday: 'Saturday',
Sunday: 'Sunday',
};
const optionInlineRadio = options('Inline Radio', valuesInlineRadio, 'Saturday', {
display: 'inline-radio',
});
const valuesSelect = {
January: 'January',
February: 'February',
March: 'March',
};
const optionSelect = options('Select', valuesSelect, 'January', { display: 'select' });
const valuesMultiSelect = {
Apple: 'apple',
Banana: 'banana',
Cherry: 'cherry',
};
const optionsMultiSelect = options('Multi Select', valuesMultiSelect, ['apple'], {
display: 'multi-select',
});
const valuesCheck = {
Corn: 'corn',
Carrot: 'carrot',
Cucumber: 'cucumber',
};
const optionsCheck = options('Check', valuesCheck, ['carrot'], { display: 'check' });
const valuesInlineCheck = {
Milk: 'milk',
Cheese: 'cheese',
Butter: 'butter',
};
const optionsInlineCheck = options('Inline Check', valuesInlineCheck, ['milk'], {
display: 'inline-check',
});
return (
<div>
<p>Weekday: {optionRadio}</p>
<p>Weekend: {optionInlineRadio}</p>
<p>Month: {optionSelect}</p>
<p>Fruit:</p>
<ul>
{optionsMultiSelect.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<p>Vegetables:</p>
<ul>
{optionsCheck.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<p>Dairy:</p>
<ul>
{optionsInlineCheck.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
</div>
);
};
export const TriggersActionsViaButton = () => {
button('Toggle item list state', () => {
if (!injectedIsLoading && injectedItems.length === 0) {
injectedIsLoading = true;
} else if (injectedIsLoading && injectedItems.length === 0) {
injectedIsLoading = false;
injectedItems = ['pencil', 'pen', 'eraser'];
} else if (injectedItems.length > 0) {
injectedItems = [];
}
});
// Needed to enforce @babel/transform-react-constant-elements deoptimization
// See https://github.com/babel/babel/issues/10522
const loaderProps = {
isLoading: injectedIsLoading,
items: injectedItems,
};
return (
<Fragment>
<p>Hit the knob button and it will toggle the items list into multiple states.</p>
<ItemLoader {...loaderProps} />
</Fragment>
);
};
TriggersActionsViaButton.storyName = 'triggers actions via button';
export const ButtonWithReactUseState = () => {
const [counter, setCounter] = React.useState(0);
button('increment', () => setCounter(counter + 1));
button('decrement', () => setCounter(counter - 1));
return counter;
};
export const XssSafety = () => (
<div
// eslint-disable-next-line react/no-danger
dangerouslySetInnerHTML={{
__html: text('Rendered string', '<img src="x" onerror="alert(\'XSS Attack\')" >'),
}}
/>
);
XssSafety.storyName = 'XSS safety';
export const AcceptsStoryParameters = () => <div>{text('Rendered string', '<h1>Hello</h1>')}</div>;
AcceptsStoryParameters.storyName = 'accepts story parameters';
AcceptsStoryParameters.parameters = {
knobs: { escapeHTML: false },
};
export const WithDuplicateDecorator = () => {
return text('Text', 'Hello');
};
WithDuplicateDecorator.decorators = [withKnobs];
export const WithKnobValueToBeEncoded = () => {
return text('Text', '10% 20%');
};

View File

@ -8,7 +8,6 @@ module.exports = {
'@storybook/addon-actions',
'@storybook/addon-docs',
'@storybook/addon-links',
'@storybook/addon-knobs',
'@storybook/addon-viewport',
'@storybook/addon-backgrounds',
'@storybook/addon-a11y',

View File

@ -18,7 +18,6 @@
"@storybook/addon-a11y": "6.3.0-alpha.21",
"@storybook/addon-actions": "6.3.0-alpha.21",
"@storybook/addon-backgrounds": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-storyshots": "6.3.0-alpha.21",
"@storybook/addon-storysource": "6.3.0-alpha.21",

View File

@ -1,20 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addons/Knobs All knobs 1`] = `
"<div style=\\"border:2px dotted deeppink; padding: 8px 22px; border-radius: 8px\\">
<h1>My name is Jane,</h1>
<h3>today is January 20, 2017</h3>
<p>
I have a stock of 20 apples, costing &amp;dollar;2.25 each.
</p>
<p>Also, I have:</p>
<ul>
<li>Laptop</li>
<li>Book</li>
<li>Whiskey</li>
</ul>
<p>Nice to meet you!</p>
</div>"
`;
exports[`Storyshots Addons/Knobs Simple 1`] = `"<div>I am John Doe and I'm 44 years old.</div>"`;

View File

@ -1,76 +0,0 @@
/** @jsx h */
import { h } from 'preact';
import { action } from '@storybook/addon-actions';
import {
withKnobs,
text,
number,
boolean,
array,
select,
color,
date,
button,
} from '@storybook/addon-knobs';
export default {
title: 'Addons/Knobs',
decorators: [withKnobs],
};
export const Simple = () => {
const name = text('Name', 'John Doe');
const age = number('Age', 44);
const content = `I am ${name} and I'm ${age} years old.`;
return <div>{content}</div>;
};
export const AllKnobs = () => {
const name = text('Name', 'Jane');
const stock = number('Stock', 20, {
range: true,
min: 0,
max: 30,
step: 5,
});
const fruits = {
Apple: 'apples',
Banana: 'bananas',
Cherry: 'cherries',
};
const fruit = select('Fruit', fruits, 'apples');
const price = number('Price', 2.25);
const colour = color('Border', 'deeppink');
const today = date('Today', new Date('Jan 20 2017 GMT+0'));
const items = array('Items', ['Laptop', 'Book', 'Whiskey']);
const nice = boolean('Nice', true);
const stockMessage = stock
? `I have a stock of ${stock} ${fruit}, costing &dollar;${price} each.`
: `I'm out of ${fruit}${nice ? ', Sorry!' : '.'}`;
const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!';
const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' };
button('Arbitrary action', action('You clicked it!'));
return (
<div style={`border:2px dotted ${colour}; padding: 8px 22px; border-radius: 8px`}>
<h1>My name is {name},</h1>
<h3>today is {new Date(today).toLocaleDateString('en-US', dateOptions)}</h3>
<p>{stockMessage}</p>
<p>Also, I have:</p>
<ul>
{items.map((item) => (
<li key={item}>{item}</li>
))}
</ul>
<p>{salutation}</p>
</div>
);
};
AllKnobs.storyName = 'All knobs';

View File

@ -19,7 +19,6 @@ module.exports = {
},
'@storybook/addon-controls',
'@storybook/addon-links',
'@storybook/addon-knobs',
'@storybook/addon-backgrounds',
'@storybook/addon-viewport',
'@storybook/addon-a11y',

View File

@ -15,7 +15,6 @@
"@storybook/addon-backgrounds": "6.3.0-alpha.21",
"@storybook/addon-controls": "6.3.0-alpha.21",
"@storybook/addon-docs": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-storyshots": "6.3.0-alpha.21",
"@storybook/addon-storysource": "6.3.0-alpha.21",

View File

@ -1,15 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addon/Knobs Simple 1`] = `
<section
class="storybook-snapshot-container"
>
<div
style="width: 200px; height: 200px; background-color: green;"
>
<p>
I am interactive
</p>
</div>
</section>
`;

View File

@ -1,34 +0,0 @@
import { withKnobs, text, number } from '@storybook/addon-knobs';
import ActionKnobView from './views/ActionKnobView.svelte';
export default {
title: 'Addon/Knobs',
decorators: [withKnobs],
};
export const Simple = () => {
const backgroundColor = text('Background', 'green');
const width = number('Width', 200, {
range: true,
min: 100,
max: 1000,
step: 100,
});
const height = number('Height', 200, {
range: true,
min: 100,
max: 1000,
step: 100,
});
return {
Component: ActionKnobView,
props: {
backgroundColor,
width,
height,
},
};
};

View File

@ -7,7 +7,6 @@ module.exports = {
'@storybook/addon-storysource',
'@storybook/addon-actions',
'@storybook/addon-links',
'@storybook/addon-knobs',
'@storybook/addon-viewport',
'@storybook/addon-backgrounds',
'@storybook/addon-a11y',

View File

@ -19,7 +19,6 @@
"@storybook/addon-backgrounds": "6.3.0-alpha.21",
"@storybook/addon-controls": "6.3.0-alpha.21",
"@storybook/addon-docs": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-storyshots": "6.3.0-alpha.21",
"@storybook/addon-storysource": "6.3.0-alpha.21",

View File

@ -1,51 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Storyshots Addon/Knobs All knobs 1`] = `
<div
style="border: 2px dotted; padding: 8px 22px; border-radius: 8px; border-color: deeppink;"
>
<h1>
My name is Jane,
</h1>
<h3>
today is January 20, 2017
</h3>
<p>
I have a stock of 20 apples, costing $2.25 each.
</p>
<p>
Also, I have:
</p>
<ul>
<li>
Laptop
</li>
<li>
Book
</li>
<li>
Whiskey
</li>
</ul>
<p>
Nice to meet you!
</p>
</div>
`;
exports[`Storyshots Addon/Knobs Simple 1`] = `
<div>
I am John Doe and I'm 40 years old.
</div>
`;
exports[`Storyshots Addon/Knobs XSS safety 1`] = `
<div>
&lt;img src=x onerror="alert('XSS Attack')" &gt;
</div>
`;

View File

@ -82,8 +82,6 @@ The biggest win here is that we don't have to worry about setting the height any
Just like in React, we can easily reference other stories in our docs:
<Story id="addon-knobs--all-knobs" />
<Story id="nonexistent-story" />
## More info

View File

@ -1,116 +0,0 @@
import { action } from '@storybook/addon-actions';
import {
withKnobs,
text,
number,
boolean,
array,
select,
color,
date,
button,
} from '@storybook/addon-knobs';
const logger = console;
export default {
title: 'Addon/Knobs',
decorators: [withKnobs],
};
export const Simple = () => ({
props: {
name: {
type: String,
default: text('Name', 'John Doe'),
},
},
template: `<div @click="age++">I am {{ name }} and I'm {{ age }} years old.</div>`,
data() {
return { age: 40 };
},
created() {
logger.debug('created');
},
destroyed() {
logger.debug('destroyed');
},
});
export const AllKnobs = () => {
const fruits = {
Apple: 'apples',
Banana: 'bananas',
Cherry: 'cherries',
};
button('Arbitrary action', action('You clicked it!'));
return {
props: {
name: { default: text('Name', 'Jane') },
stock: {
default: number('Stock', 20, {
range: true,
min: 0,
max: 30,
step: 5,
}),
},
fruit: { default: select('Fruit', fruits, 'apples') },
price: { default: number('Price', 2.25) },
colour: { default: color('Border', 'deeppink') },
today: { default: date('Today', new Date('Jan 20 2017 GMT+0')) },
// this is necessary, because we cant use arrays/objects directly in vue prop default values
// a factory function is required, but we need to make sure the knob is only called once
items: { default: ((items) => () => items)(array('Items', ['Laptop', 'Book', 'Whiskey'])) },
nice: { default: boolean('Nice', true) },
},
data: () => ({
dateOptions: { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' },
}),
computed: {
stockMessage() {
return this.stock
? `I have a stock of ${this.stock} ${this.fruit}, costing $${this.price} each.`
: `I'm out of ${this.fruit}${this.nice ? ', Sorry!' : '.'}`;
},
salutation() {
return this.nice ? 'Nice to meet you!' : 'Leave me alone!';
},
formattedDate() {
return new Date(this.today).toLocaleDateString('en-US', this.dateOptions);
},
style() {
return {
'border-color': this.colour,
};
},
},
template: `
<div style="border: 2px dotted; padding: 8px 22px; border-radius: 8px" :style="style">
<h1>My name is {{ name }},</h1>
<h3>today is {{ formattedDate }}</h3>
<p>{{ stockMessage }}</p>
<p>Also, I have:</p>
<ul>
<li v-for="item in items" :key="item">{{ item }}</li>
</ul>
<p>{{ salutation }}</p>
</div>
`,
};
};
AllKnobs.storyName = 'All knobs';
export const XssSafety = () => ({
props: {
text: { default: text('Rendered string', '<img src=x onerror="alert(\'XSS Attack\')" >') },
},
template: '<div v-html="text"></div>',
});
XssSafety.storyName = 'XSS safety';

View File

@ -7,7 +7,6 @@ module.exports = {
'@storybook/addon-a11y',
'@storybook/addon-actions',
'@storybook/addon-backgrounds',
'@storybook/addon-knobs',
'@storybook/addon-links',
'@storybook/addon-storysource',
'@storybook/addon-viewport',

View File

@ -18,7 +18,6 @@
"@storybook/addon-controls": "6.3.0-alpha.21",
"@storybook/addon-docs": "6.3.0-alpha.21",
"@storybook/addon-jest": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-storyshots": "6.3.0-alpha.21",
"@storybook/addon-storysource": "6.3.0-alpha.21",

View File

@ -1,103 +0,0 @@
/* eslint-disable import/extensions */
import { action } from '@storybook/addon-actions';
import { html } from 'lit-html';
import '../demo-wc-card.js';
import {
array,
boolean,
button,
color,
date,
select,
withKnobs,
text,
number,
} from '@storybook/addon-knobs';
export default {
title: 'Addons/Knobs',
decorators: [withKnobs],
};
export const Simple = () => {
const header = text('header', 'Power Ranger');
const age = number('Age', 44);
return html`
<demo-wc-card .header=${header}>
I am ${text('Name', 'John Doe')} and I'm ${age} years old.
</demo-wc-card>
`;
};
export const Story3 = () => {
const header = text('header', 'Power Ranger');
const textColor = color('Text color', 'orangered');
return html`
<demo-wc-card .header=${header}>
I am ${text('Name', 'John Doe')} and I'm 30 years old.
</demo-wc-card>
<style>
html {
--demo-wc-card-front-color: ${textColor};
}
</style>
`;
};
Story3.storyName = 'Color Selection';
export const Story4 = () => {
const name = text('Name', 'Jane');
const stock = number('Stock', 20, {
range: true,
min: 0,
max: 30,
step: 5,
});
const fruits = {
Apple: 'apples',
Banana: 'bananas',
Cherry: 'cherries',
};
const fruit = select('Fruit', fruits, 'apples');
const price = number('Price', 2.25);
const colour = color('Border', 'deeppink');
const today = date('Today', new Date('Jan 20 2017 GMT+0'));
const items = array('Items', ['Laptop', 'Book', 'Whiskey']);
const nice = boolean('Nice', true);
const stockMessage = stock
? `I have a stock of ${stock} ${fruit}, costing &dollar;${price} each.`
: `I'm out of ${fruit}${nice ? ', Sorry!' : '.'}`;
const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!';
const dateOptions = { year: 'numeric', month: 'long', day: 'numeric', timeZone: 'UTC' };
button('Arbitrary action', action('You clicked it!'));
const style = `border: 2px dotted ${colour}; padding: 8px 22px; border-radius: 8px`;
return html`
<div style="${style}">
<h1>My name is ${name},</h1>
<h3>today is ${new Date(today).toLocaleDateString('en-US', dateOptions)}</h3>
<p>${stockMessage}</p>
<p>Also, I have:</p>
<ul>
${items.map((item) => html` <li>${item}</li> `)}
</ul>
<p>${salutation}</p>
</div>
`;
};
Story4.storyName = 'All knobs';
export const XssSafety = () => {
const content = text('content', '<img src=x onerror="alert(\'XSS Attack\')" >');
return html`
<demo-wc-card>
Code text works :)<br />
Xss insert? ${content}
</demo-wc-card>
`;
};

View File

@ -7,7 +7,6 @@
"@storybook/addon-essentials": "6.3.0-alpha.21",
"@storybook/addon-google-analytics": "6.3.0-alpha.21",
"@storybook/addon-jest": "6.3.0-alpha.21",
"@storybook/addon-knobs": "6.3.0-alpha.21",
"@storybook/addon-links": "6.3.0-alpha.21",
"@storybook/addon-storyshots": "6.3.0-alpha.21",
"@storybook/addon-storyshots-puppeteer": "6.3.0-alpha.21",

View File

@ -11,7 +11,6 @@ Object {
"ROOT/addons/backgrounds/dist/esm/register.js",
"ROOT/addons/controls/dist/esm/register.js",
"ROOT/addons/jest/register.js",
"ROOT/addons/knobs/dist/esm/register.js",
"ROOT/addons/links/dist/esm/register.js",
"ROOT/addons/storysource/dist/esm/register.js",
"ROOT/addons/viewport/dist/esm/register.js",

View File

@ -11,7 +11,6 @@ Object {
"ROOT/addons/backgrounds/dist/esm/register.js",
"ROOT/addons/controls/dist/esm/register.js",
"ROOT/addons/jest/register.js",
"ROOT/addons/knobs/dist/esm/register.js",
"ROOT/addons/links/dist/esm/register.js",
"ROOT/addons/storysource/dist/esm/register.js",
"ROOT/addons/viewport/dist/esm/register.js",

View File

@ -14,7 +14,6 @@ Object {
"ROOT/addons/actions/dist/esm/preset/addArgs.js-generated-other-entry.js",
"ROOT/addons/backgrounds/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/addons/backgrounds/dist/esm/preset/addParameter.js-generated-other-entry.js",
"ROOT/addons/knobs/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/addons/links/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/examples/html-kitchen-sink/.storybook/preview.js-generated-config-entry.js",
"ROOT/examples/html-kitchen-sink/.storybook/generated-stories-entry.js",

View File

@ -14,7 +14,6 @@ Object {
"ROOT/addons/actions/dist/esm/preset/addArgs.js-generated-other-entry.js",
"ROOT/addons/backgrounds/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/addons/backgrounds/dist/esm/preset/addParameter.js-generated-other-entry.js",
"ROOT/addons/knobs/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/addons/links/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/examples/html-kitchen-sink/.storybook/preview.js-generated-config-entry.js",
"ROOT/examples/html-kitchen-sink/.storybook/generated-stories-entry.js",

View File

@ -10,7 +10,6 @@ Object {
"ROOT/addons/a11y/dist/esm/register.js",
"ROOT/addons/actions/dist/esm/register.js",
"ROOT/addons/backgrounds/dist/esm/register.js",
"ROOT/addons/knobs/dist/esm/register.js",
"ROOT/addons/links/dist/esm/register.js",
"ROOT/addons/storysource/dist/esm/register.js",
"ROOT/addons/viewport/dist/esm/register.js",

View File

@ -10,7 +10,6 @@ Object {
"ROOT/addons/a11y/dist/esm/register.js",
"ROOT/addons/actions/dist/esm/register.js",
"ROOT/addons/backgrounds/dist/esm/register.js",
"ROOT/addons/knobs/dist/esm/register.js",
"ROOT/addons/links/dist/esm/register.js",
"ROOT/addons/storysource/dist/esm/register.js",
"ROOT/addons/viewport/dist/esm/register.js",

View File

@ -14,7 +14,6 @@ Object {
"ROOT/addons/actions/dist/esm/preset/addArgs.js-generated-other-entry.js",
"ROOT/addons/backgrounds/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/addons/backgrounds/dist/esm/preset/addParameter.js-generated-other-entry.js",
"ROOT/addons/knobs/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/addons/links/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/examples/web-components-kitchen-sink/.storybook/preview.js-generated-config-entry.js",
"ROOT/examples/web-components-kitchen-sink/.storybook/generated-stories-entry.js",

View File

@ -14,7 +14,6 @@ Object {
"ROOT/addons/actions/dist/esm/preset/addArgs.js-generated-other-entry.js",
"ROOT/addons/backgrounds/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/addons/backgrounds/dist/esm/preset/addParameter.js-generated-other-entry.js",
"ROOT/addons/knobs/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/addons/links/dist/esm/preset/addDecorator.js-generated-other-entry.js",
"ROOT/examples/web-components-kitchen-sink/.storybook/preview.js-generated-config-entry.js",
"ROOT/examples/web-components-kitchen-sink/.storybook/generated-stories-entry.js",

View File

@ -1,6 +1,5 @@
/* eslint-disable react/destructuring-assignment */
import React, { Fragment } from 'react';
import { withKnobs, boolean, number } from '@storybook/addon-knobs';
import { DecoratorFn } from '@storybook/react';
import isChromatic from 'chromatic/isChromatic';
@ -15,11 +14,10 @@ export default {
component: Desktop,
parameters: { passArgsFirst: false },
decorators: [
withKnobs,
((StoryFn, c) => {
const mocked = boolean('mock', true);
const height = number('height', 900);
const width = number('width', 1200);
const mocked = true;
const height = 900;
const width = 1200;
if (isChromatic) {
store.local.set(`storybook-layout`, {});

View File

@ -1,6 +1,5 @@
/* eslint-disable react/destructuring-assignment */
import React, { Fragment } from 'react';
import { withKnobs, boolean } from '@storybook/addon-knobs';
import { ActiveTabs } from '@storybook/api';
import { DecoratorFn } from '@storybook/react';
@ -13,9 +12,8 @@ export default {
component: Mobile,
parameters: { passArgsFirst: false },
decorators: [
withKnobs,
((storyFn, c) => {
const mocked = boolean('mock', true);
const mocked = true;
const props = {
...(mocked ? mockProps : realProps),

Some files were not shown because too many files have changed in this diff Show More