diff --git a/addons/knobs/README.md b/addons/knobs/README.md index 4c3788d9d0ae..8869ab26ef48 100644 --- a/addons/knobs/README.md +++ b/addons/knobs/README.md @@ -231,6 +231,18 @@ const value = date(label, defaultValue); > Note: the default value must not change - e.g., do not do `date('Label', new Date())` or `date('Label')` +### button + +Allows you to include a button and associated handler. + +```js +import { button } from '@storybook/addon-knobs'; + +const label = 'Do Something'; +const handler = () => doSomething('foobar'); +button(label, handler); +``` + ### withKnobs vs withKnobsOptions If you feel like this addon is not performing well enough there is an option to use `withKnobsOptions` instead of `withKnobs`. diff --git a/addons/knobs/src/components/Panel.js b/addons/knobs/src/components/Panel.js index 429958e71b29..062f660c23a1 100644 --- a/addons/knobs/src/components/Panel.js +++ b/addons/knobs/src/components/Panel.js @@ -46,6 +46,7 @@ export default class Panel extends React.Component { constructor(props) { super(props); this.handleChange = this.handleChange.bind(this); + this.handleClick = this.handleClick.bind(this); this.setKnobs = this.setKnobs.bind(this); this.reset = this.reset.bind(this); this.setOptions = this.setOptions.bind(this); @@ -133,6 +134,10 @@ export default class Panel extends React.Component { this.setState({ knobs: newKnobs }, this.emitChange(changedKnob)); } + handleClick(knob) { + this.props.channel.emit('addon:knobs:knobClick', knob); + } + render() { const { knobs } = this.state; const knobsArray = Object.keys(knobs) @@ -146,7 +151,11 @@ export default class Panel extends React.Component { return (
- +
+ ); + } +} + +ButtonType.defaultProps = { + knob: {}, +}; + +ButtonType.propTypes = { + knob: PropTypes.shape({ + name: PropTypes.string, + }), + onClick: PropTypes.func.isRequired, +}; + +ButtonType.serialize = value => value; +ButtonType.deserialize = value => value; + +export default ButtonType; diff --git a/addons/knobs/src/components/types/index.js b/addons/knobs/src/components/types/index.js index a74e2459eaa6..ebd51deb858e 100644 --- a/addons/knobs/src/components/types/index.js +++ b/addons/knobs/src/components/types/index.js @@ -6,6 +6,7 @@ import ObjectType from './Object'; import SelectType from './Select'; import ArrayType from './Array'; import DateType from './Date'; +import ButtonType from './Button'; export default { text: TextType, @@ -16,4 +17,5 @@ export default { select: SelectType, array: ArrayType, date: DateType, + button: ButtonType, }; diff --git a/addons/knobs/src/index.js b/addons/knobs/src/index.js index 2f4c16dcddca..84df833e5c5a 100644 --- a/addons/knobs/src/index.js +++ b/addons/knobs/src/index.js @@ -58,6 +58,10 @@ export function date(name, value = new Date()) { return manager.knob(name, { type: 'date', value: proxyValue }); } +export function button(name, callback) { + return manager.knob(name, { type: 'button', callback, hideLabel: true }); +} + // "Higher order component" / wrapper style API // In 3.3, this will become `withKnobs`, once our decorator API supports it. // See https://github.com/storybooks/storybook/pull/1527 diff --git a/addons/knobs/src/react/WrapStory.js b/addons/knobs/src/react/WrapStory.js index 29568e2950ad..d3f7652c9678 100644 --- a/addons/knobs/src/react/WrapStory.js +++ b/addons/knobs/src/react/WrapStory.js @@ -7,6 +7,7 @@ export default class WrapStory extends React.Component { constructor(props) { super(props); this.knobChanged = this.knobChanged.bind(this); + this.knobClicked = this.knobClicked.bind(this); this.resetKnobs = this.resetKnobs.bind(this); this.setPaneKnobs = this.setPaneKnobs.bind(this); this._knobsAreReset = false; @@ -16,6 +17,8 @@ export default class WrapStory extends React.Component { componentDidMount() { // Watch for changes in knob editor. this.props.channel.on('addon:knobs:knobChange', this.knobChanged); + // Watch for clicks in knob editor. + this.props.channel.on('addon:knobs:knobClick', this.knobClicked); // Watch for the reset event and reset knobs. this.props.channel.on('addon:knobs:reset', this.resetKnobs); // Watch for any change in the knobStore and set the panel again for those @@ -31,6 +34,7 @@ export default class WrapStory extends React.Component { componentWillUnmount() { this.props.channel.removeListener('addon:knobs:knobChange', this.knobChanged); + this.props.channel.removeListener('addon:knobs:knobClick', this.knobClicked); this.props.channel.removeListener('addon:knobs:reset', this.resetKnobs); this.props.knobStore.unsubscribe(this.setPaneKnobs); } @@ -45,11 +49,17 @@ export default class WrapStory extends React.Component { const { knobStore, storyFn, context } = this.props; // Update the related knob and it's value. const knobOptions = knobStore.get(name); + knobOptions.value = value; knobStore.markAllUnused(); this.setState({ storyContent: storyFn(context) }); } + knobClicked(knob) { + const knobOptions = this.props.knobStore.get(knob.name); + knobOptions.callback(); + } + resetKnobs() { const { knobStore, storyFn, context } = this.props; knobStore.reset(); diff --git a/addons/knobs/src/vue/index.js b/addons/knobs/src/vue/index.js index ae99e44f1c1a..a59285819aa5 100644 --- a/addons/knobs/src/vue/index.js +++ b/addons/knobs/src/vue/index.js @@ -8,10 +8,16 @@ export const vueHandler = (channel, knobStore) => getStory => context => ({ const { name, value } = change; // Update the related knob and it's value. const knobOptions = knobStore.get(name); + knobOptions.value = value; this.$forceUpdate(); }, + onKnobClick(knob) { + const knobOptions = knobStore.get(knob.name); + knobOptions.callback(); + }, + onKnobReset() { knobStore.reset(); this.setPaneKnobs(false); @@ -26,12 +32,14 @@ export const vueHandler = (channel, knobStore) => getStory => context => ({ created() { channel.on('addon:knobs:reset', this.onKnobReset); channel.on('addon:knobs:knobChange', this.onKnobChange); + channel.on('addon:knobs:knobClick', this.onKnobClick); knobStore.subscribe(this.setPaneKnobs); }, beforeDestroy() { channel.removeListener('addon:knobs:reset', this.onKnobReset); channel.removeListener('addon:knobs:knobChange', this.onKnobChange); + channel.removeListener('addon:knobs:knobClick', this.onKnobClick); knobStore.unsubscribe(this.setPaneKnobs); }, }); diff --git a/addons/knobs/src/vue/index.test.js b/addons/knobs/src/vue/index.test.js index dcf2528ce45d..9e2b4ca7a3d3 100644 --- a/addons/knobs/src/vue/index.test.js +++ b/addons/knobs/src/vue/index.test.js @@ -32,8 +32,9 @@ describe('Vue handler', () => { const testStore = new KnobStore(); new Vue(vueHandler(testChannel, testStore)(testStory)(testContext)).$mount(); - expect(testChannel.on).toHaveBeenCalledTimes(2); + expect(testChannel.on).toHaveBeenCalledTimes(3); expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:reset', expect.any(Function)); expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:knobChange', expect.any(Function)); + expect(testChannel.on).toHaveBeenCalledWith('addon:knobs:knobClick', expect.any(Function)); }); }); diff --git a/examples/cra-kitchen-sink/src/__snapshots__/storyshots.test.js.snap b/examples/cra-kitchen-sink/src/__snapshots__/storyshots.test.js.snap index 61ad984c0719..ffe4f769c193 100644 --- a/examples/cra-kitchen-sink/src/__snapshots__/storyshots.test.js.snap +++ b/examples/cra-kitchen-sink/src/__snapshots__/storyshots.test.js.snap @@ -1318,6 +1318,13 @@ exports[`Storyshots Button with knobs 1`] = `

Nice to meet you!

+
+

+ PS. My shirt pocket contains: +

+
  • + No items! +
  • `; diff --git a/examples/cra-kitchen-sink/src/stories/index.js b/examples/cra-kitchen-sink/src/stories/index.js index 6668fb687166..b067b63ffa14 100644 --- a/examples/cra-kitchen-sink/src/stories/index.js +++ b/examples/cra-kitchen-sink/src/stories/index.js @@ -1,4 +1,5 @@ import React from 'react'; +import PropTypes from 'prop-types'; import EventEmiter from 'eventemitter3'; import { storiesOf } from '@storybook/react'; @@ -16,6 +17,7 @@ import { select, array, date, + button, object, } from '@storybook/addon-knobs'; import centered from '@storybook/addon-centered'; @@ -59,6 +61,23 @@ const InfoButton = () => ( ); +class AsyncItemLoader extends React.Component { + constructor() { + super(); + this.state = { items: [] }; + } + + loadItems() { + setTimeout(() => this.setState({ items: ['pencil', 'pen', 'eraser'] }), 1500); + } + + render() { + button('Load the items', () => this.loadItems()); + return this.props.children(this.state.items); + } +} +AsyncItemLoader.propTypes = { children: PropTypes.func.isRequired }; + storiesOf('Button', module) .addDecorator(withKnobs) .add('with text', () => ( @@ -118,6 +137,14 @@ storiesOf('Button', module)

    In my backpack, I have:

    {salutation}

    +
    +

    PS. My shirt pocket contains:

    + + {loadedItems => { + if (!loadedItems.length) return
  • No items!
  • ; + return ; + }} +
    ); }) diff --git a/examples/vue-kitchen-sink/src/stories/index.js b/examples/vue-kitchen-sink/src/stories/index.js index 82c9820433c8..9ea6dfdd6ceb 100644 --- a/examples/vue-kitchen-sink/src/stories/index.js +++ b/examples/vue-kitchen-sink/src/stories/index.js @@ -14,6 +14,7 @@ import { select, color, date, + button, } from '@storybook/addon-knobs'; import Centered from '@storybook/addon-centered'; @@ -231,6 +232,8 @@ storiesOf('Addon Knobs', module) : `I'm out of ${fruit}${nice ? ', Sorry!' : '.'}`; const salutation = nice ? 'Nice to meet you!' : 'Leave me alone!'; + button('Arbitrary action', action('You clicked it!')); + return { template: `