wix-storybook-utils
Version:
Utilities for automated component documentation within Storybook
330 lines • 16.7 kB
JavaScript
;
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