UNPKG

wix-storybook-utils

Version:

Utilities for automated component documentation within Storybook

330 lines 16.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var tslib_1 = require("tslib"); var react_1 = tslib_1.__importStar(require("react")); var prop_types_1 = tslib_1.__importDefault(require("prop-types")); var styles_scss_1 = tslib_1.__importDefault(require("./styles.scss")); var no_value_type_1 = tslib_1.__importDefault(require("./no-value-type")); var categorize_props_1 = tslib_1.__importDefault(require("./categorize-props")); var components_1 = require("./components"); var Layout_1 = require("../ui/Layout"); var section_collapse_1 = tslib_1.__importDefault(require("./components/section-collapse")); var match_func_prop_1 = tslib_1.__importDefault(require("./utils/match-func-prop")); var strip_quotes_1 = tslib_1.__importDefault(require("./utils/strip-quotes")); var omit_1 = tslib_1.__importDefault(require("./utils/omit")); var ensure_regexp_1 = tslib_1.__importDefault(require("./utils/ensure-regexp")); var html_props_list_json_1 = tslib_1.__importDefault(require("./utils/html-props-list.json")); /** * Create a playground for some component, which is suitable for storybook. Given raw `source`, component reference * and, optionally, `componentProps`,`AutoExample` will render: * * * list of all available props with toggles or input fields to control them (with `defaultProps` values applied) * * live preview of `component` * * live code example * * * ### Example: * * ```js * import AutoExample from 'stories/utils/Components/AutoExample'; * import component from 'wix-style-react/MyComponent'; * import source from '!raw-loader!wix-style-react/MyComponent/MyComponent'; // raw string, not something like `export {default} from './MyComponent.js';` * * <AutoExample * source={source} * component={component} * componentProps={{ * value: 'some default value', * onClick: () => console.log('some handler') * }} * /> * ``` */ var default_1 = /** @class */ (function (_super) { tslib_1.__extends(default_1, _super); function default_1(props) { var _this = _super.call(this, props) || this; _this._initialPropsState = {}; _this._categorizedProps = []; _this.resetState = function () { return _this.setState({ propsState: _this._initialPropsState }); }; _this.remountComponent = function () { _this.setState({ forceRemount: _this.state.forceRemount + 1 }); }; _this.prepareComponentProps = function (props) { return typeof props === 'function' ? props( // setState function (componentProps) { return _this.setState({ propsState: tslib_1.__assign(tslib_1.__assign({}, _this.state.propsState), componentProps), }); }, // getState function () { return _this.state.propsState || {}; }) : props; }; _this.setProp = function (key, value) { var _a; if (value === no_value_type_1.default) { // eslint-disable-next-line no-unused-vars var _b = _this.state.propsState, _c = key, deletedKey = _b[_c], propsState = tslib_1.__rest(_b, [typeof _c === "symbol" ? _c : _c + ""]); _this.setState({ propsState: propsState }); } else { _this.setState({ propsState: tslib_1.__assign(tslib_1.__assign({}, _this.state.propsState), (_a = {}, _a[key] = value, _a)) }); } }; _this.propControllers = [ { types: ['func', /event/, /\) => void$/], controller: function (_a) { var propKey = _a.propKey; var classNames = styles_scss_1.default.example; if (_this.state.funcAnimate[propKey]) { classNames += " ".concat(styles_scss_1.default.active); setTimeout(function () { var _a; return _this.setState({ funcAnimate: tslib_1.__assign(tslib_1.__assign({}, _this.state.funcAnimate), (_a = {}, _a[propKey] = false, _a)), }); }, 2000); } if (_this.props.exampleProps[propKey]) { return (react_1.default.createElement("div", { className: classNames }, _this.state.funcValues[propKey] || 'Interaction preview')); } }, }, { types: ['bool', 'Boolean'], controller: function () { return react_1.default.createElement(components_1.Toggle, null); }, }, { types: ['enum'], controller: function (_a) { var type = _a.type; return type && typeof type.value === 'string' ? (react_1.default.createElement(components_1.Input, null)) : (react_1.default.createElement(components_1.List, { values: type.value.map(function (_a) { var value = _a.value; return (0, strip_quotes_1.default)(value); }) })); }, }, { types: ['string', /ReactText/, 'arrayOf', 'union', 'node', 'ReactNode'], controller: function () { return react_1.default.createElement(components_1.Input, null); }, }, { types: ['number'], controller: function () { return react_1.default.createElement(components_1.NumberInput, null); }, }, ]; _this.getPropControlComponent = function (propKey, type) { if (type === void 0) { type = {}; } if (!(0, match_func_prop_1.default)(type.name) && _this.props.exampleProps[propKey]) { return react_1.default.createElement(components_1.List, { values: _this.props.exampleProps[propKey] }); } var propControllerCandidate = _this.propControllers.find(function (_a) { var types = _a.types; return types.some(function (t) { return (0, ensure_regexp_1.default)(t).test(type.name); }); }); return propControllerCandidate && propControllerCandidate.controller ? (propControllerCandidate.controller({ propKey: propKey, type: type })) : (react_1.default.createElement(components_1.Input, null)); }; _this.renderPropControllers = function (_a) { var props = _a.props, allProps = _a.allProps; return Object.entries(props) .filter(function (_a) { var prop = _a[1]; return prop; }) .map(function (_a) { var key = _a[0], prop = _a[1]; return (react_1.default.createElement(components_1.Option, tslib_1.__assign({ key: key }, { label: key, value: allProps[key], defaultValue: typeof _this.props.componentProps === 'function' ? undefined : _this.props.componentProps[key], isRequired: prop.required || false, onChange: function (value) { return _this.setProp(key, value); }, children: _this.getPropControlComponent(key, prop.type), }))); }); }; _this.propsCategories = { primary: { title: 'Primary Props', order: 0, isOpen: true, matcher: function (name) { // primary props are all those set in componentProps and exampleProps // except for callback (starts with `on`) and data attributes (starts // with `data`, including data-hook or dataHook) return Object.keys(tslib_1.__assign(tslib_1.__assign({}, _this.props.exampleProps), _this.preparedComponentProps)) .filter(function (n) { return !['on', 'data'].some(function (i) { return n.startsWith(i); }); }) .some(function (propName) { return propName === name; }); }, }, events: { title: 'Callback Props', order: 1, matcher: function (name) { return name.toLowerCase().startsWith('on'); }, }, html: { title: 'HTML Props', order: 3, matcher: function (name) { return html_props_list_json_1.default.some(function (i) { return name === i; }); }, }, accessibility: { title: 'Accessibility Props', order: 4, matcher: function (name) { return name.toLowerCase().startsWith('aria'); }, }, other: { // miscellaneous props are everything that doesn't fit in other categories title: 'Misc. Props', order: 5, matcher: function () { return true; }, }, }; _this.parsedComponent = props.parsedSource; _this.preparedComponentProps = _this.prepareComponentProps(_this.props.componentProps); _this.state = { propsState: tslib_1.__assign(tslib_1.__assign({}, (_this.props.component.defaultProps || {})), _this.preparedComponentProps), funcValues: {}, funcAnimate: {}, isRtl: false, isDarkBackground: false, forceRemount: 0, }; _this._initialPropsState = _this.state.propsState; _this._categorizedProps = Object.entries((0, categorize_props_1.default)(tslib_1.__assign(tslib_1.__assign({}, _this.preparedComponentProps), _this.parsedComponent.props), _this.propsCategories)) .map(function (_a) { var category = _a[1]; return category; }) .sort(function (_a, _b) { var _c = _a.order, aOrder = _c === void 0 ? -1 : _c; var _d = _b.order, bOrder = _d === void 0 ? -1 : _d; return aOrder - bOrder; }); return _this; } default_1.prototype.componentWillReceiveProps = function (nextProps) { this.setState({ propsState: tslib_1.__assign(tslib_1.__assign({}, this.state.propsState), this.prepareComponentProps(nextProps.componentProps)), }); }; default_1.prototype.render = function () { var _this = this; var functionExampleProps = Object.keys(this.props.exampleProps).filter(function (prop) { return _this.parsedComponent.props[prop] && (0, match_func_prop_1.default)(_this.parsedComponent.props[prop].type.name); }); var componentProps = tslib_1.__assign(tslib_1.__assign({}, this.state.propsState), functionExampleProps.reduce(function (acc, prop) { acc[prop] = function () { var _a, _b, _c, _d; var rest = []; for (var _i = 0; _i < arguments.length; _i++) { rest[_i] = arguments[_i]; } if (_this.state.propsState[prop]) { (_a = _this.state.propsState)[prop].apply(_a, rest); } _this.setState({ funcValues: tslib_1.__assign(tslib_1.__assign({}, _this.state.funcValues), (_b = {}, _b[prop] = (_c = _this.props.exampleProps)[prop].apply(_c, rest), _b)), funcAnimate: tslib_1.__assign(tslib_1.__assign({}, _this.state.funcAnimate), (_d = {}, _d[prop] = true, _d)), }); }; return acc; }, {})); var codeProps = tslib_1.__assign(tslib_1.__assign({}, (0, omit_1.default)(this.state.propsState)(function (key) { return key.startsWith('data'); })), functionExampleProps.reduce(function (acc, key) { acc[key] = _this.props.exampleProps[key]; return acc; }, {})); var component = react_1.default.createElement(this.props.component, componentProps); var componentToRender = this.props.componentWrapper ? react_1.default.cloneElement(this.props.componentWrapper({ component: component, metadata: this.props.parsedSource, }), { 'data-hook': 'componentWrapper', }) : component; if (!this.props.isInteractive) { return componentToRender; } return (react_1.default.createElement(Layout_1.Layout, { dataHook: "auto-example" }, react_1.default.createElement(Layout_1.Cell, { span: 6 }, this._categorizedProps.reduce(function (components, _a, i) { var title = _a.title, isOpen = _a.isOpen, props = _a.props; var renderablePropControllers = _this.renderPropControllers({ props: props, allProps: componentProps, // TODO: ideally this should not be here }).filter(function (_a) { var children = _a.props.children; return children; }); return renderablePropControllers.length ? components.concat(react_1.default.createElement(section_collapse_1.default, { key: title, title: title, isOpen: isOpen || i === 0, children: renderablePropControllers, })) : components; }, [])), react_1.default.createElement(components_1.Preview, { key: this.state.forceRemount, isRtl: this.state.isRtl, isDarkBackground: this.state.isDarkBackground, onToggleRtl: function (isRtl) { return _this.setState({ isRtl: isRtl }); }, onToggleBackground: function (isDarkBackground) { return _this.setState({ isDarkBackground: isDarkBackground }); }, onRemountComponent: this.remountComponent, children: componentToRender }), this.props.codeExample && (react_1.default.createElement(components_1.Code, { dataHook: "metadata-codeblock", component: react_1.default.createElement(this.props.component, codeProps) })))); }; default_1.displayName = 'AutoExample'; default_1.propTypes = { /** * parsed meta object about component. * * Generated by `react-autodocs-utils` * */ parsedSource: prop_types_1.default.object, /** reference to react component */ component: prop_types_1.default.func.isRequired, /** * control default props and their state of component in preview. * * can be either `object` or `function`: * * * `object` - simple javascript object which reflects `component` properties. * * `function` - `(setProps, getProps) => props` * receives `setProps` setter and `getProps` getter. can be used to persist props state and react to event * handlers and must return an object which will be used as new props. For example: * * ```js * <AutoExample * component={ToggleSwitch} * componentProps={(setProps, getProps) => ({ * checked: false, * onChange: () => setProps({ checked: !getProps().checked }) * })} * ``` */ componentProps: prop_types_1.default.oneOfType([prop_types_1.default.func, prop_types_1.default.object]), /** A render function for the component (in the Preview). Typicaly this function can wrap the component in something usefull like a className which is needed. ({component}) => JSXElement */ componentWrapper: prop_types_1.default.func, exampleProps: prop_types_1.default.object, /** when true, display only component preview without interactive props nor code example */ isInteractive: prop_types_1.default.bool, /** currently only `false` possible. later same property shall be used for configuring code example */ codeExample: prop_types_1.default.bool, }; default_1.defaultProps = { source: '', component: function () { return null; }, componentProps: {}, parsedSource: {}, exampleProps: {}, isInteractive: true, codeExample: true, }; return default_1; }(react_1.Component)); exports.default = default_1; //# sourceMappingURL=index.js.map