|
| 1 | +/** |
| 2 | + * Storybook addons are React components. The `createAddon` function returns a React component that |
| 3 | + * wraps a custom element and passes on properties and events. This allows for creating addons with |
| 4 | + * web components (and therefore LitElement). |
| 5 | + * |
| 6 | + * The wrapper can forward specific events to your addon (web component) as they occur. Your addon |
| 7 | + * can listen for these events. Some useful Storybook events are forwarded by default (such as when |
| 8 | + * the user changes stories). An `options` parameter can be passed to `createAddon` that contains |
| 9 | + * additional events that you may need for your use case. |
| 10 | + * |
| 11 | + * Storybook expects only 1 addon to be in the DOM, which is the addon that is selected (active). |
| 12 | + * This means addons can be continuously connected/disconnected when switching between addons and |
| 13 | + * stories. This is important to understand to work effectively with LitElement lifecycle methods |
| 14 | + * and events. Addons that rely on events that might occur when it is not active, should have their |
| 15 | + * event listeners set up in the `constructor`. Event listeners set up in the `connectedCallback` |
| 16 | + * should always also be disconnected. |
| 17 | + */ |
| 18 | + |
| 19 | +import { React } from './manager.js'; |
| 20 | +import { |
| 21 | + STORY_SPECIFIED, |
| 22 | + STORY_CHANGED, |
| 23 | + STORY_RENDERED, |
| 24 | +} from './core-events.js'; |
| 25 | + |
| 26 | +// A default set of Storybook events that are forwarded to the addon as they occur. If an addon |
| 27 | +// needs additional events (either Storybook or custom events), they can be passed via the options. |
| 28 | +const storybookEvents = [STORY_SPECIFIED, STORY_CHANGED, STORY_RENDERED]; |
| 29 | +const { Component, createRef, createElement } = React; |
| 30 | +/** |
| 31 | + * @param {String} customElementName |
| 32 | + * @param {Object} [options] |
| 33 | + */ |
| 34 | +export function createAddon(customElementName, options = {}) { |
| 35 | + return class extends Component { |
| 36 | + constructor(props) { |
| 37 | + super(props); |
| 38 | + this.ref = createRef(); |
| 39 | + } |
| 40 | + |
| 41 | + componentDidMount() { |
| 42 | + const customEvents = options.events ?? []; |
| 43 | + const uniqueEvents = Array.from( |
| 44 | + new Set([...storybookEvents, ...customEvents]) |
| 45 | + ); |
| 46 | + uniqueEvents.forEach(event => { |
| 47 | + this.props.api.getChannel().on(event, detail => { |
| 48 | + if (!this.addonElement) { |
| 49 | + this.updateAddon(event); |
| 50 | + } |
| 51 | + this.addonElement.dispatchEvent(new CustomEvent(event, { detail })); |
| 52 | + }); |
| 53 | + }); |
| 54 | + } |
| 55 | + |
| 56 | + componentDidUpdate() { |
| 57 | + this.updateAddon(); |
| 58 | + } |
| 59 | + |
| 60 | + updateAddon() { |
| 61 | + if (!this.addonElement) { |
| 62 | + this.addonElement = document.createElement(customElementName); |
| 63 | + } |
| 64 | + |
| 65 | + const { api, active } = this.props; |
| 66 | + Object.assign(this.addonElement, { api, active }); |
| 67 | + |
| 68 | + // Here, the element could get added for the first time, or re-added after a switch between addons. |
| 69 | + if (this.shouldAddonBeInDom() && !this.ref.current.firstChild) { |
| 70 | + this.ref.current.appendChild(this.addonElement); |
| 71 | + } |
| 72 | + } |
| 73 | + |
| 74 | + shouldAddonBeInDom() { |
| 75 | + return this.ref.current && this.props.active; |
| 76 | + } |
| 77 | + |
| 78 | + render() { |
| 79 | + if (!this.props.active) { |
| 80 | + return null; |
| 81 | + } |
| 82 | + return createElement('div', { ref: this.ref }); |
| 83 | + } |
| 84 | + }; |
| 85 | +} |
0 commit comments