mirror of
https://github.com/storybookjs/storybook.git
synced 2025-04-08 11:11:53 +08:00
Merge branch 'next' into pr/rafaelfbs/6347
This commit is contained in:
commit
b327281569
33
.babelrc.js
33
.babelrc.js
@ -1,21 +1,34 @@
|
||||
module.exports = {
|
||||
presets: [
|
||||
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage' }],
|
||||
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage', corejs: '2' }],
|
||||
'@babel/preset-typescript',
|
||||
'@babel/preset-react',
|
||||
'@babel/preset-flow',
|
||||
],
|
||||
plugins: [
|
||||
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
|
||||
[
|
||||
'@babel/plugin-proposal-decorators',
|
||||
{
|
||||
legacy: true,
|
||||
},
|
||||
],
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
'@babel/plugin-syntax-dynamic-import',
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
['@babel/plugin-proposal-object-rest-spread', { loose: true, useBuiltIns: true }],
|
||||
'babel-plugin-macros',
|
||||
['emotion', { sourceMap: true, autoLabel: true }],
|
||||
],
|
||||
env: {
|
||||
test: {
|
||||
presets: [['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage' }]],
|
||||
plugins: ['babel-plugin-require-context-hook', 'babel-plugin-dynamic-import-node'],
|
||||
presets: [
|
||||
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage', corejs: '2' }],
|
||||
],
|
||||
plugins: [
|
||||
'babel-plugin-require-context-hook',
|
||||
'babel-plugin-dynamic-import-node',
|
||||
'@babel/plugin-transform-runtime',
|
||||
],
|
||||
},
|
||||
},
|
||||
overrides: [
|
||||
@ -26,7 +39,7 @@ module.exports = {
|
||||
{
|
||||
test: './lib',
|
||||
presets: [
|
||||
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage' }],
|
||||
['@babel/preset-env', { shippedProposals: true, useBuiltIns: 'usage', corejs: '2' }],
|
||||
'@babel/preset-react',
|
||||
],
|
||||
plugins: [
|
||||
@ -59,9 +72,17 @@ module.exports = {
|
||||
targets: {
|
||||
node: '8.11',
|
||||
},
|
||||
corejs: '2',
|
||||
},
|
||||
],
|
||||
],
|
||||
plugins: [
|
||||
'emotion',
|
||||
'babel-plugin-macros',
|
||||
['@babel/plugin-proposal-class-properties', { loose: true }],
|
||||
'@babel/plugin-proposal-object-rest-spread',
|
||||
'@babel/plugin-proposal-export-default-from',
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -97,7 +97,14 @@ module.exports = {
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/__tests__/**', '**/*.test.js', '**/*.stories.js', '**/storyshots/**/stories/**'],
|
||||
files: [
|
||||
'**/__tests__/**',
|
||||
'**/*.test.js',
|
||||
'**/*.stories.js',
|
||||
'**/storyshots/**/stories/**',
|
||||
'docs/src/new-components/lib/StoryLinkWrapper.js',
|
||||
'docs/src/stories/**',
|
||||
],
|
||||
rules: {
|
||||
'import/no-extraneous-dependencies': ignore,
|
||||
},
|
||||
|
2
.github/CODEOWNERS
vendored
2
.github/CODEOWNERS
vendored
@ -2,7 +2,7 @@
|
||||
.teamcity/ @hypnosphi
|
||||
.github/ @danielduan
|
||||
|
||||
/addons/a11y/ @jbovenschen
|
||||
/addons/a11y/ @jbovenschen @codebyalex
|
||||
/addons/actions/ @rhalff
|
||||
/addons/backgrounds/ @ndelangen
|
||||
/addons/centered/ @kazupon
|
||||
|
8109
CHANGELOG.md
8109
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-a11y",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "a11y addon for storybook",
|
||||
"keywords": [
|
||||
"a11y",
|
||||
@ -26,16 +26,17 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/api": "5.1.0-alpha.20",
|
||||
"@storybook/client-logger": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/api": "5.1.0-alpha.24",
|
||||
"@storybook/client-logger": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"axe-core": "^3.2.2",
|
||||
"common-tags": "^1.8.0",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
"hoist-non-react-statics": "^3.3.0",
|
||||
"memoizerific": "^1.11.3",
|
||||
"react": "^16.8.4",
|
||||
"react-redux": "^6.0.1",
|
||||
@ -44,7 +45,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/common-tags": "^1.8.0",
|
||||
"@types/react-redux": "^7.0.3"
|
||||
"@types/react-redux": "^7.0.6"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
|
@ -26,6 +26,7 @@ const axeResult = {
|
||||
'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
|
||||
help: 'Elements must have sufficient color contrast',
|
||||
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
|
||||
nodes: [],
|
||||
},
|
||||
],
|
||||
passes: [
|
||||
@ -36,6 +37,7 @@ const axeResult = {
|
||||
description: "Ensures ARIA attributes are allowed for an element's role",
|
||||
help: 'Elements must only use allowed ARIA attributes',
|
||||
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/aria-allowed-attr?application=axeAPI',
|
||||
nodes: [],
|
||||
},
|
||||
],
|
||||
violations: [
|
||||
@ -47,6 +49,7 @@ const axeResult = {
|
||||
'Ensures the contrast between foreground and background colors meets WCAG 2 AA contrast ratio thresholds',
|
||||
help: 'Elements must have sufficient color contrast',
|
||||
helpUrl: 'https://dequeuniversity.com/rules/axe/3.2/color-contrast?application=axeAPI',
|
||||
nodes: [],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
@ -91,7 +91,7 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
|
||||
|
||||
if (!prevProps.active && active) {
|
||||
// removes all elements from the redux map in store from the previous panel
|
||||
store.dispatch(clearElements(null));
|
||||
store.dispatch(clearElements());
|
||||
this.request();
|
||||
}
|
||||
}
|
||||
@ -134,7 +134,7 @@ export class A11YPanel extends Component<A11YPanelProps, A11YPanelState> {
|
||||
() => {
|
||||
api.emit(EVENTS.REQUEST);
|
||||
// removes all elements from the redux map in store from the previous panel
|
||||
store.dispatch(clearElements(null));
|
||||
store.dispatch(clearElements());
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -23,7 +23,7 @@ const HighlightToggleElement = styled.span({
|
||||
fontWeight: 'normal',
|
||||
float: 'right',
|
||||
paddingRight: '15px',
|
||||
input: { margin: 0, },
|
||||
input: { margin: 0 },
|
||||
});
|
||||
|
||||
interface ElementProps {
|
||||
@ -43,7 +43,12 @@ const Element: FunctionComponent<ElementProps> = ({ element, passes, type }) =>
|
||||
<ItemTitle>
|
||||
{element.target[0]}
|
||||
<HighlightToggleElement>
|
||||
<HighlightToggle toggleId={highlightToggleId} type={type} elementsToHighlight={[element]} label={highlightLabel} />
|
||||
<HighlightToggle
|
||||
toggleId={highlightToggleId}
|
||||
type={type}
|
||||
elementsToHighlight={[element]}
|
||||
label={highlightLabel}
|
||||
/>
|
||||
</HighlightToggleElement>
|
||||
</ItemTitle>
|
||||
<Rules rules={rules} passes={passes} />
|
||||
|
@ -1,23 +1,44 @@
|
||||
import React, { Component, Fragment } from 'react';
|
||||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import { styled, themes, convert } from '@storybook/theming';
|
||||
import memoize from 'memoizerific';
|
||||
|
||||
import { NodeResult } from 'axe-core';
|
||||
import { Rules } from './Rules';
|
||||
import { RuleType } from '../A11YPanel';
|
||||
import { addElement } from '../../redux-config';
|
||||
import { IFRAME } from '../../constants';
|
||||
|
||||
const Checkbox = styled.input({
|
||||
cursor: 'pointer',
|
||||
});
|
||||
|
||||
export class HighlightedElementData {
|
||||
originalOutline: string;
|
||||
isHighlighted: boolean;
|
||||
}
|
||||
|
||||
interface ToggleProps {
|
||||
elementsToHighlight: NodeResult[];
|
||||
type: RuleType;
|
||||
addElement?: (data: any) => void;
|
||||
highlightedElementsMap?: Map<HTMLElement, HighlightedElementData>;
|
||||
isToggledOn?: boolean;
|
||||
toggleId?: string;
|
||||
indeterminate?: boolean;
|
||||
}
|
||||
|
||||
enum CheckBoxStates {
|
||||
CHECKED,
|
||||
UNCHECKED,
|
||||
INDETERMINATE,
|
||||
}
|
||||
|
||||
const Checkbox = styled.input(({ disabled }) => ({
|
||||
cursor: disabled ? 'not-allowed' : 'pointer',
|
||||
}));
|
||||
|
||||
const colorsByType = [
|
||||
convert(themes.normal).color.negative, // VIOLATION,
|
||||
convert(themes.normal).color.positive, // PASS,
|
||||
convert(themes.normal).color.warning, // INCOMPLETION,
|
||||
];
|
||||
|
||||
const getIframe = memoize(1)(() => document.getElementsByTagName(IFRAME)[0]);
|
||||
|
||||
function getElementBySelectorPath(elementPath: string): HTMLElement {
|
||||
@ -28,106 +49,92 @@ function getElementBySelectorPath(elementPath: string): HTMLElement {
|
||||
return null;
|
||||
}
|
||||
|
||||
function areAllRequiredElementsHiglighted(
|
||||
elementsToHighlight: NodeResult[],
|
||||
highlightedElementsMap: Map<HTMLElement, HighlightedElementData>
|
||||
): boolean {
|
||||
let elementsInMapExist = false;
|
||||
if (elementsToHighlight) {
|
||||
for (let element of elementsToHighlight) {
|
||||
if (element) {
|
||||
const targetElement = getElementBySelectorPath(element.target[0]);
|
||||
if (highlightedElementsMap.get(targetElement)) {
|
||||
elementsInMapExist = true;
|
||||
if (!highlightedElementsMap.get(targetElement).isHighlighted) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return elementsInMapExist;
|
||||
function setElementOutlineStyle(targetElement: HTMLElement, outlineStyle: string): void {
|
||||
targetElement.style.outline = outlineStyle;
|
||||
}
|
||||
|
||||
interface ToggleProps {
|
||||
elementsToHighlight: NodeResult[];
|
||||
type: RuleType;
|
||||
addElement?: (data: any) => void;
|
||||
highlightedElementsMap?: Map<HTMLElement, HighlightedElementData>;
|
||||
isToggledOn?: boolean;
|
||||
toggleId?: string;
|
||||
function areAllRequiredElementsHighlighted(
|
||||
elementsToHighlight: NodeResult[],
|
||||
highlightedElementsMap: Map<HTMLElement, HighlightedElementData>
|
||||
): CheckBoxStates {
|
||||
const highlightedCount = elementsToHighlight.filter(item => {
|
||||
const targetElement = getElementBySelectorPath(item.target[0]);
|
||||
return (
|
||||
highlightedElementsMap.has(targetElement) &&
|
||||
highlightedElementsMap.get(targetElement).isHighlighted
|
||||
);
|
||||
}).length;
|
||||
|
||||
return highlightedCount === 0
|
||||
? CheckBoxStates.UNCHECKED
|
||||
: highlightedCount === elementsToHighlight.length
|
||||
? CheckBoxStates.CHECKED
|
||||
: CheckBoxStates.INDETERMINATE;
|
||||
}
|
||||
|
||||
function mapDispatchToProps(dispatch: any) {
|
||||
return {
|
||||
addElement: (data: any) => dispatch(addElement(data)),
|
||||
addElement: (data: { element: HTMLElement; data: HighlightedElementData }) =>
|
||||
dispatch(addElement(data)),
|
||||
};
|
||||
}
|
||||
|
||||
const mapStateToProps = (state: any, ownProps: any) => {
|
||||
const isToggledOn = areAllRequiredElementsHiglighted(
|
||||
ownProps.elementsToHighlight,
|
||||
const checkBoxState = areAllRequiredElementsHighlighted(
|
||||
ownProps.elementsToHighlight || [],
|
||||
state.highlightedElementsMap
|
||||
);
|
||||
return {
|
||||
highlightedElementsMap: state.highlightedElementsMap,
|
||||
isToggledOn,
|
||||
isToggledOn: checkBoxState === CheckBoxStates.CHECKED,
|
||||
indeterminate: checkBoxState === CheckBoxStates.INDETERMINATE,
|
||||
};
|
||||
};
|
||||
|
||||
class HighlightToggle extends Component<ToggleProps, {}> {
|
||||
class HighlightToggle extends Component<ToggleProps> {
|
||||
static defaultProps: Partial<ToggleProps> = {
|
||||
elementsToHighlight: [],
|
||||
};
|
||||
|
||||
private checkBoxRef = React.createRef<HTMLInputElement>();
|
||||
|
||||
componentDidMount() {
|
||||
for (let element of this.props.elementsToHighlight) {
|
||||
if (element) {
|
||||
const targetElement = getElementBySelectorPath(element.target[0]);
|
||||
if (targetElement && !this.props.highlightedElementsMap.get(targetElement)) {
|
||||
this.saveElementDataToMap(
|
||||
targetElement,
|
||||
false,
|
||||
targetElement.style.outline,
|
||||
this.props.type
|
||||
);
|
||||
}
|
||||
this.props.elementsToHighlight.forEach(element => {
|
||||
const targetElement = getElementBySelectorPath(element.target[0]);
|
||||
if (targetElement && !this.props.highlightedElementsMap.has(targetElement)) {
|
||||
this.saveElementDataToMap(targetElement, false, targetElement.style.outline);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps: Readonly<ToggleProps>): void {
|
||||
if (this.checkBoxRef.current) {
|
||||
this.checkBoxRef.current.indeterminate = this.props.indeterminate;
|
||||
}
|
||||
}
|
||||
|
||||
higlightRuleLocation(targetElement: HTMLElement, addHighlight: boolean): void {
|
||||
const OUTLINE_STYLE = `dotted`;
|
||||
const OUTLINE_WIDTH = `1px`;
|
||||
if (targetElement) {
|
||||
if (addHighlight) {
|
||||
switch (this.props.type) {
|
||||
case RuleType.PASS:
|
||||
this.setTargetElementOutlineStyle(targetElement, `${convert(themes.normal).color.positive} ${OUTLINE_STYLE} ${OUTLINE_WIDTH}`);
|
||||
break;
|
||||
case RuleType.VIOLATION:
|
||||
this.setTargetElementOutlineStyle(targetElement, `${convert(themes.normal).color.negative} ${OUTLINE_STYLE} ${OUTLINE_WIDTH}`);
|
||||
break;
|
||||
case RuleType.INCOMPLETION:
|
||||
this.setTargetElementOutlineStyle(targetElement, `${convert(themes.normal).color.warning} ${OUTLINE_STYLE} ${OUTLINE_WIDTH}`);
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (this.props.highlightedElementsMap.get(targetElement)) {
|
||||
this.setTargetElementOutlineStyle(
|
||||
targetElement,
|
||||
this.props.highlightedElementsMap.get(targetElement).originalOutline
|
||||
);
|
||||
}
|
||||
}
|
||||
highlightRuleLocation(targetElement: HTMLElement, addHighlight: boolean): void {
|
||||
if (!targetElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (addHighlight) {
|
||||
setElementOutlineStyle(targetElement, `${colorsByType[this.props.type]} dotted 1px`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.props.highlightedElementsMap.has(targetElement)) {
|
||||
setElementOutlineStyle(
|
||||
targetElement,
|
||||
this.props.highlightedElementsMap.get(targetElement).originalOutline
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
saveElementDataToMap(
|
||||
targetElement: HTMLElement,
|
||||
isHighlighted: boolean,
|
||||
originalOutline: string,
|
||||
ruleTypeState: RuleType
|
||||
originalOutline: string
|
||||
): void {
|
||||
const data: HighlightedElementData = new HighlightedElementData();
|
||||
data.isHighlighted = isHighlighted;
|
||||
@ -136,42 +143,32 @@ class HighlightToggle extends Component<ToggleProps, {}> {
|
||||
this.props.addElement(payload);
|
||||
}
|
||||
|
||||
setTargetElementOutlineStyle(targetElement: HTMLElement, outlineStyle: string): void {
|
||||
targetElement.style.outline = outlineStyle;
|
||||
}
|
||||
|
||||
onToggle(): void {
|
||||
for (let element of this.props.elementsToHighlight) {
|
||||
if (element) {
|
||||
const targetElement = getElementBySelectorPath(element.target[0]);
|
||||
if (this.props.highlightedElementsMap.get(targetElement)) {
|
||||
let originalOutline = this.props.highlightedElementsMap.get(targetElement).originalOutline;
|
||||
if (
|
||||
this.props.isToggledOn &&
|
||||
this.props.highlightedElementsMap.get(targetElement).isHighlighted
|
||||
) {
|
||||
this.higlightRuleLocation(targetElement, false);
|
||||
this.saveElementDataToMap(targetElement, false, originalOutline, this.props.type);
|
||||
} else if (
|
||||
!this.props.isToggledOn &&
|
||||
!this.props.highlightedElementsMap.get(targetElement).isHighlighted
|
||||
) {
|
||||
this.higlightRuleLocation(targetElement, true);
|
||||
this.saveElementDataToMap(targetElement, true, originalOutline, this.props.type);
|
||||
}
|
||||
}
|
||||
onToggle = (): void => {
|
||||
this.props.elementsToHighlight.forEach(element => {
|
||||
const targetElement = getElementBySelectorPath(element.target[0]);
|
||||
if (!this.props.highlightedElementsMap.has(targetElement)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
const originalOutline = this.props.highlightedElementsMap.get(targetElement).originalOutline;
|
||||
const { isHighlighted } = this.props.highlightedElementsMap.get(targetElement);
|
||||
const { isToggledOn } = this.props;
|
||||
if ((isToggledOn && isHighlighted) || (!isToggledOn && !isHighlighted)) {
|
||||
const addHighlight = !isToggledOn && !isHighlighted;
|
||||
this.highlightRuleLocation(targetElement, addHighlight);
|
||||
this.saveElementDataToMap(targetElement, addHighlight, originalOutline);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
return (
|
||||
<Checkbox
|
||||
ref={this.checkBoxRef}
|
||||
id={this.props.toggleId}
|
||||
type="checkbox"
|
||||
aria-label="Highlight result"
|
||||
disabled={this.props.elementsToHighlight && this.props.elementsToHighlight.length === 0 ? true : false}
|
||||
onChange={() => this.onToggle()}
|
||||
disabled={!this.props.elementsToHighlight.length}
|
||||
onChange={this.onToggle}
|
||||
checked={this.props.isToggledOn}
|
||||
/>
|
||||
);
|
||||
|
@ -51,7 +51,7 @@ const HighlightToggleElement = styled.span({
|
||||
marginRight: '15px',
|
||||
marginTop: '10px',
|
||||
|
||||
input: { margin: 0, },
|
||||
input: { margin: 0 },
|
||||
});
|
||||
|
||||
interface ItemProps {
|
||||
@ -94,7 +94,11 @@ export class Item extends Component<ItemProps, ItemState> {
|
||||
{item.description}
|
||||
</HeaderBar>
|
||||
<HighlightToggleElement>
|
||||
<HighlightToggle toggleId={highlightToggleId} type={type} elementsToHighlight={item ? item.nodes : null} />
|
||||
<HighlightToggle
|
||||
toggleId={highlightToggleId}
|
||||
type={type}
|
||||
elementsToHighlight={item ? item.nodes : null}
|
||||
/>
|
||||
</HighlightToggleElement>
|
||||
</Wrapper>
|
||||
{open ? (
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
exports[`HighlightToggle component should match snapshot 1`] = `
|
||||
.emotion-0 {
|
||||
cursor: pointer;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
<Provider
|
||||
@ -78,25 +78,13 @@ exports[`HighlightToggle component should match snapshot 1`] = `
|
||||
"toString": [Function],
|
||||
},
|
||||
"hoverable": Object {
|
||||
"map": undefined,
|
||||
"name": "wpaw6f",
|
||||
"next": undefined,
|
||||
"styles": "
|
||||
transition: all 150ms ease-out;
|
||||
transform: translate3d(0, 0, 0);
|
||||
|
||||
&:hover {
|
||||
transform: translate3d(0, -2px, 0);
|
||||
}
|
||||
|
||||
&:active {
|
||||
transform: translate3d(0, 0, 0);
|
||||
}
|
||||
",
|
||||
"map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9hbmltYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBd0NxQiIsImZpbGUiOiIuLi9zcmMvYW5pbWF0aW9uLnRzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcblxuZXhwb3J0IGNvbnN0IGVhc2luZyA9IHtcbiAgcnViYmVyOiAnY3ViaWMtYmV6aWVyKDAuMTc1LCAwLjg4NSwgMC4zMzUsIDEuMDUpJyxcbn07XG5cbmNvbnN0IHJvdGF0ZTM2MCA9IGtleWZyYW1lc2Bcblx0ZnJvbSB7XG5cdFx0dHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG5cdH1cblx0dG8ge1xuXHRcdHRyYW5zZm9ybTogcm90YXRlKDM2MGRlZyk7XG5cdH1cbmA7XG5cbmNvbnN0IGdsb3cgPSBrZXlmcmFtZXNgXG4gIDAlLCAxMDAlIHsgb3BhY2l0eTogMTsgfVxuICA1MCUgeyBvcGFjaXR5OiAuNDsgfVxuYDtcblxuY29uc3QgZmxvYXQgPSBrZXlmcmFtZXNgXG4gIDAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDFweCk7IH1cbiAgMjUlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDBweCk7IH1cbiAgNTAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKC0zcHgpOyB9XG4gIDEwMCUgeyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoMXB4KTsgfVxuYDtcblxuY29uc3QgamlnZ2xlID0ga2V5ZnJhbWVzYFxuICAwJSwgMTAwJSB7IHRyYW5zZm9ybTp0cmFuc2xhdGUzZCgwLDAsMCk7IH1cbiAgMTIuNSUsIDYyLjUlIHsgdHJhbnNmb3JtOnRyYW5zbGF0ZTNkKC00cHgsMCwwKTsgfVxuICAzNy41JSwgODcuNSUgeyAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCg0cHgsMCwwKTsgIH1cbmA7XG5cbmNvbnN0IGlubGluZUdsb3cgPSBjc3NgXG4gIGFuaW1hdGlvbjogJHtnbG93fSAxLjVzIGVhc2UtaW4tb3V0IGluZmluaXRlO1xuICBjb2xvcjogdHJhbnNwYXJlbnQ7XG4gIGN1cnNvcjogcHJvZ3Jlc3M7XG5gO1xuXG4vLyBob3ZlciAmIGFjdGl2ZSBzdGF0ZSBmb3IgbGlua3MgYW5kIGJ1dHRvbnNcbmNvbnN0IGhvdmVyYWJsZSA9IGNzc2BcbiAgdHJhbnNpdGlvbjogYWxsIDE1MG1zIGVhc2Utb3V0O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIDAsIDApO1xuXG4gICY6aG92ZXIge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoMCwgLTJweCwgMCk7XG4gIH1cblxuICAmOmFjdGl2ZSB7XG4gICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCgwLCAwLCAwKTtcbiAgfVxuYDtcblxuZXhwb3J0IGNvbnN0IGFuaW1hdGlvbiA9IHtcbiAgcm90YXRlMzYwLFxuICBnbG93LFxuICBmbG9hdCxcbiAgamlnZ2xlLFxuICBpbmxpbmVHbG93LFxuICBob3ZlcmFibGUsXG59O1xuIl19 */",
|
||||
"name": "1023qba-hoverable",
|
||||
"styles": "transition:all 150ms ease-out;transform:translate3d(0,0,0);&:hover{transform:translate3d(0,-2px,0);}&:active{transform:translate3d(0,0,0);}label:hoverable;",
|
||||
},
|
||||
"inlineGlow": Object {
|
||||
"map": undefined,
|
||||
"name": "zv3h0s",
|
||||
"map": "/*# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIi4uL3NyYy9hbmltYXRpb24udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBaUNzQiIsImZpbGUiOiIuLi9zcmMvYW5pbWF0aW9uLnRzIiwic291cmNlc0NvbnRlbnQiOlsiaW1wb3J0IHsgY3NzLCBrZXlmcmFtZXMgfSBmcm9tICdAZW1vdGlvbi9jb3JlJztcblxuZXhwb3J0IGNvbnN0IGVhc2luZyA9IHtcbiAgcnViYmVyOiAnY3ViaWMtYmV6aWVyKDAuMTc1LCAwLjg4NSwgMC4zMzUsIDEuMDUpJyxcbn07XG5cbmNvbnN0IHJvdGF0ZTM2MCA9IGtleWZyYW1lc2Bcblx0ZnJvbSB7XG5cdFx0dHJhbnNmb3JtOiByb3RhdGUoMGRlZyk7XG5cdH1cblx0dG8ge1xuXHRcdHRyYW5zZm9ybTogcm90YXRlKDM2MGRlZyk7XG5cdH1cbmA7XG5cbmNvbnN0IGdsb3cgPSBrZXlmcmFtZXNgXG4gIDAlLCAxMDAlIHsgb3BhY2l0eTogMTsgfVxuICA1MCUgeyBvcGFjaXR5OiAuNDsgfVxuYDtcblxuY29uc3QgZmxvYXQgPSBrZXlmcmFtZXNgXG4gIDAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDFweCk7IH1cbiAgMjUlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKDBweCk7IH1cbiAgNTAlIHsgdHJhbnNmb3JtOiB0cmFuc2xhdGVZKC0zcHgpOyB9XG4gIDEwMCUgeyB0cmFuc2Zvcm06IHRyYW5zbGF0ZVkoMXB4KTsgfVxuYDtcblxuY29uc3QgamlnZ2xlID0ga2V5ZnJhbWVzYFxuICAwJSwgMTAwJSB7IHRyYW5zZm9ybTp0cmFuc2xhdGUzZCgwLDAsMCk7IH1cbiAgMTIuNSUsIDYyLjUlIHsgdHJhbnNmb3JtOnRyYW5zbGF0ZTNkKC00cHgsMCwwKTsgfVxuICAzNy41JSwgODcuNSUgeyAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCg0cHgsMCwwKTsgIH1cbmA7XG5cbmNvbnN0IGlubGluZUdsb3cgPSBjc3NgXG4gIGFuaW1hdGlvbjogJHtnbG93fSAxLjVzIGVhc2UtaW4tb3V0IGluZmluaXRlO1xuICBjb2xvcjogdHJhbnNwYXJlbnQ7XG4gIGN1cnNvcjogcHJvZ3Jlc3M7XG5gO1xuXG4vLyBob3ZlciAmIGFjdGl2ZSBzdGF0ZSBmb3IgbGlua3MgYW5kIGJ1dHRvbnNcbmNvbnN0IGhvdmVyYWJsZSA9IGNzc2BcbiAgdHJhbnNpdGlvbjogYWxsIDE1MG1zIGVhc2Utb3V0O1xuICB0cmFuc2Zvcm06IHRyYW5zbGF0ZTNkKDAsIDAsIDApO1xuXG4gICY6aG92ZXIge1xuICAgIHRyYW5zZm9ybTogdHJhbnNsYXRlM2QoMCwgLTJweCwgMCk7XG4gIH1cblxuICAmOmFjdGl2ZSB7XG4gICAgdHJhbnNmb3JtOiB0cmFuc2xhdGUzZCgwLCAwLCAwKTtcbiAgfVxuYDtcblxuZXhwb3J0IGNvbnN0IGFuaW1hdGlvbiA9IHtcbiAgcm90YXRlMzYwLFxuICBnbG93LFxuICBmbG9hdCxcbiAgamlnZ2xlLFxuICBpbmxpbmVHbG93LFxuICBob3ZlcmFibGUsXG59O1xuIl19 */",
|
||||
"name": "1euta6d-inlineGlow",
|
||||
"next": Object {
|
||||
"name": "animation-r0iffl",
|
||||
"next": undefined,
|
||||
@ -105,12 +93,7 @@ exports[`HighlightToggle component should match snapshot 1`] = `
|
||||
50% { opacity: .4; }
|
||||
}",
|
||||
},
|
||||
"styles": "
|
||||
animation: animation-r0iffl 1.5s ease-in-out infinite;
|
||||
background: rgba(0,0,0,.1);
|
||||
color: transparent;
|
||||
cursor: progress;
|
||||
",
|
||||
"styles": "animation:animation-r0iffl 1.5s ease-in-out infinite;color:transparent;cursor:progress;label:inlineGlow;",
|
||||
},
|
||||
"jiggle": Object {
|
||||
"anim": 1,
|
||||
@ -338,6 +321,7 @@ exports[`HighlightToggle component should match snapshot 1`] = `
|
||||
addElement={[Function]}
|
||||
elementsToHighlight={Array []}
|
||||
highlightedElementsMap={Map {}}
|
||||
indeterminate={false}
|
||||
isToggledOn={false}
|
||||
>
|
||||
<Styled(input)
|
||||
|
@ -1,10 +1,8 @@
|
||||
import React, { Fragment, FunctionComponent } from 'react';
|
||||
import { Placeholder } from '@storybook/components';
|
||||
import { styled } from '@storybook/theming';
|
||||
import { Result, NodeResult } from 'axe-core';
|
||||
import { Result } from 'axe-core';
|
||||
import { Item } from './Item';
|
||||
import { RuleType } from '../A11YPanel';
|
||||
import HighlightToggle from './HighlightToggle';
|
||||
|
||||
export interface ReportProps {
|
||||
items: Result[];
|
||||
|
@ -1,4 +1,4 @@
|
||||
import React, { Component } from 'react';
|
||||
import React, { Component, SyntheticEvent } from 'react';
|
||||
|
||||
import { styled } from '@storybook/theming';
|
||||
import store, { clearElements } from '../redux-config';
|
||||
@ -71,7 +71,7 @@ const TabsWrapper = styled.div({});
|
||||
|
||||
const List = styled.div(({ theme }) => ({
|
||||
boxShadow: `${theme.appBorderColor} 0 -1px 0 0 inset`,
|
||||
background: 'rgba(0,0,0,.05)',
|
||||
background: 'rgba(0, 0, 0, .05)',
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
whiteSpace: 'nowrap',
|
||||
@ -90,27 +90,23 @@ interface TabsState {
|
||||
active: number;
|
||||
}
|
||||
|
||||
function retrieveAllNodesFromResults(items: Result[]): NodeResult[] {
|
||||
return items.reduce((acc, item) => acc.concat(item.nodes), []);
|
||||
}
|
||||
|
||||
export class Tabs extends Component<TabsProps, TabsState> {
|
||||
state: TabsState = {
|
||||
active: 0,
|
||||
};
|
||||
|
||||
onToggle = (index: number) => {
|
||||
onToggle = (event: SyntheticEvent) => {
|
||||
this.setState({
|
||||
active: index,
|
||||
active: parseInt(event.currentTarget.getAttribute('data-index'), 10),
|
||||
});
|
||||
// removes all elements from the redux map in store from the previous panel
|
||||
store.dispatch(clearElements(null));
|
||||
store.dispatch(clearElements());
|
||||
};
|
||||
|
||||
retrieveAllNodeResults(items: Result[]): NodeResult[] {
|
||||
let nodeArray: NodeResult[] = [];
|
||||
for (const item of items) {
|
||||
nodeArray = nodeArray.concat(item.nodes);
|
||||
}
|
||||
return nodeArray;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tabs } = this.props;
|
||||
const { active } = this.state;
|
||||
@ -120,21 +116,25 @@ export class Tabs extends Component<TabsProps, TabsState> {
|
||||
<Container>
|
||||
<List>
|
||||
<TabsWrapper>
|
||||
{tabs.map((tab, index) => (
|
||||
<Item
|
||||
key={index}
|
||||
active={active === index ? true : undefined}
|
||||
onClick={() => this.onToggle(index)}>
|
||||
{tab.label}
|
||||
</Item>
|
||||
))}
|
||||
{tabs.map((tab, index) => (
|
||||
<Item
|
||||
key={index}
|
||||
data-index={index}
|
||||
active={active === index}
|
||||
onClick={this.onToggle}
|
||||
>
|
||||
{tab.label}
|
||||
</Item>
|
||||
))}
|
||||
</TabsWrapper>
|
||||
<GlobalToggleWrapper>
|
||||
<HighlightToggleLabel htmlFor={highlightToggleId}>{highlightLabel}</HighlightToggleLabel>
|
||||
<HighlightToggleLabel htmlFor={highlightToggleId}>
|
||||
{highlightLabel}
|
||||
</HighlightToggleLabel>
|
||||
<HighlightToggle
|
||||
toggleId={highlightToggleId}
|
||||
type={tabs[active].type}
|
||||
elementsToHighlight={this.retrieveAllNodeResults(tabs[active].items)}
|
||||
elementsToHighlight={retrieveAllNodesFromResults(tabs[active].items)}
|
||||
label={highlightLabel}
|
||||
/>
|
||||
</GlobalToggleWrapper>
|
||||
|
@ -215,7 +215,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
}
|
||||
|
||||
.emotion-8 {
|
||||
cursor: pointer;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.emotion-12 {
|
||||
@ -316,7 +316,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
Symbol(observable): [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
@ -336,16 +336,15 @@ exports[`A11YPanel should render report 1`] = `
|
||||
"110qmus": true,
|
||||
"152wg9i": true,
|
||||
"1551xjo": true,
|
||||
"15paq49": true,
|
||||
"176o2y5": true,
|
||||
"1977chw": true,
|
||||
"1cwfnw4": true,
|
||||
"1fp6daz": true,
|
||||
"1kjdm0k": true,
|
||||
"1l7fvsg": true,
|
||||
"1myfomu": true,
|
||||
"1s6ajii": true,
|
||||
"1vwgrhn": true,
|
||||
"4g6ai3": true,
|
||||
"4ryd4s": true,
|
||||
"6hqipu": true,
|
||||
"animation-u07e3c": true,
|
||||
@ -360,6 +359,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
"qb28": true,
|
||||
"snh8f7": true,
|
||||
"tkevr6": true,
|
||||
"vdhlfv": true,
|
||||
},
|
||||
"key": "css",
|
||||
"nonce": undefined,
|
||||
@ -683,7 +683,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
data-emotion="css"
|
||||
>
|
||||
|
||||
.emotion-8{cursor:pointer;}
|
||||
.emotion-8{cursor:not-allowed;}
|
||||
</style>
|
||||
<style
|
||||
data-emotion="css"
|
||||
@ -911,7 +911,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
data-emotion="css"
|
||||
>
|
||||
|
||||
.emotion-8{cursor:pointer;}
|
||||
.emotion-8{cursor:not-allowed;}
|
||||
</style>,
|
||||
<style
|
||||
data-emotion="css"
|
||||
@ -1110,7 +1110,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
0
|
||||
Violations
|
||||
</ForwardRef(render)>,
|
||||
"panel": <Unknown
|
||||
"panel": <Report
|
||||
empty="No a11y violations found."
|
||||
items={Array []}
|
||||
passes={false}
|
||||
@ -1124,7 +1124,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
0
|
||||
Passes
|
||||
</ForwardRef(render)>,
|
||||
"panel": <Unknown
|
||||
"panel": <Report
|
||||
empty="No a11y check passed."
|
||||
items={Array []}
|
||||
passes={true}
|
||||
@ -1138,7 +1138,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
0
|
||||
Incomplete
|
||||
</ForwardRef(render)>,
|
||||
"panel": <Unknown
|
||||
"panel": <Report
|
||||
empty="No a11y incomplete found."
|
||||
items={Array []}
|
||||
passes={false}
|
||||
@ -1163,11 +1163,13 @@ exports[`A11YPanel should render report 1`] = `
|
||||
>
|
||||
<Styled(button)
|
||||
active={true}
|
||||
data-index={0}
|
||||
key="0"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<button
|
||||
className="emotion-1"
|
||||
data-index={0}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Styled(span)>
|
||||
@ -1181,11 +1183,14 @@ exports[`A11YPanel should render report 1`] = `
|
||||
</button>
|
||||
</Styled(button)>
|
||||
<Styled(button)
|
||||
active={false}
|
||||
data-index={1}
|
||||
key="1"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<button
|
||||
className="emotion-3"
|
||||
data-index={1}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Styled(span)>
|
||||
@ -1199,11 +1204,14 @@ exports[`A11YPanel should render report 1`] = `
|
||||
</button>
|
||||
</Styled(button)>
|
||||
<Styled(button)
|
||||
active={false}
|
||||
data-index={2}
|
||||
key="2"
|
||||
onClick={[Function]}
|
||||
>
|
||||
<button
|
||||
className="emotion-3"
|
||||
data-index={2}
|
||||
onClick={[Function]}
|
||||
>
|
||||
<Styled(span)>
|
||||
@ -1242,6 +1250,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
addElement={[Function]}
|
||||
elementsToHighlight={Array []}
|
||||
highlightedElementsMap={Map {}}
|
||||
indeterminate={false}
|
||||
isToggledOn={false}
|
||||
label="Highlight results"
|
||||
toggleId="0-global-checkbox"
|
||||
@ -1271,7 +1280,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
</Styled(div)>
|
||||
</div>
|
||||
</Styled(div)>
|
||||
<Component
|
||||
<Report
|
||||
empty="No a11y violations found."
|
||||
items={Array []}
|
||||
passes={false}
|
||||
@ -1294,7 +1303,7 @@ exports[`A11YPanel should render report 1`] = `
|
||||
</div>
|
||||
</Styled(div)>
|
||||
</Placeholder>
|
||||
</Component>
|
||||
</Report>
|
||||
</div>
|
||||
</Styled(div)>
|
||||
</Tabs>
|
||||
|
@ -3,16 +3,16 @@ import axe, { AxeResults, ElementContext, RunOptions, Spec } from 'axe-core';
|
||||
import deprecate from 'util-deprecate';
|
||||
import { stripIndents } from 'common-tags';
|
||||
|
||||
import addons, { StoryWrapper } from '@storybook/addons';
|
||||
import addons, { makeDecorator } from '@storybook/addons';
|
||||
import { EVENTS, PARAM_KEY } from './constants';
|
||||
|
||||
const channel = addons.getChannel();
|
||||
let progress = Promise.resolve();
|
||||
let setup: {
|
||||
interface Setup {
|
||||
element?: ElementContext;
|
||||
config: Spec;
|
||||
options: RunOptions;
|
||||
} = { element: null, config: {}, options: {} };
|
||||
}
|
||||
let setup: Setup = { element: null, config: {}, options: {} };
|
||||
|
||||
const getElement = () => {
|
||||
const storyRoot = document.getElementById('story-root');
|
||||
@ -23,9 +23,7 @@ const getElement = () => {
|
||||
return document.getElementById('root');
|
||||
};
|
||||
|
||||
const report = (input: AxeResults) => {
|
||||
channel.emit(EVENTS.RESULT, input);
|
||||
};
|
||||
const report = (input: AxeResults) => addons.getChannel().emit(EVENTS.RESULT, input);
|
||||
|
||||
const run = (element: ElementContext, config: Spec, options: RunOptions) => {
|
||||
progress = progress.then(() => {
|
||||
@ -46,21 +44,23 @@ const run = (element: ElementContext, config: Spec, options: RunOptions) => {
|
||||
});
|
||||
};
|
||||
|
||||
// NOTE: we should add paramaters to the STORY_RENDERED event and deprecate this
|
||||
export const withA11y: StoryWrapper = (getStory, context) => {
|
||||
const params = context.parameters[PARAM_KEY];
|
||||
if (params) {
|
||||
setup = params;
|
||||
}
|
||||
return getStory(context);
|
||||
};
|
||||
|
||||
channel.on(EVENTS.REQUEST, () => run(setup.element, setup.config, setup.options));
|
||||
|
||||
if (module && module.hot && module.hot.decline) {
|
||||
module.hot.decline();
|
||||
}
|
||||
|
||||
export const withA11y = makeDecorator({
|
||||
name: 'withA11Y',
|
||||
parameterName: PARAM_KEY,
|
||||
wrapper: (getStory, context, { parameters }) => {
|
||||
if (parameters) {
|
||||
setup = parameters as Setup;
|
||||
}
|
||||
addons.getChannel().on(EVENTS.REQUEST, () => run(setup.element, setup.config, setup.options));
|
||||
|
||||
return getStory(context);
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: REMOVE at v6.0.0
|
||||
export const withA11Y = deprecate(
|
||||
// @ts-ignore
|
||||
|
@ -1,16 +1,17 @@
|
||||
import { createStore } from 'redux';
|
||||
import { ADD_ELEMENT, CLEAR_ELEMENTS } from './constants';
|
||||
import { HighlightedElementData } from './components/Report/HighlightToggle';
|
||||
|
||||
// actions
|
||||
|
||||
// add element is passed a HighlightedElementData object as the payload
|
||||
export function addElement(payload: any) {
|
||||
export function addElement(payload: { element: HTMLElement; data: HighlightedElementData }) {
|
||||
return { type: ADD_ELEMENT, payload };
|
||||
}
|
||||
|
||||
// clear elements is a function to remove elements from the map and reset elements to their original state
|
||||
export function clearElements(payload: any) {
|
||||
return { type: CLEAR_ELEMENTS, payload };
|
||||
export function clearElements() {
|
||||
return { type: CLEAR_ELEMENTS };
|
||||
}
|
||||
|
||||
// reducers
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-actions",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Action Logger addon for storybook",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
@ -21,11 +21,11 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/api": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/api": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"fast-deep-equal": "^2.0.1",
|
||||
"global": "^4.3.2",
|
||||
|
@ -12,11 +12,16 @@ export function action(name: string, options: ActionOptions = {}): HandlerFuncti
|
||||
const handler = function action(...args: any[]) {
|
||||
const channel = addons.getChannel();
|
||||
const id = uuid();
|
||||
const minDepth = 5; // anything less is really just storybook internals
|
||||
|
||||
const actionDisplayToEmit: ActionDisplay = {
|
||||
id,
|
||||
count: 0,
|
||||
data: { name, args },
|
||||
options: actionOptions,
|
||||
options: {
|
||||
...actionOptions,
|
||||
depth: minDepth + (actionOptions.depth || 3),
|
||||
},
|
||||
};
|
||||
channel.emit(EVENT_ID, actionDisplayToEmit);
|
||||
};
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-backgrounds",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "A storybook addon to show different backgrounds for your preview",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -25,12 +25,12 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/api": "5.1.0-alpha.20",
|
||||
"@storybook/client-logger": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/api": "5.1.0-alpha.24",
|
||||
"@storybook/client-logger": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"memoizerific": "^1.11.3",
|
||||
"react": "^16.8.4",
|
||||
|
18
addons/centered/angular.d.ts
vendored
18
addons/centered/angular.d.ts
vendored
@ -1,6 +1,22 @@
|
||||
// tslint:disable-next-line:no-implicit-dependencies
|
||||
import { IStory } from '@storybook/angular';
|
||||
export interface ICollection {
|
||||
[p: string]: any;
|
||||
}
|
||||
|
||||
export interface NgModuleMetadata {
|
||||
declarations?: any[];
|
||||
entryComponents?: any[];
|
||||
imports?: any[];
|
||||
schemas?: any[];
|
||||
providers?: any[];
|
||||
}
|
||||
|
||||
export interface IStory {
|
||||
props?: ICollection;
|
||||
moduleMetadata?: Partial<NgModuleMetadata>;
|
||||
component?: any;
|
||||
template?: string;
|
||||
}
|
||||
declare module '@storybook/addon-centered/angular' {
|
||||
export function centered(story: IStory): IStory;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-centered",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook decorator to center components",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -12,6 +12,7 @@ const styles = {
|
||||
innerStyle: {
|
||||
margin: 'auto',
|
||||
maxHeight: '100%', // Hack for centering correctly in IE11
|
||||
overflow: 'auto',
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-cssresources",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "A storybook addon to switch between css resources at runtime for your story",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -25,10 +25,10 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/api": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/api": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
"react": "^16.8.4"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-events",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Add events to your Storybook stories.",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -24,9 +24,9 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"format-json": "^1.0.3",
|
||||
"prop-types": "^15.7.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-google-analytics",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook addon for google analytics",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -20,8 +20,8 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
"react-ga": "^2.5.7"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-graphql",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook addon to display the GraphiQL IDE",
|
||||
"keywords": [
|
||||
"addon",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-info",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "A Storybook addon to show additional information for your stories.",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -22,10 +22,10 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/client-logger": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/client-logger": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
"marksy": "^6.1.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-jest",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "React storybook addon that show component jest report",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -23,18 +23,18 @@
|
||||
"license": "MIT",
|
||||
"author": "Renaud Tertrais <renaud.tertrais@gmail.com> (https://github.com/renaudtertrais)",
|
||||
"main": "dist/index.js",
|
||||
"jsnext:main": "src/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/api": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
"prop-types": "^15.7.2",
|
||||
"react": "^16.8.4",
|
||||
"upath": "^1.1.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
|
@ -1,8 +1,17 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { styled } from '@storybook/theming';
|
||||
|
||||
const Indicator = styled.div(
|
||||
interface IndicatorProps {
|
||||
color: string;
|
||||
size: number;
|
||||
children?: React.ReactNode;
|
||||
right?: boolean;
|
||||
overrides?: any;
|
||||
styles?: React.CSSProperties;
|
||||
}
|
||||
|
||||
const Indicator = styled.div<IndicatorProps>(
|
||||
({ color, size }) => ({
|
||||
boxSizing: 'border-box',
|
||||
padding: `0 ${size / 2}px`,
|
||||
@ -25,11 +34,4 @@ Indicator.defaultProps = {
|
||||
children: '',
|
||||
};
|
||||
|
||||
Indicator.propTypes = {
|
||||
color: PropTypes.string.isRequired,
|
||||
size: PropTypes.number.isRequired,
|
||||
children: PropTypes.node,
|
||||
right: PropTypes.bool,
|
||||
};
|
||||
|
||||
export default Indicator;
|
202
addons/jest/src/components/Message.tsx
Normal file
202
addons/jest/src/components/Message.tsx
Normal file
@ -0,0 +1,202 @@
|
||||
/* tslint:disable:object-literal-sort-keys */
|
||||
|
||||
import React from 'react';
|
||||
import { styled } from '@storybook/theming';
|
||||
import colors from '../colors';
|
||||
|
||||
const patterns = [/^\x08+/, /^\x1b\[[012]?K/, /^\x1b\[?[\d;]{0,3}/];
|
||||
|
||||
const Pre = styled.pre({
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
const Positive = styled.strong({
|
||||
color: colors.success,
|
||||
fontWeight: 500,
|
||||
});
|
||||
const Negative = styled.strong({
|
||||
color: colors.error,
|
||||
fontWeight: 500,
|
||||
});
|
||||
|
||||
interface StackTraceProps {
|
||||
trace: MsgElement[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const StackTrace = styled(({ trace, className }: StackTraceProps) => (
|
||||
<details className={className}>
|
||||
<summary>Callstack</summary>
|
||||
{trace
|
||||
.join('')
|
||||
.trim()
|
||||
.split(/\n/)
|
||||
.map((traceLine, traceLineIndex) => (
|
||||
<div key={traceLineIndex}>{traceLine.trim()}</div>
|
||||
))}
|
||||
</details>
|
||||
))({
|
||||
background: 'silver',
|
||||
padding: 10,
|
||||
overflow: 'auto',
|
||||
});
|
||||
|
||||
const Main = styled(({ msg, className }) => <section className={className}>{msg}</section>)({
|
||||
padding: 10,
|
||||
borderBottom: '1px solid silver',
|
||||
});
|
||||
|
||||
interface SubProps {
|
||||
msg: MsgElement[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Sub = styled(({ msg, className }: SubProps) => (
|
||||
<section className={className}>
|
||||
{msg
|
||||
.filter(item => typeof item !== 'string' || item.trim() !== '')
|
||||
.map((item, index, list) => {
|
||||
if (typeof item === 'string') {
|
||||
if (index === 0 && index === list.length - 1) {
|
||||
return item.trim();
|
||||
}
|
||||
if (index === 0) {
|
||||
return item.replace(/^[\s\n]*/, '');
|
||||
}
|
||||
if (index === list.length - 1) {
|
||||
return item.replace(/[\s\n]*$/, '');
|
||||
}
|
||||
}
|
||||
return item;
|
||||
})}
|
||||
</section>
|
||||
))({
|
||||
padding: 10,
|
||||
});
|
||||
|
||||
interface SubgroupOptions {
|
||||
startTrigger: (e: MsgElement) => boolean;
|
||||
endTrigger: (e: MsgElement) => boolean;
|
||||
grouper: (list: MsgElement[], key: number) => JSX.Element;
|
||||
accList?: MsgElement[];
|
||||
grouped?: MsgElement[];
|
||||
grouperIndex?: number;
|
||||
mode?: 'inject' | 'stop';
|
||||
injectionPoint?: number;
|
||||
}
|
||||
|
||||
const createSubgroup = ({
|
||||
startTrigger,
|
||||
endTrigger,
|
||||
grouper,
|
||||
accList = [],
|
||||
grouped = [],
|
||||
grouperIndex = 0,
|
||||
mode,
|
||||
injectionPoint,
|
||||
}: SubgroupOptions) => (acc: MsgElement[], item: MsgElement, i: number, list: MsgElement[]) => {
|
||||
grouperIndex += 1;
|
||||
|
||||
// start or stop extraction
|
||||
if (startTrigger(item)) {
|
||||
mode = 'inject';
|
||||
injectionPoint = i;
|
||||
}
|
||||
if (endTrigger(item)) {
|
||||
mode = 'stop';
|
||||
}
|
||||
|
||||
// push item in correct aggregator
|
||||
if (mode === 'inject') {
|
||||
grouped.push(item);
|
||||
} else {
|
||||
accList.push(item);
|
||||
}
|
||||
|
||||
// on last iteration inject at detected injection point, and group
|
||||
if (i === list.length - 1) {
|
||||
// Provide a "safety net" when Jest returns a partially recognized "group"
|
||||
// (recognized by acc.startTrigger but acc.endTrigger was never found) and
|
||||
// it's the only group in output for a test result. In that case, accList
|
||||
// will be empty, so return whatever was found, even if it will be unstyled
|
||||
// and prevent next createSubgroup calls from throwing due to empty lists.
|
||||
accList.push(null);
|
||||
|
||||
return accList.reduce<MsgElement[]>((eacc, el, ei) => {
|
||||
if (injectionPoint === 0 && ei === 0) {
|
||||
// at index 0, inject before
|
||||
return eacc.concat(grouper(grouped, grouperIndex)).concat(el);
|
||||
}
|
||||
if (injectionPoint > 0 && injectionPoint === ei + 1) {
|
||||
// at index > 0, and next index WOULD BE injectionPoint, inject after
|
||||
return eacc.concat(el).concat(grouper(grouped, grouperIndex));
|
||||
}
|
||||
// do not inject
|
||||
return eacc.concat(el);
|
||||
}, []);
|
||||
}
|
||||
return acc;
|
||||
};
|
||||
|
||||
interface MessageProps {
|
||||
msg: string;
|
||||
}
|
||||
|
||||
type MsgElement = string | JSX.Element;
|
||||
|
||||
const Message = ({ msg }: MessageProps) => {
|
||||
const data = patterns
|
||||
.reduce((acc, regex) => acc.replace(regex, ''), msg)
|
||||
.split(/\[2m/)
|
||||
.join('')
|
||||
.split(/\[22m/)
|
||||
.reduce((acc, item) => acc.concat(item), [] as string[])
|
||||
.map((item, li) =>
|
||||
item
|
||||
.split(/\[32m(.*?)\[39m/)
|
||||
.map((i, index) => (index % 2 ? <Positive key={`p_${li}_${i}`}>{i}</Positive> : i))
|
||||
)
|
||||
.reduce((acc, item) => acc.concat(item))
|
||||
.map((item, li) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
.split(/\[31m(.*?)\[39m/)
|
||||
.map((i, index) => (index % 2 ? <Negative key={`n_${li}_${i}`}>{i}</Negative> : i))
|
||||
: item
|
||||
)
|
||||
.reduce<MsgElement[]>((acc, item) => acc.concat(item), [])
|
||||
.reduce(
|
||||
createSubgroup({
|
||||
startTrigger: e => typeof e === 'string' && e.indexOf('Error: ') === 0,
|
||||
endTrigger: e => typeof e === 'string' && Boolean(e.match('Expected ')),
|
||||
grouper: (list, key) => <Main key={key} msg={list} />,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
.reduce(
|
||||
(acc, it) =>
|
||||
typeof it === 'string' ? acc.concat(it.split(/(at(.|\n)+\d+:\d+\))/)) : acc.concat(it),
|
||||
[] as MsgElement[]
|
||||
)
|
||||
.reduce((acc, item) => acc.concat(item), [] as MsgElement[])
|
||||
.reduce(
|
||||
createSubgroup({
|
||||
startTrigger: e => typeof e === 'string' && e.indexOf('Expected ') !== -1,
|
||||
endTrigger: e => typeof e === 'string' && Boolean(e.match(/^at/)),
|
||||
grouper: (list, key) => <Sub key={key} msg={list} />,
|
||||
}),
|
||||
[]
|
||||
)
|
||||
.reduce(
|
||||
createSubgroup({
|
||||
startTrigger: e => typeof e === 'string' && Boolean(e.match(/at(.|\n)+\d+:\d+\)/)),
|
||||
endTrigger: () => false,
|
||||
grouper: (list, key) => <StackTrace key={key} trace={list} />,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
|
||||
return <Pre>{data}</Pre>;
|
||||
};
|
||||
|
||||
export default Message;
|
@ -1,11 +1,10 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { styled } from '@storybook/theming';
|
||||
import { ScrollArea } from '@storybook/components';
|
||||
|
||||
import Indicator from './Indicator';
|
||||
import Result, { FailedResult } from './Result';
|
||||
import provideJestResult from '../hoc/provideJestResult';
|
||||
import provideJestResult, { Test } from '../hoc/provideJestResult';
|
||||
import colors from '../colors';
|
||||
|
||||
const List = styled.ul({
|
||||
@ -95,7 +94,12 @@ const SuiteTitle = styled.div({
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const Content = styled(({ tests, className }) => (
|
||||
interface ContentProps {
|
||||
tests: Test[];
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const Content = styled(({ tests, className }: ContentProps) => (
|
||||
<div className={className}>
|
||||
{tests.map(({ name, result }) => {
|
||||
if (!result) {
|
||||
@ -142,7 +146,11 @@ const Content = styled(({ tests, className }) => (
|
||||
flex: '1 1 0%',
|
||||
});
|
||||
|
||||
const Panel = ({ tests }) => (
|
||||
interface PanelProps {
|
||||
tests: null | Test[];
|
||||
}
|
||||
|
||||
const Panel = ({ tests }: PanelProps) => (
|
||||
<ScrollArea vertical>
|
||||
{tests ? <Content tests={tests} /> : <NoTests>This story has no tests configured</NoTests>}
|
||||
</ScrollArea>
|
||||
@ -152,12 +160,4 @@ Panel.defaultProps = {
|
||||
tests: null,
|
||||
};
|
||||
|
||||
Panel.propTypes = {
|
||||
tests: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
result: PropTypes.object,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
export default provideJestResult(Panel);
|
@ -1,47 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { styled } from '@storybook/theming';
|
||||
|
||||
import provideJestResult from '../hoc/provideJestResult';
|
||||
import Indicator from './Indicator';
|
||||
import colors from '../colors';
|
||||
|
||||
const Wrapper = styled.div({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
const PanelName = styled.div({
|
||||
paddingLeft: 5,
|
||||
});
|
||||
|
||||
const PanelTitle = ({ tests }) => {
|
||||
if (!tests) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const results = tests.map(report => report.result).filter(report => !!report);
|
||||
const success = results.reduce((acc, result) => acc && result.status === 'passed', true);
|
||||
const color = success ? colors.success : colors.error;
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<Indicator color={results.length < tests.length ? colors.warning : color} size={10} />
|
||||
<PanelName>Tests</PanelName>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
PanelTitle.defaultProps = {
|
||||
tests: null,
|
||||
};
|
||||
|
||||
PanelTitle.propTypes = {
|
||||
tests: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
result: PropTypes.object,
|
||||
})
|
||||
),
|
||||
};
|
||||
|
||||
export default provideJestResult(PanelTitle);
|
@ -1,264 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { styled } from '@storybook/theming';
|
||||
|
||||
import Indicator from './Indicator';
|
||||
import colors from '../colors';
|
||||
|
||||
const Pre = styled.pre({
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
const FlexContainer = styled.div({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
/* eslint no-control-regex:0 */
|
||||
const patterns = [/^\x08+/, /^\x1b\[[012]?K/, /^\x1b\[?[\d;]{0,3}/];
|
||||
|
||||
const Positive = styled.strong({
|
||||
color: colors.success,
|
||||
fontWeight: 500,
|
||||
});
|
||||
const Negative = styled.strong({
|
||||
color: colors.error,
|
||||
fontWeight: 500,
|
||||
});
|
||||
const StackTrace = styled(({ trace, className }) => (
|
||||
<details className={className}>
|
||||
<summary>Callstack</summary>
|
||||
{trace
|
||||
.join('')
|
||||
.trim()
|
||||
.split(/\n/)
|
||||
.map((traceLine, traceLineIndex) => (
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
<div key={traceLineIndex}>{traceLine.trim()}</div>
|
||||
))}
|
||||
</details>
|
||||
))({
|
||||
background: 'silver',
|
||||
padding: 10,
|
||||
overflow: 'auto',
|
||||
});
|
||||
const Main = styled(({ msg, className }) => <section className={className}>{msg}</section>)({
|
||||
padding: 10,
|
||||
borderBottom: '1px solid silver',
|
||||
});
|
||||
const Sub = styled(({ msg, className }) => (
|
||||
<section className={className}>
|
||||
{msg
|
||||
.filter(item => typeof item !== 'string' || (typeof item === 'string' && item.trim() !== ''))
|
||||
.map((item, index, list) => {
|
||||
switch (true) {
|
||||
case typeof item === 'string' && index === 0 && index === list.length - 1: {
|
||||
return item.trim();
|
||||
}
|
||||
case typeof item === 'string' && index === 0: {
|
||||
return item.replace(/^[\s\n]*/, '');
|
||||
}
|
||||
case typeof item === 'string' && index === list.length - 1: {
|
||||
return item.replace(/[\s\n]*$/, '');
|
||||
}
|
||||
default: {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
// typeof item === 'string' ? <span>{item}</span> : item;
|
||||
})}
|
||||
</section>
|
||||
))({
|
||||
padding: 10,
|
||||
});
|
||||
|
||||
const createSubgroup = (acc, item, i, list) => {
|
||||
// setup aggregators
|
||||
if (!acc.list) {
|
||||
acc.list = [];
|
||||
}
|
||||
if (!acc.grouped) {
|
||||
acc.grouped = [];
|
||||
}
|
||||
if (!('grouperIndex' in acc)) {
|
||||
acc.grouperIndex = 0;
|
||||
} else {
|
||||
acc.grouperIndex += 1;
|
||||
}
|
||||
|
||||
// start or stop extraction
|
||||
if (acc.startTrigger(item)) {
|
||||
acc.mode = 'inject';
|
||||
acc.injectionPoint = i;
|
||||
}
|
||||
if (acc.endTrigger(item)) {
|
||||
acc.mode = 'stop';
|
||||
}
|
||||
|
||||
// push item in correct aggregator
|
||||
if (acc.mode === 'inject') {
|
||||
acc.grouped.push(item);
|
||||
} else {
|
||||
acc.list.push(item);
|
||||
}
|
||||
|
||||
// on last iteration inject at detected injectionpoint, and group
|
||||
if (i === list.length - 1) {
|
||||
// Provide a "safety net" when Jest returns a partially recognized "group"
|
||||
// (recognized by acc.startTrigger but acc.endTrigger was never found) and
|
||||
// it's the only group in output for a test result. In that case, acc.list
|
||||
// will be empty, so return whatever was found, even if it will be unstyled
|
||||
// and prevent next createSubgroup calls from throwing due to empty lists.
|
||||
acc.list.push(null);
|
||||
|
||||
return acc.list.reduce((eacc, el, ei) => {
|
||||
switch (true) {
|
||||
case acc.injectionPoint === 0 && ei === 0: {
|
||||
// at index 0, inject before
|
||||
return eacc.concat(acc.grouper(acc.grouped, acc.grouperIndex)).concat(el);
|
||||
}
|
||||
case acc.injectionPoint > 0 && acc.injectionPoint === ei + 1: {
|
||||
// at index > 0, and next index WOULD BE injectionPoint, inject after
|
||||
return eacc.concat(el).concat(acc.grouper(acc.grouped, acc.grouperIndex));
|
||||
}
|
||||
default: {
|
||||
// do not inject
|
||||
return eacc.concat(el);
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
}
|
||||
return acc;
|
||||
};
|
||||
|
||||
const Message = ({ msg }) => {
|
||||
const data = patterns
|
||||
.reduce((acc, regex) => acc.replace(regex, ''), msg)
|
||||
.split(/\[2m/)
|
||||
.join('')
|
||||
.split(/\[22m/)
|
||||
.reduce((acc, item) => acc.concat(item), [])
|
||||
.map((item, li) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
.split(/\[32m(.*?)\[39m/)
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
.map((i, index) => (index % 2 ? <Positive key={`p_${li}_${i}`}>{i}</Positive> : i))
|
||||
: item
|
||||
)
|
||||
.reduce((acc, item) => acc.concat(item), [])
|
||||
.map((item, li) =>
|
||||
typeof item === 'string'
|
||||
? item
|
||||
.split(/\[31m(.*?)\[39m/)
|
||||
// eslint-disable-next-line react/no-array-index-key
|
||||
.map((i, index) => (index % 2 ? <Negative key={`n_${li}_${i}`}>{i}</Negative> : i))
|
||||
: item
|
||||
)
|
||||
.reduce((acc, item) => acc.concat(item), [])
|
||||
.reduce(createSubgroup, {
|
||||
startTrigger: e => typeof e === 'string' && e.indexOf('Error: ') === 0,
|
||||
endTrigger: e => typeof e === 'string' && e.match('Expected '),
|
||||
grouper: (list, key) => <Main key={key} msg={list} />,
|
||||
})
|
||||
.reduce(
|
||||
(acc, it) =>
|
||||
typeof it === 'string' ? acc.concat(it.split(/(at(.|\n)+\d+:\d+\))/)) : acc.concat(it),
|
||||
[]
|
||||
)
|
||||
.reduce((acc, item) => acc.concat(item), [])
|
||||
.reduce(createSubgroup, {
|
||||
startTrigger: e => typeof e === 'string' && e.indexOf('Expected ') !== -1,
|
||||
endTrigger: e => typeof e === 'string' && e.match(/^at/),
|
||||
grouper: (list, key) => <Sub key={key} msg={list} />,
|
||||
})
|
||||
.reduce(createSubgroup, {
|
||||
startTrigger: e => typeof e === 'string' && e.match(/at(.|\n)+\d+:\d+\)/),
|
||||
endTrigger: () => false,
|
||||
grouper: (list, key) => <StackTrace key={key} trace={list} />,
|
||||
});
|
||||
|
||||
return <Pre>{data}</Pre>;
|
||||
};
|
||||
Message.propTypes = {
|
||||
msg: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const Head = styled.header({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
});
|
||||
|
||||
const Title = styled.h3({
|
||||
padding: '10px 10px 0 10px',
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
export const FailedResult = styled(({ fullName, title, status, failureMessages, className }) => (
|
||||
<div className={className}>
|
||||
<Head>
|
||||
<FlexContainer>
|
||||
<Indicator
|
||||
color={colors.error}
|
||||
size={10}
|
||||
overrides={{ borderRadius: '5px 0', position: 'absolute', top: -1, left: -1 }}
|
||||
/>
|
||||
<Title>{fullName || title}</Title>
|
||||
</FlexContainer>
|
||||
<Indicator
|
||||
color={colors.error}
|
||||
size={16}
|
||||
overrides={{ borderRadius: '0 5px', position: 'absolute', top: -1, right: -1 }}
|
||||
>
|
||||
{status}
|
||||
</Indicator>
|
||||
</Head>
|
||||
{/* eslint-disable react/no-array-index-key */}
|
||||
{failureMessages.map((msg, i) => (
|
||||
<Message msg={msg} key={i} />
|
||||
))}
|
||||
</div>
|
||||
))({
|
||||
display: 'block',
|
||||
borderRadius: 5,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
border: '1px solid silver',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
|
||||
const Result = ({ fullName, title, status }) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<FlexContainer>
|
||||
<Indicator color={colors.success} size={10} overrides={{ marginRight: 10 }} />
|
||||
<div>{fullName || title}</div>
|
||||
</FlexContainer>
|
||||
<FlexContainer>
|
||||
<Indicator color={colors.success} size={14} right>
|
||||
{status}
|
||||
</Indicator>
|
||||
</FlexContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
Result.defaultProps = {
|
||||
fullName: '',
|
||||
title: '',
|
||||
};
|
||||
|
||||
Result.propTypes = {
|
||||
fullName: PropTypes.string,
|
||||
title: PropTypes.string,
|
||||
status: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default Result;
|
88
addons/jest/src/components/Result.tsx
Normal file
88
addons/jest/src/components/Result.tsx
Normal file
@ -0,0 +1,88 @@
|
||||
import React from 'react';
|
||||
|
||||
import { styled } from '@storybook/theming';
|
||||
|
||||
import Message from './Message';
|
||||
import Indicator from './Indicator';
|
||||
import colors from '../colors';
|
||||
|
||||
const FlexContainer = styled.div({
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
});
|
||||
|
||||
const Head = styled.header({
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'flex-start',
|
||||
});
|
||||
|
||||
const Title = styled.h3({
|
||||
padding: '10px 10px 0 10px',
|
||||
margin: 0,
|
||||
});
|
||||
|
||||
export const FailedResult = styled(({ fullName, title, status, failureMessages, className }) => (
|
||||
<div className={className}>
|
||||
<Head>
|
||||
<FlexContainer>
|
||||
<Indicator
|
||||
color={colors.error}
|
||||
size={10}
|
||||
overrides={{ borderRadius: '5px 0', position: 'absolute', top: -1, left: -1 }}
|
||||
/>
|
||||
<Title>{fullName || title}</Title>
|
||||
</FlexContainer>
|
||||
<Indicator
|
||||
color={colors.error}
|
||||
size={16}
|
||||
overrides={{ borderRadius: '0 5px', position: 'absolute', top: -1, right: -1 }}
|
||||
>
|
||||
{status}
|
||||
</Indicator>
|
||||
</Head>
|
||||
{failureMessages.map((msg: string, i: number) => (
|
||||
<Message msg={msg} key={i} />
|
||||
))}
|
||||
</div>
|
||||
))({
|
||||
display: 'block',
|
||||
borderRadius: 5,
|
||||
margin: 0,
|
||||
padding: 0,
|
||||
position: 'relative',
|
||||
border: '1px solid silver',
|
||||
boxSizing: 'border-box',
|
||||
});
|
||||
|
||||
interface ResultProps {
|
||||
fullName?: string;
|
||||
title?: string;
|
||||
status: string;
|
||||
}
|
||||
const Result = ({ fullName, title, status }: ResultProps) => (
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'space-between',
|
||||
}}
|
||||
>
|
||||
<FlexContainer>
|
||||
<Indicator color={colors.success} size={10} overrides={{ marginRight: 10 }} />
|
||||
<div>{fullName || title}</div>
|
||||
</FlexContainer>
|
||||
<FlexContainer>
|
||||
<Indicator color={colors.success} size={14} right>
|
||||
{status}
|
||||
</Indicator>
|
||||
</FlexContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
Result.defaultProps = {
|
||||
fullName: '',
|
||||
title: '',
|
||||
};
|
||||
|
||||
export default Result;
|
@ -1,59 +0,0 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { STORY_CHANGED } from '@storybook/core-events';
|
||||
import { ADD_TESTS } from '../shared';
|
||||
|
||||
const provideTests = Component =>
|
||||
class TestProvider extends React.Component {
|
||||
static propTypes = {
|
||||
channel: PropTypes.shape({
|
||||
on: PropTypes.func,
|
||||
removeListener: PropTypes.func,
|
||||
}).isRequired,
|
||||
api: PropTypes.shape({
|
||||
on: PropTypes.func,
|
||||
}).isRequired,
|
||||
active: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
active: false,
|
||||
};
|
||||
|
||||
state = {};
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
const { channel, api } = this.props;
|
||||
|
||||
this.stopListeningOnStory = api.on(STORY_CHANGED, () => {
|
||||
const { kind, storyName, tests } = this.state;
|
||||
if (this.mounted && (kind || storyName || tests)) {
|
||||
this.onAddTests({});
|
||||
}
|
||||
});
|
||||
|
||||
channel.on(ADD_TESTS, this.onAddTests);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
const { channel } = this.props;
|
||||
|
||||
this.stopListeningOnStory();
|
||||
channel.removeListener(ADD_TESTS, this.onAddTests);
|
||||
}
|
||||
|
||||
onAddTests = ({ kind, storyName, tests }) => {
|
||||
this.setState({ kind, storyName, tests });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { active } = this.props;
|
||||
const { tests } = this.state;
|
||||
|
||||
return active && tests ? <Component {...this.state} /> : null;
|
||||
}
|
||||
};
|
||||
|
||||
export default provideTests;
|
82
addons/jest/src/hoc/provideJestResult.tsx
Normal file
82
addons/jest/src/hoc/provideJestResult.tsx
Normal file
@ -0,0 +1,82 @@
|
||||
import React from 'react';
|
||||
import { STORY_CHANGED } from '@storybook/core-events';
|
||||
import { ADD_TESTS } from '../shared';
|
||||
import { API } from '@storybook/api';
|
||||
|
||||
// TODO: import type from @types/jest
|
||||
interface AssertionResult {
|
||||
status: string;
|
||||
fullName: string;
|
||||
title: string;
|
||||
failureMessages: string[];
|
||||
}
|
||||
|
||||
export interface Test {
|
||||
name: string;
|
||||
result: {
|
||||
status: string;
|
||||
assertionResults: AssertionResult[];
|
||||
};
|
||||
}
|
||||
|
||||
interface InjectedProps {
|
||||
tests?: Test[];
|
||||
}
|
||||
|
||||
export interface HocProps {
|
||||
api: API;
|
||||
active?: boolean;
|
||||
}
|
||||
|
||||
export interface HocState {
|
||||
kind?: string;
|
||||
storyName?: string;
|
||||
tests?: Test[];
|
||||
}
|
||||
|
||||
const provideTests = (Component: React.ComponentType<InjectedProps>) =>
|
||||
class TestProvider extends React.Component<HocProps, HocState> {
|
||||
static defaultProps = {
|
||||
active: false,
|
||||
};
|
||||
|
||||
mounted: boolean;
|
||||
stopListeningOnStory: () => void;
|
||||
|
||||
state: HocState = {};
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
const { api } = this.props;
|
||||
|
||||
this.stopListeningOnStory = api.on(STORY_CHANGED, () => {
|
||||
const { kind, storyName, tests } = this.state;
|
||||
if (this.mounted && (kind || storyName || tests)) {
|
||||
this.onAddTests({});
|
||||
}
|
||||
});
|
||||
|
||||
api.on(ADD_TESTS, this.onAddTests);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
const { api } = this.props;
|
||||
|
||||
this.stopListeningOnStory();
|
||||
api.removeListener(ADD_TESTS, this.onAddTests);
|
||||
}
|
||||
|
||||
onAddTests = ({ kind, storyName, tests }: HocState) => {
|
||||
this.setState({ kind, storyName, tests });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { active } = this.props;
|
||||
const { tests } = this.state;
|
||||
|
||||
return active && tests ? <Component tests={tests} /> : null;
|
||||
}
|
||||
};
|
||||
|
||||
export default provideTests;
|
@ -3,7 +3,11 @@ import deprecate from 'util-deprecate';
|
||||
import { normalize } from 'upath';
|
||||
import { ADD_TESTS } from './shared';
|
||||
|
||||
const findTestResults = (testFiles, jestTestResults, jestTestFilesExt) =>
|
||||
const findTestResults = (
|
||||
testFiles: string[],
|
||||
jestTestResults: { testResults: Array<{ name: string }> },
|
||||
jestTestFilesExt: string
|
||||
) =>
|
||||
Object.values(testFiles).map(name => {
|
||||
const fileName = `${name}${jestTestFilesExt}`;
|
||||
|
||||
@ -14,7 +18,7 @@ const findTestResults = (testFiles, jestTestResults, jestTestFilesExt) =>
|
||||
fileName,
|
||||
name,
|
||||
result: jestTestResults.testResults.find(test =>
|
||||
normalize(test.name).match(fileNamePattern)
|
||||
Boolean(normalize(test.name).match(fileNamePattern))
|
||||
),
|
||||
};
|
||||
}
|
||||
@ -22,7 +26,17 @@ const findTestResults = (testFiles, jestTestResults, jestTestFilesExt) =>
|
||||
return { fileName, name };
|
||||
});
|
||||
|
||||
const emitAddTests = ({ kind, story, testFiles, options }) => {
|
||||
const emitAddTests = ({
|
||||
kind,
|
||||
story,
|
||||
testFiles,
|
||||
options,
|
||||
}: {
|
||||
kind: string;
|
||||
story: () => void;
|
||||
testFiles: any;
|
||||
options: { results: { testResults: Array<{ name: string }> }; filesExt: string };
|
||||
}) => {
|
||||
addons.getChannel().emit(ADD_TESTS, {
|
||||
kind,
|
||||
storyName: story,
|
||||
@ -30,15 +44,16 @@ const emitAddTests = ({ kind, story, testFiles, options }) => {
|
||||
});
|
||||
};
|
||||
|
||||
export const withTests = userOptions => {
|
||||
export const withTests = (userOptions: { results: any; filesExt: string }) => {
|
||||
const defaultOptions = {
|
||||
filesExt: '((\\.specs?)|(\\.tests?))?(\\.js)?$',
|
||||
};
|
||||
const options = Object.assign({}, defaultOptions, userOptions);
|
||||
const options = { ...defaultOptions, ...userOptions };
|
||||
|
||||
return (...args) => {
|
||||
return (...args: [(string | (() => void)), { kind: string; parameters: { jest?: any } }]) => {
|
||||
if (typeof args[0] === 'string') {
|
||||
return deprecate((storyFn, { kind }) => {
|
||||
// tslint:disable-next-line:no-shadowed-variable
|
||||
return deprecate((storyFn: () => void, { kind }: { kind: string }) => {
|
||||
emitAddTests({ kind, story: storyFn, testFiles: args, options });
|
||||
|
||||
return storyFn();
|
@ -2,15 +2,11 @@ import React from 'react';
|
||||
import addons from '@storybook/addons';
|
||||
import { ADDON_ID, PANEL_ID } from './shared';
|
||||
|
||||
// import PanelTitle from './components/PanelTitle';
|
||||
import Panel from './components/Panel';
|
||||
|
||||
addons.register(ADDON_ID, api => {
|
||||
const channel = addons.getChannel();
|
||||
|
||||
addons.addPanel(PANEL_ID, {
|
||||
title: 'tests',
|
||||
// eslint-disable-next-line react/prop-types
|
||||
render: ({ active, key }) => <Panel key={key} channel={channel} api={api} active={active} />,
|
||||
render: ({ active, key }) => <Panel key={key} api={api} active={active} />,
|
||||
});
|
||||
});
|
@ -1,573 +0,0 @@
|
||||
import { document } from 'global';
|
||||
|
||||
const styles = `
|
||||
@font-face {
|
||||
font-family: octicons-link;
|
||||
src: url(data:font/woff;charset=utf-8;base64,d09GRgABAAAAAAZwABAAAAAACFQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABEU0lHAAAGaAAAAAgAAAAIAAAAAUdTVUIAAAZcAAAACgAAAAoAAQAAT1MvMgAAAyQAAABJAAAAYFYEU3RjbWFwAAADcAAAAEUAAACAAJThvmN2dCAAAATkAAAABAAAAAQAAAAAZnBnbQAAA7gAAACyAAABCUM+8IhnYXNwAAAGTAAAABAAAAAQABoAI2dseWYAAAFsAAABPAAAAZwcEq9taGVhZAAAAsgAAAA0AAAANgh4a91oaGVhAAADCAAAABoAAAAkCA8DRGhtdHgAAAL8AAAADAAAAAwGAACfbG9jYQAAAsAAAAAIAAAACABiATBtYXhwAAACqAAAABgAAAAgAA8ASm5hbWUAAAToAAABQgAAAlXu73sOcG9zdAAABiwAAAAeAAAAME3QpOBwcmVwAAAEbAAAAHYAAAB/aFGpk3jaTY6xa8JAGMW/O62BDi0tJLYQincXEypYIiGJjSgHniQ6umTsUEyLm5BV6NDBP8Tpts6F0v+k/0an2i+itHDw3v2+9+DBKTzsJNnWJNTgHEy4BgG3EMI9DCEDOGEXzDADU5hBKMIgNPZqoD3SilVaXZCER3/I7AtxEJLtzzuZfI+VVkprxTlXShWKb3TBecG11rwoNlmmn1P2WYcJczl32etSpKnziC7lQyWe1smVPy/Lt7Kc+0vWY/gAgIIEqAN9we0pwKXreiMasxvabDQMM4riO+qxM2ogwDGOZTXxwxDiycQIcoYFBLj5K3EIaSctAq2kTYiw+ymhce7vwM9jSqO8JyVd5RH9gyTt2+J/yUmYlIR0s04n6+7Vm1ozezUeLEaUjhaDSuXHwVRgvLJn1tQ7xiuVv/ocTRF42mNgZGBgYGbwZOBiAAFGJBIMAAizAFoAAABiAGIAznjaY2BkYGAA4in8zwXi+W2+MjCzMIDApSwvXzC97Z4Ig8N/BxYGZgcgl52BCSQKAA3jCV8CAABfAAAAAAQAAEB42mNgZGBg4f3vACQZQABIMjKgAmYAKEgBXgAAeNpjYGY6wTiBgZWBg2kmUxoDA4MPhGZMYzBi1AHygVLYQUCaawqDA4PChxhmh/8ODDEsvAwHgMKMIDnGL0x7gJQCAwMAJd4MFwAAAHjaY2BgYGaA4DAGRgYQkAHyGMF8NgYrIM3JIAGVYYDT+AEjAwuDFpBmA9KMDEwMCh9i/v8H8sH0/4dQc1iAmAkALaUKLgAAAHjaTY9LDsIgEIbtgqHUPpDi3gPoBVyRTmTddOmqTXThEXqrob2gQ1FjwpDvfwCBdmdXC5AVKFu3e5MfNFJ29KTQT48Ob9/lqYwOGZxeUelN2U2R6+cArgtCJpauW7UQBqnFkUsjAY/kOU1cP+DAgvxwn1chZDwUbd6CFimGXwzwF6tPbFIcjEl+vvmM/byA48e6tWrKArm4ZJlCbdsrxksL1AwWn/yBSJKpYbq8AXaaTb8AAHja28jAwOC00ZrBeQNDQOWO//sdBBgYGRiYWYAEELEwMTE4uzo5Zzo5b2BxdnFOcALxNjA6b2ByTswC8jYwg0VlNuoCTWAMqNzMzsoK1rEhNqByEyerg5PMJlYuVueETKcd/89uBpnpvIEVomeHLoMsAAe1Id4AAAAAAAB42oWQT07CQBTGv0JBhagk7HQzKxca2sJCE1hDt4QF+9JOS0nbaaYDCQfwCJ7Au3AHj+LO13FMmm6cl7785vven0kBjHCBhfpYuNa5Ph1c0e2Xu3jEvWG7UdPDLZ4N92nOm+EBXuAbHmIMSRMs+4aUEd4Nd3CHD8NdvOLTsA2GL8M9PODbcL+hD7C1xoaHeLJSEao0FEW14ckxC+TU8TxvsY6X0eLPmRhry2WVioLpkrbp84LLQPGI7c6sOiUzpWIWS5GzlSgUzzLBSikOPFTOXqly7rqx0Z1Q5BAIoZBSFihQYQOOBEdkCOgXTOHA07HAGjGWiIjaPZNW13/+lm6S9FT7rLHFJ6fQbkATOG1j2OFMucKJJsxIVfQORl+9Jyda6Sl1dUYhSCm1dyClfoeDve4qMYdLEbfqHf3O/AdDumsjAAB42mNgYoAAZQYjBmyAGYQZmdhL8zLdDEydARfoAqIAAAABAAMABwAKABMAB///AA8AAQAAAAAAAAAAAAAAAAABAAAAAA==) format('woff');
|
||||
}
|
||||
.markdown-body {
|
||||
-ms-text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
line-height: 1.5;
|
||||
color: #333;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
font-size: 16px;
|
||||
line-height: 1.5;
|
||||
word-wrap: break-word;
|
||||
}
|
||||
.markdown-body .pl-c {
|
||||
color: #969896;
|
||||
}
|
||||
.markdown-body .pl-c1,
|
||||
.markdown-body .pl-s .pl-v {
|
||||
color: #0086b3;
|
||||
}
|
||||
.markdown-body .pl-e,
|
||||
.markdown-body .pl-en {
|
||||
color: #795da3;
|
||||
}
|
||||
.markdown-body .pl-smi,
|
||||
.markdown-body .pl-s .pl-s1 {
|
||||
color: #333;
|
||||
}
|
||||
.markdown-body .pl-ent {
|
||||
color: #63a35c;
|
||||
}
|
||||
.markdown-body .pl-k {
|
||||
color: #a71d5d;
|
||||
}
|
||||
.markdown-body .pl-s,
|
||||
.markdown-body .pl-pds,
|
||||
.markdown-body .pl-s .pl-pse .pl-s1,
|
||||
.markdown-body .pl-sr,
|
||||
.markdown-body .pl-sr .pl-cce,
|
||||
.markdown-body .pl-sr .pl-sre,
|
||||
.markdown-body .pl-sr .pl-sra {
|
||||
color: #183691;
|
||||
}
|
||||
.markdown-body .pl-v {
|
||||
color: #ed6a43;
|
||||
}
|
||||
.markdown-body .pl-id {
|
||||
color: #b52a1d;
|
||||
}
|
||||
.markdown-body .pl-ii {
|
||||
color: #f8f8f8;
|
||||
background-color: #b52a1d;
|
||||
}
|
||||
.markdown-body .pl-sr .pl-cce {
|
||||
font-weight: bold;
|
||||
color: #63a35c;
|
||||
}
|
||||
.markdown-body .pl-ml {
|
||||
color: #693a17;
|
||||
}
|
||||
.markdown-body .pl-mh,
|
||||
.markdown-body .pl-mh .pl-en,
|
||||
.markdown-body .pl-ms {
|
||||
font-weight: bold;
|
||||
color: #1d3e81;
|
||||
}
|
||||
.markdown-body .pl-mq {
|
||||
color: #008080;
|
||||
}
|
||||
.markdown-body .pl-mi {
|
||||
font-style: italic;
|
||||
color: #333;
|
||||
}
|
||||
.markdown-body .pl-mb {
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.markdown-body .pl-md {
|
||||
color: #bd2c00;
|
||||
background-color: #ffecec;
|
||||
}
|
||||
.markdown-body .pl-mi1 {
|
||||
color: #55a532;
|
||||
background-color: #eaffea;
|
||||
}
|
||||
.markdown-body .pl-mdr {
|
||||
font-weight: bold;
|
||||
color: #795da3;
|
||||
}
|
||||
.markdown-body .pl-mo {
|
||||
color: #1d3e81;
|
||||
}
|
||||
.markdown-body .octicon {
|
||||
display: inline-block;
|
||||
vertical-align: text-top;
|
||||
fill: currentColor;
|
||||
}
|
||||
.markdown-body a {
|
||||
background-color: transparent;
|
||||
-webkit-text-decoration-skip: objects;
|
||||
}
|
||||
.markdown-body a:active,
|
||||
.markdown-body a:hover {
|
||||
outline-width: 0;
|
||||
}
|
||||
.markdown-body strong {
|
||||
font-weight: inherit;
|
||||
}
|
||||
.markdown-body strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
.markdown-body h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
.markdown-body img {
|
||||
border-style: none;
|
||||
}
|
||||
.markdown-body svg:not(:root) {
|
||||
overflow: hidden;
|
||||
}
|
||||
.markdown-body code,
|
||||
.markdown-body kbd,
|
||||
.markdown-body pre {
|
||||
font-family: monospace, monospace;
|
||||
font-size: 1em;
|
||||
}
|
||||
.markdown-body hr {
|
||||
box-sizing: content-box;
|
||||
height: 0;
|
||||
overflow: visible;
|
||||
}
|
||||
.markdown-body input {
|
||||
font: inherit;
|
||||
margin: 0;
|
||||
}
|
||||
.markdown-body input {
|
||||
overflow: visible;
|
||||
}
|
||||
.markdown-body [type="checkbox"] {
|
||||
box-sizing: border-box;
|
||||
padding: 0;
|
||||
}
|
||||
.markdown-body * {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.markdown-body input {
|
||||
font-family: inherit;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
}
|
||||
.markdown-body a {
|
||||
color: #4078c0;
|
||||
text-decoration: none;
|
||||
}
|
||||
.markdown-body a:hover,
|
||||
.markdown-body a:active {
|
||||
text-decoration: underline;
|
||||
}
|
||||
.markdown-body strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-body hr {
|
||||
height: 0;
|
||||
margin: 15px 0;
|
||||
overflow: hidden;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
.markdown-body hr::before {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
.markdown-body hr::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
.markdown-body table {
|
||||
border-spacing: 0;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
.markdown-body td,
|
||||
.markdown-body th {
|
||||
padding: 0;
|
||||
}
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown-body h1 {
|
||||
font-size: 32px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-body h2 {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-body h3 {
|
||||
font-size: 20px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-body h4 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-body h5 {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-body h6 {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.markdown-body p {
|
||||
margin-top: 0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.markdown-body blockquote {
|
||||
margin: 0;
|
||||
}
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
padding-left: 0;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown-body ol ol,
|
||||
.markdown-body ul ol {
|
||||
list-style-type: lower-roman;
|
||||
}
|
||||
.markdown-body ul ul ol,
|
||||
.markdown-body ul ol ol,
|
||||
.markdown-body ol ul ol,
|
||||
.markdown-body ol ol ol {
|
||||
list-style-type: lower-alpha;
|
||||
}
|
||||
.markdown-body dd {
|
||||
margin-left: 0;
|
||||
}
|
||||
.markdown-body code {
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 12px;
|
||||
}
|
||||
.markdown-body pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
font: 12px Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
}
|
||||
.markdown-body .octicon {
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.markdown-body input {
|
||||
-webkit-font-feature-settings: "liga" 0;
|
||||
font-feature-settings: "liga" 0;
|
||||
}
|
||||
.markdown-body::before {
|
||||
display: table;
|
||||
content: "";
|
||||
}
|
||||
.markdown-body::after {
|
||||
display: table;
|
||||
clear: both;
|
||||
content: "";
|
||||
}
|
||||
.markdown-body>*:first-child {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.markdown-body>*:last-child {
|
||||
margin-bottom: 0 !important;
|
||||
}
|
||||
.markdown-body a:not([href]) {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.markdown-body .anchor {
|
||||
float: left;
|
||||
padding-right: 4px;
|
||||
margin-left: -20px;
|
||||
line-height: 1;
|
||||
}
|
||||
.markdown-body .anchor:focus {
|
||||
outline: none;
|
||||
}
|
||||
.markdown-body p,
|
||||
.markdown-body blockquote,
|
||||
.markdown-body ul,
|
||||
.markdown-body ol,
|
||||
.markdown-body dl,
|
||||
.markdown-body table,
|
||||
.markdown-body pre {
|
||||
margin-top: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.markdown-body hr {
|
||||
height: 0.25em;
|
||||
padding: 0;
|
||||
margin: 24px 0;
|
||||
background-color: #e7e7e7;
|
||||
border: 0;
|
||||
}
|
||||
.markdown-body blockquote {
|
||||
padding: 0 1em;
|
||||
color: #777;
|
||||
border-left: 0.25em solid #ddd;
|
||||
}
|
||||
.markdown-body blockquote>:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.markdown-body blockquote>:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown-body kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
font-size: 11px;
|
||||
line-height: 10px;
|
||||
color: #555;
|
||||
vertical-align: middle;
|
||||
background-color: #fcfcfc;
|
||||
border: solid 1px #ccc;
|
||||
border-bottom-color: #bbb;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 #bbb;
|
||||
}
|
||||
.markdown-body h1,
|
||||
.markdown-body h2,
|
||||
.markdown-body h3,
|
||||
.markdown-body h4,
|
||||
.markdown-body h5,
|
||||
.markdown-body h6 {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 1.25;
|
||||
}
|
||||
.markdown-body h1 .octicon-link,
|
||||
.markdown-body h2 .octicon-link,
|
||||
.markdown-body h3 .octicon-link,
|
||||
.markdown-body h4 .octicon-link,
|
||||
.markdown-body h5 .octicon-link,
|
||||
.markdown-body h6 .octicon-link {
|
||||
color: #000;
|
||||
vertical-align: middle;
|
||||
visibility: hidden;
|
||||
}
|
||||
.markdown-body h1:hover .anchor,
|
||||
.markdown-body h2:hover .anchor,
|
||||
.markdown-body h3:hover .anchor,
|
||||
.markdown-body h4:hover .anchor,
|
||||
.markdown-body h5:hover .anchor,
|
||||
.markdown-body h6:hover .anchor {
|
||||
text-decoration: none;
|
||||
}
|
||||
.markdown-body h1:hover .anchor .octicon-link,
|
||||
.markdown-body h2:hover .anchor .octicon-link,
|
||||
.markdown-body h3:hover .anchor .octicon-link,
|
||||
.markdown-body h4:hover .anchor .octicon-link,
|
||||
.markdown-body h5:hover .anchor .octicon-link,
|
||||
.markdown-body h6:hover .anchor .octicon-link {
|
||||
visibility: visible;
|
||||
}
|
||||
.markdown-body h1 {
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 2em;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.markdown-body h2 {
|
||||
padding-bottom: 0.3em;
|
||||
font-size: 1.5em;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.markdown-body h3 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
.markdown-body h4 {
|
||||
font-size: 1em;
|
||||
}
|
||||
.markdown-body h5 {
|
||||
font-size: 0.875em;
|
||||
}
|
||||
.markdown-body h6 {
|
||||
font-size: 0.85em;
|
||||
color: #777;
|
||||
}
|
||||
.markdown-body ul,
|
||||
.markdown-body ol {
|
||||
padding-left: 2em;
|
||||
}
|
||||
.markdown-body ul ul,
|
||||
.markdown-body ul ol,
|
||||
.markdown-body ol ol,
|
||||
.markdown-body ol ul {
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.markdown-body li>p {
|
||||
margin-top: 16px;
|
||||
}
|
||||
.markdown-body li+li {
|
||||
margin-top: 0.25em;
|
||||
}
|
||||
.markdown-body dl {
|
||||
padding: 0;
|
||||
}
|
||||
.markdown-body dl dt {
|
||||
padding: 0;
|
||||
margin-top: 16px;
|
||||
font-size: 1em;
|
||||
font-style: italic;
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown-body dl dd {
|
||||
padding: 0 16px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.markdown-body table {
|
||||
display: block;
|
||||
width: 100%;
|
||||
overflow: auto;
|
||||
}
|
||||
.markdown-body table th {
|
||||
font-weight: bold;
|
||||
}
|
||||
.markdown-body table th,
|
||||
.markdown-body table td {
|
||||
padding: 6px 13px;
|
||||
border: 1px solid #ddd;
|
||||
}
|
||||
.markdown-body table tr {
|
||||
background-color: #fff;
|
||||
border-top: 1px solid #ccc;
|
||||
}
|
||||
.markdown-body table tr:nth-child(2n) {
|
||||
background-color: #f8f8f8;
|
||||
}
|
||||
.markdown-body img {
|
||||
max-width: 100%;
|
||||
box-sizing: content-box;
|
||||
background-color: #fff;
|
||||
}
|
||||
.markdown-body code {
|
||||
padding: 0;
|
||||
padding-top: 0.2em;
|
||||
padding-bottom: 0.2em;
|
||||
margin: 0;
|
||||
font-size: 85%;
|
||||
background-color: rgba(0,0,0,0.04);
|
||||
border-radius: 3px;
|
||||
}
|
||||
.markdown-body code::before,
|
||||
.markdown-body code::after {
|
||||
letter-spacing: -0.2em;
|
||||
content: "\00a0";
|
||||
}
|
||||
.markdown-body pre {
|
||||
word-wrap: normal;
|
||||
}
|
||||
.markdown-body pre>code {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 100%;
|
||||
word-break: normal;
|
||||
white-space: pre;
|
||||
background: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.markdown-body .highlight {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.markdown-body .highlight pre {
|
||||
margin-bottom: 0;
|
||||
word-break: normal;
|
||||
}
|
||||
.markdown-body .highlight pre,
|
||||
.markdown-body pre {
|
||||
padding: 16px;
|
||||
overflow: auto;
|
||||
font-size: 85%;
|
||||
line-height: 1.45;
|
||||
background-color: #f7f7f7;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.markdown-body pre code {
|
||||
display: inline;
|
||||
max-width: auto;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
overflow: visible;
|
||||
line-height: inherit;
|
||||
word-wrap: normal;
|
||||
background-color: transparent;
|
||||
border: 0;
|
||||
}
|
||||
.markdown-body pre code::before,
|
||||
.markdown-body pre code::after {
|
||||
content: normal;
|
||||
}
|
||||
.markdown-body .pl-0 {
|
||||
padding-left: 0 !important;
|
||||
}
|
||||
.markdown-body .pl-1 {
|
||||
padding-left: 3px !important;
|
||||
}
|
||||
.markdown-body .pl-2 {
|
||||
padding-left: 6px !important;
|
||||
}
|
||||
.markdown-body .pl-3 {
|
||||
padding-left: 12px !important;
|
||||
}
|
||||
.markdown-body .pl-4 {
|
||||
padding-left: 24px !important;
|
||||
}
|
||||
.markdown-body .pl-5 {
|
||||
padding-left: 36px !important;
|
||||
}
|
||||
.markdown-body .pl-6 {
|
||||
padding-left: 48px !important;
|
||||
}
|
||||
.markdown-body .full-commit .btn-outline:not(:disabled):hover {
|
||||
color: #4078c0;
|
||||
border: 1px solid #4078c0;
|
||||
}
|
||||
.markdown-body kbd {
|
||||
display: inline-block;
|
||||
padding: 3px 5px;
|
||||
font: 11px Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
line-height: 10px;
|
||||
color: #555;
|
||||
vertical-align: middle;
|
||||
background-color: #fcfcfc;
|
||||
border: solid 1px #ccc;
|
||||
border-bottom-color: #bbb;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 -1px 0 #bbb;
|
||||
}
|
||||
.markdown-body :checked+.radio-label {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
border-color: #4078c0;
|
||||
}
|
||||
.markdown-body .task-list-item {
|
||||
list-style-type: none;
|
||||
}
|
||||
.markdown-body .task-list-item+.task-list-item {
|
||||
margin-top: 3px;
|
||||
}
|
||||
.markdown-body .task-list-item input {
|
||||
margin: 0 0.2em 0.25em -1.6em;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.markdown-body hr {
|
||||
border-bottom-color: #eee;
|
||||
}
|
||||
`;
|
||||
|
||||
if (document && !document.getElementById('github-markdown-css')) {
|
||||
const styleNode = document.createElement('style');
|
||||
styleNode.id = 'github-markdown-css';
|
||||
styleNode.innerHTML = styles;
|
||||
|
||||
document.head.appendChild(styleNode);
|
||||
}
|
4
addons/jest/src/typings.d.ts
vendored
Normal file
4
addons/jest/src/typings.d.ts
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
// TODO: following packages need definition files or a TS migration
|
||||
|
||||
declare module 'global';
|
||||
declare module '@storybook/components';
|
@ -1 +0,0 @@
|
||||
require('./dist/styles');
|
13
addons/jest/tsconfig.json
Normal file
13
addons/jest/tsconfig.json
Normal file
@ -0,0 +1,13 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src",
|
||||
"types": ["webpack-env"]
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
],
|
||||
"exclude": [
|
||||
"src/__tests__/**/*"
|
||||
]
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-knobs",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook Addon Prop Editor Component",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -22,10 +22,11 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/client-api": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"copy-to-clipboard": "^3.0.8",
|
||||
"core-js": "^2.6.5",
|
||||
"escape-html": "^1.0.3",
|
||||
|
@ -1,10 +1,21 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { navigator } from 'global';
|
||||
import escape from 'escape-html';
|
||||
|
||||
import { getQueryParams } from '@storybook/client-api';
|
||||
|
||||
import KnobStore from './KnobStore';
|
||||
import { SET } from './shared';
|
||||
|
||||
import { deserializers } from './converters';
|
||||
|
||||
const knobValuesFromUrl = 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;
|
||||
|
||||
@ -49,20 +60,36 @@ export default class KnobManager {
|
||||
|
||||
const { knobStore } = this;
|
||||
const existingKnob = knobStore.get(name);
|
||||
|
||||
// We need to return the value set by the knob editor via this.
|
||||
// But, if the user changes the code for the defaultValue we should set
|
||||
// that value instead.
|
||||
if (existingKnob && deepEqual(options.value, existingKnob.defaultValue)) {
|
||||
// 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 &&
|
||||
!navigator.userAgent.includes('jsdom')
|
||||
) {
|
||||
return this.getKnobValue(existingKnob);
|
||||
}
|
||||
|
||||
const defaultValue = options.value;
|
||||
const knobInfo = {
|
||||
...options,
|
||||
name,
|
||||
defaultValue,
|
||||
};
|
||||
|
||||
if (knobValuesFromUrl[name]) {
|
||||
const value = deserializers[options.type](knobValuesFromUrl[name]);
|
||||
|
||||
knobInfo.defaultValue = value;
|
||||
knobInfo.value = value;
|
||||
|
||||
delete knobValuesFromUrl[name];
|
||||
} else {
|
||||
knobInfo.defaultValue = options.value;
|
||||
}
|
||||
|
||||
knobStore.set(name, knobInfo);
|
||||
return this.getKnobValue(knobStore.get(name));
|
||||
}
|
||||
|
@ -1,6 +1,23 @@
|
||||
import { shallow } from 'enzyme'; // eslint-disable-line
|
||||
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', () => {
|
||||
@ -11,15 +28,17 @@ describe('KnobManager', () => {
|
||||
set: jest.fn(),
|
||||
get: () => ({
|
||||
defaultValue: 'default value',
|
||||
value: 'current value',
|
||||
name: 'foo',
|
||||
type: 'string',
|
||||
value: 'current value',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
it('should return the existing knob value when defaults match', () => {
|
||||
it('should return the existing knob value when types match', () => {
|
||||
const defaultKnob = {
|
||||
name: 'foo',
|
||||
type: 'string',
|
||||
value: 'default value',
|
||||
};
|
||||
const knob = testManager.knob('foo', defaultKnob);
|
||||
@ -30,7 +49,8 @@ describe('KnobManager', () => {
|
||||
it('should return the new default knob value when default has changed', () => {
|
||||
const defaultKnob = {
|
||||
name: 'foo',
|
||||
value: 'changed default value',
|
||||
value: true,
|
||||
type: 'boolean',
|
||||
};
|
||||
testManager.knob('foo', defaultKnob);
|
||||
|
||||
|
@ -46,9 +46,9 @@ export default class KnobPanel extends PureComponent {
|
||||
|
||||
componentDidMount() {
|
||||
this.mounted = true;
|
||||
const { channel, api } = this.props;
|
||||
channel.on(SET, this.setKnobs);
|
||||
channel.on(SET_OPTIONS, this.setOptions);
|
||||
const { api } = this.props;
|
||||
api.on(SET, this.setKnobs);
|
||||
api.on(SET_OPTIONS, this.setOptions);
|
||||
|
||||
this.stopListeningOnStory = api.on(STORY_CHANGED, () => {
|
||||
if (this.mounted) {
|
||||
@ -60,9 +60,9 @@ export default class KnobPanel extends PureComponent {
|
||||
|
||||
componentWillUnmount() {
|
||||
this.mounted = false;
|
||||
const { channel } = this.props;
|
||||
const { api } = this.props;
|
||||
|
||||
channel.removeListener(SET, this.setKnobs);
|
||||
api.off(SET, this.setKnobs);
|
||||
this.stopListeningOnStory();
|
||||
}
|
||||
|
||||
@ -72,7 +72,7 @@ export default class KnobPanel extends PureComponent {
|
||||
|
||||
setKnobs = ({ knobs, timestamp }) => {
|
||||
const queryParams = {};
|
||||
const { api, channel } = this.props;
|
||||
const { api } = this.props;
|
||||
|
||||
if (!this.options.timestamps || !timestamp || this.lastEdit <= timestamp) {
|
||||
Object.keys(knobs).forEach(name => {
|
||||
@ -80,15 +80,16 @@ export default class KnobPanel extends PureComponent {
|
||||
// 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) {
|
||||
// If the knob value present in url
|
||||
knob.value = Types[knob.type].deserialize(urlValue);
|
||||
channel.emit(CHANGE, knob);
|
||||
const value = Types[knob.type].deserialize(urlValue);
|
||||
knob.value = value;
|
||||
queryParams[`knob-${name}`] = Types[knob.type].serialize(value);
|
||||
|
||||
api.emit(CHANGE, knob);
|
||||
}
|
||||
}
|
||||
|
||||
// set all knobsquery params to be deleted from URL
|
||||
queryParams[`knob-${name}`] = null;
|
||||
});
|
||||
|
||||
api.setQueryParams(queryParams);
|
||||
@ -99,9 +100,9 @@ export default class KnobPanel extends PureComponent {
|
||||
};
|
||||
|
||||
reset = () => {
|
||||
const { channel } = this.props;
|
||||
const { api } = this.props;
|
||||
|
||||
channel.emit(RESET);
|
||||
api.emit(RESET);
|
||||
};
|
||||
|
||||
copy = () => {
|
||||
@ -119,9 +120,9 @@ export default class KnobPanel extends PureComponent {
|
||||
};
|
||||
|
||||
emitChange = changedKnob => {
|
||||
const { channel } = this.props;
|
||||
const { api } = this.props;
|
||||
|
||||
channel.emit(CHANGE, changedKnob);
|
||||
api.emit(CHANGE, changedKnob);
|
||||
};
|
||||
|
||||
handleChange = changedKnob => {
|
||||
@ -138,9 +139,9 @@ export default class KnobPanel extends PureComponent {
|
||||
};
|
||||
|
||||
handleClick = knob => {
|
||||
const { channel } = this.props;
|
||||
const { api } = this.props;
|
||||
|
||||
channel.emit(CLICK, knob);
|
||||
api.emit(CLICK, knob);
|
||||
};
|
||||
|
||||
render() {
|
||||
@ -233,11 +234,6 @@ export default class KnobPanel extends PureComponent {
|
||||
KnobPanel.propTypes = {
|
||||
active: PropTypes.bool.isRequired,
|
||||
onReset: PropTypes.object, // eslint-disable-line
|
||||
channel: PropTypes.shape({
|
||||
emit: PropTypes.func,
|
||||
on: PropTypes.func,
|
||||
removeListener: PropTypes.func,
|
||||
}).isRequired,
|
||||
api: PropTypes.shape({
|
||||
on: PropTypes.func,
|
||||
getQueryParam: PropTypes.func,
|
||||
|
@ -8,10 +8,6 @@ import Panel from '../Panel';
|
||||
import { CHANGE, SET } from '../../shared';
|
||||
import PropForm from '../PropForm';
|
||||
|
||||
const createTestChannel = () => ({
|
||||
on: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
});
|
||||
const createTestApi = () => ({
|
||||
on: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
@ -27,32 +23,23 @@ jest.mock('react', () => {
|
||||
|
||||
describe('Panel', () => {
|
||||
it('should subscribe to setKnobs event of channel', () => {
|
||||
const testChannel = createTestChannel();
|
||||
const testApi = createTestApi();
|
||||
shallow(<Panel channel={testChannel} api={testApi} active />);
|
||||
expect(testChannel.on).toHaveBeenCalledWith(SET, expect.any(Function));
|
||||
shallow(<Panel api={testApi} active />);
|
||||
expect(testApi.on).toHaveBeenCalledWith(SET, expect.any(Function));
|
||||
});
|
||||
|
||||
it('should subscribe to STORY_CHANGE event', () => {
|
||||
const testChannel = createTestChannel();
|
||||
const testApi = createTestApi();
|
||||
shallow(<Panel channel={testChannel} api={testApi} active />);
|
||||
shallow(<Panel api={testApi} active />);
|
||||
|
||||
expect(testApi.on.mock.calls).toContainEqual([STORY_CHANGED, expect.any(Function)]);
|
||||
expect(testChannel.on).toHaveBeenCalledWith(SET, 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 testChannel = {
|
||||
on: (e, handler) => {
|
||||
handlers[e] = handler;
|
||||
},
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
const testQueryParams = {
|
||||
'knob-foo': 'test string',
|
||||
bar: 'some other string',
|
||||
@ -62,11 +49,12 @@ describe('Panel', () => {
|
||||
on: (e, handler) => {
|
||||
handlers[e] = handler;
|
||||
},
|
||||
emit: jest.fn(),
|
||||
getQueryParam: key => testQueryParams[key],
|
||||
setQueryParams: jest.fn(),
|
||||
};
|
||||
|
||||
shallow(<Panel channel={testChannel} api={testApi} active />);
|
||||
shallow(<Panel api={testApi} active />);
|
||||
const setKnobsHandler = handlers[SET];
|
||||
|
||||
const knobs = {
|
||||
@ -89,75 +77,20 @@ describe('Panel', () => {
|
||||
type: 'text',
|
||||
};
|
||||
const e = CHANGE;
|
||||
expect(testChannel.emit).toHaveBeenCalledWith(e, knobFromUrl);
|
||||
});
|
||||
|
||||
it('should remove query params when url params are already read', () => {
|
||||
const handlers = {};
|
||||
|
||||
const testChannel = {
|
||||
on: (e, handler) => {
|
||||
handlers[e] = handler;
|
||||
},
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
const testQueryParams = {
|
||||
'knob-foo': 'test string',
|
||||
bar: 'some other string',
|
||||
};
|
||||
|
||||
const testApi = {
|
||||
on: (e, handler) => {
|
||||
handlers[e] = handler;
|
||||
},
|
||||
getQueryParam: key => testQueryParams[key],
|
||||
setQueryParams: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallow(<Panel channel={testChannel} 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',
|
||||
},
|
||||
};
|
||||
|
||||
// Make it act like that url params are already checked
|
||||
wrapper.instance().loadedFromUrl = true;
|
||||
|
||||
setKnobsHandler({ knobs, timestamp: +new Date() });
|
||||
const knobFromStory = {
|
||||
'knob-foo': null,
|
||||
'knob-baz': null,
|
||||
};
|
||||
|
||||
expect(testApi.setQueryParams).toHaveBeenCalledWith(knobFromStory);
|
||||
expect(testApi.emit).toHaveBeenCalledWith(e, knobFromUrl);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleChange()', () => {
|
||||
it('should set queryParams and emit knobChange event', () => {
|
||||
const testChannel = {
|
||||
on: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
const testApi = {
|
||||
getQueryParam: jest.fn(),
|
||||
setQueryParams: jest.fn(),
|
||||
on: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
};
|
||||
|
||||
const wrapper = shallow(<Panel channel={testChannel} api={testApi} active />);
|
||||
const wrapper = shallow(<Panel api={testApi} active />);
|
||||
|
||||
const testChangedKnob = {
|
||||
name: 'foo',
|
||||
@ -165,7 +98,7 @@ describe('Panel', () => {
|
||||
type: 'text',
|
||||
};
|
||||
wrapper.instance().handleChange(testChangedKnob);
|
||||
expect(testChannel.emit).toHaveBeenCalledWith(CHANGE, testChangedKnob);
|
||||
expect(testApi.emit).toHaveBeenCalledWith(CHANGE, testChangedKnob);
|
||||
|
||||
// const paramsChange = { 'knob-foo': 'changed text' };
|
||||
// expect(testApi.setQueryParams).toHaveBeenCalledWith(paramsChange);
|
||||
@ -173,12 +106,9 @@ describe('Panel', () => {
|
||||
});
|
||||
|
||||
describe('groups', () => {
|
||||
const testChannel = {
|
||||
on: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
};
|
||||
const testApi = {
|
||||
off: jest.fn(),
|
||||
emit: jest.fn(),
|
||||
getQueryParam: jest.fn(),
|
||||
setQueryParams: jest.fn(),
|
||||
on: jest.fn(() => () => {}),
|
||||
@ -192,11 +122,11 @@ describe('Panel', () => {
|
||||
|
||||
const root = mount(
|
||||
<ThemeProvider theme={convert(themes.light)}>
|
||||
<Panel channel={testChannel} api={testApi} active />
|
||||
<Panel api={testApi} active />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
testChannel.on.mock.calls[0][1]({
|
||||
testApi.on.mock.calls[0][1]({
|
||||
knobs: {
|
||||
foo: {
|
||||
name: 'foo',
|
||||
@ -226,11 +156,11 @@ describe('Panel', () => {
|
||||
it('should have one tab per groupId and an empty Other tab when all are defined', () => {
|
||||
const root = mount(
|
||||
<ThemeProvider theme={convert(themes.light)}>
|
||||
<Panel channel={testChannel} api={testApi} active />
|
||||
<Panel api={testApi} active />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
testChannel.on.mock.calls[0][1]({
|
||||
testApi.on.mock.calls[0][1]({
|
||||
knobs: {
|
||||
foo: {
|
||||
name: 'foo',
|
||||
@ -266,11 +196,11 @@ describe('Panel', () => {
|
||||
it('the Other 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 channel={testChannel} api={testApi} active />
|
||||
<Panel api={testApi} active />
|
||||
</ThemeProvider>
|
||||
);
|
||||
|
||||
testChannel.on.mock.calls[0][1]({
|
||||
testApi.on.mock.calls[0][1]({
|
||||
knobs: {
|
||||
foo: {
|
||||
name: 'foo',
|
||||
|
51
addons/knobs/src/converters.js
Normal file
51
addons/knobs/src/converters.js
Normal file
@ -0,0 +1,51 @@
|
||||
const unconvertable = () => undefined;
|
||||
|
||||
export const converters = {
|
||||
jsonParse: value => JSON.parse(value),
|
||||
jsonStringify: value => JSON.stringify(value),
|
||||
simple: value => value,
|
||||
stringifyIfSet: value => (value === null || value === undefined ? '' : String(value)),
|
||||
stringifyIfTruthy: value => (value ? String(value) : null),
|
||||
toArray: value => {
|
||||
if (Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
return value.split(',');
|
||||
},
|
||||
toBoolean: value => value === 'true',
|
||||
toDate: value => new Date(value).getTime() || new Date().getTime(),
|
||||
toFloat: value => (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,
|
||||
};
|
@ -4,10 +4,9 @@ import Panel from './components/Panel';
|
||||
import { ADDON_ID, PANEL_ID } from './shared';
|
||||
|
||||
addons.register(ADDON_ID, api => {
|
||||
const channel = addons.getChannel();
|
||||
addons.addPanel(PANEL_ID, {
|
||||
title: 'Knobs',
|
||||
// eslint-disable-next-line react/prop-types
|
||||
render: ({ active, key }) => <Panel channel={channel} api={api} key={key} active={active} />,
|
||||
render: ({ active, key }) => <Panel api={api} key={key} active={active} />,
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-links",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Story Links addon for storybook",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -22,9 +22,9 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/router": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"@storybook/router": "5.1.0-alpha.24",
|
||||
"common-tags": "^1.8.0",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
|
@ -6,7 +6,7 @@ Storybook Addon Notes allows you to write notes (text or HTML) for your stories
|
||||
|
||||

|
||||
|
||||
### Getting Started
|
||||
## Getting Started
|
||||
|
||||
**NOTE: Documentation on master branch is for alpha version, stable release is on [master](https://github.com/storybooks/storybook/tree/master/addons/)**
|
||||
|
||||
@ -14,47 +14,65 @@ Storybook Addon Notes allows you to write notes (text or HTML) for your stories
|
||||
yarn add -D @storybook/addon-notes
|
||||
```
|
||||
|
||||
Then create a file called `addons.js` in your storybook config.
|
||||
Then create a file called `addons.js` in your Storybook config.
|
||||
|
||||
Add following content to it:
|
||||
|
||||
```js
|
||||
// register the notes addon as a tab
|
||||
import '@storybook/addon-notes/register';
|
||||
// or register the notes addon as a panel. Only one can be used!
|
||||
import '@storybook/addon-notes/register-panel';
|
||||
```
|
||||
|
||||
You can use the `notes` parameter to add a note to each story:
|
||||
Now, you can use the `notes` parameter to add a note to each story.
|
||||
|
||||
### With React
|
||||
|
||||
```js
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
import Component from './Component';
|
||||
|
||||
storiesOf('Component', module)
|
||||
.add('with some emoji', () => <Component />, {
|
||||
storiesOf('Component', module).add('with some emoji', () => <Component />, {
|
||||
notes: 'A very simple example of addon notes',
|
||||
});
|
||||
```
|
||||
|
||||
### With Vue
|
||||
|
||||
```js
|
||||
import { storiesOf } from '@storybook/vue';
|
||||
|
||||
import MyButton from './MyButton.vue';
|
||||
|
||||
storiesOf('MyButton', module)
|
||||
.add('with some emoji', () => ({
|
||||
components: { MyButton },
|
||||
template: '<my-button>😀 😎 👍 💯</my-button>'
|
||||
}), {
|
||||
notes: 'A very simple example of addon notes',
|
||||
});
|
||||
```
|
||||
|
||||
#### Using Markdown
|
||||
## Using Markdown
|
||||
|
||||
To use markdown in your notes is supported, storybook will load markdown as raw by default.
|
||||
Using Markdown in your notes is supported, Storybook will load Markdown as raw by default.
|
||||
|
||||
```js
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import Component from './Component';
|
||||
import notes from './someMarkdownText.md';
|
||||
|
||||
storiesOf('Component', module)
|
||||
.add('With Markdown', () => <Component />, { notes });
|
||||
storiesOf('Component', module).add('With Markdown', () => <Component />, { notes });
|
||||
```
|
||||
|
||||
### Giphy
|
||||
## Giphy
|
||||
|
||||
When using markdown, you can also embed gifs from Giphy into your markdown. Currently, the value `gif` of the gif prop is used to search and return the first result returned by Giphy.
|
||||
When using Markdown, you can also embed gifs from Giphy into your Markdown. Currently, the value `gif` of the gif prop is used to search and return the first result returned by Giphy.
|
||||
|
||||
```md
|
||||
# Title
|
||||
|
||||
<Giphy gif='cheese' />
|
||||
```
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-notes",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Write notes for your Storybook stories.",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -23,12 +23,12 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/api": "5.1.0-alpha.20",
|
||||
"@storybook/client-logger": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/api": "5.1.0-alpha.24",
|
||||
"@storybook/client-logger": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"markdown-to-jsx": "^6.9.3",
|
||||
"prop-types": "^15.7.2",
|
||||
|
1
addons/notes/register-panel.js
Normal file
1
addons/notes/register-panel.js
Normal file
@ -0,0 +1 @@
|
||||
require('./dist/register.js').default('panel');
|
@ -1 +1 @@
|
||||
require('./dist/register.js');
|
||||
require('./dist/register.js').default('tab');
|
||||
|
@ -6,12 +6,14 @@ import { ADDON_ID, PANEL_ID } from './shared';
|
||||
// TODO: fix eslint in tslint (igor said he fixed it, should ask him)
|
||||
import Panel from './Panel';
|
||||
|
||||
addons.register(ADDON_ID, api => {
|
||||
addons.add(PANEL_ID, {
|
||||
type: types.TAB,
|
||||
title: 'Notes',
|
||||
route: ({ storyId }) => `/info/${storyId}`, // todo add type
|
||||
match: ({ viewMode }) => viewMode === 'info', // todo add type
|
||||
render: ({ active }) => <Panel api={api} active={active} />,
|
||||
export default function register(type: types) {
|
||||
addons.register(ADDON_ID, api => {
|
||||
addons.add(PANEL_ID, {
|
||||
type,
|
||||
title: 'Notes',
|
||||
route: ({ storyId }) => `/info/${storyId}`, // todo add type
|
||||
match: ({ viewMode }) => viewMode === 'info', // todo add type
|
||||
render: ({ active }) => <Panel api={api} active={active} />,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,5 +1,3 @@
|
||||
import { ReactElement } from 'react';
|
||||
|
||||
export const ADDON_ID = 'storybooks/notes';
|
||||
export const PANEL_ID = `${ADDON_ID}/panel`;
|
||||
export const PARAM_KEY = `notes`;
|
||||
|
@ -1,20 +1,18 @@
|
||||
# Storybook Addon On Device Backgrounds
|
||||
# Storybook Backgrounds Addon for react-native
|
||||
|
||||
Storybook On Device Background Addon can be used to change background colors inside the the simulator in [Storybook](https://storybook.js.org).
|
||||
Storybook Backgrounds Addon for react-native can be used to change background colors of your stories right from the device.
|
||||
|
||||
[Framework Support](https://github.com/storybooks/storybook/blob/master/ADDONS_SUPPORT.md)
|
||||
|
||||

|
||||
<img src="docs/demo.gif" alt="Storybook Backgrounds Addon Demo" width="400" />
|
||||
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
npm i -D @storybook/addon-ondevice-backgrounds
|
||||
yarn add -D @storybook/addon-ondevice-backgrounds
|
||||
```
|
||||
|
||||
## Configuration
|
||||
|
||||
Then create a file called `rn-addons.js` in your storybook config.
|
||||
Create a file called `rn-addons.js` in your storybook config.
|
||||
|
||||
Add following content to it:
|
||||
|
||||
@ -23,13 +21,15 @@ import '@storybook/addon-ondevice-backgrounds/register';
|
||||
```
|
||||
|
||||
Then import `rn-addons.js` next to your `getStorybookUI` call.
|
||||
|
||||
```js
|
||||
import './rn-addons';
|
||||
```
|
||||
|
||||
|
||||
## Usage
|
||||
|
||||
react-native users will have to import `storiesOf` from `@storybook/react-native` and are required to add the `withBackgrounds` decorator.
|
||||
|
||||
Then write your stories like this:
|
||||
|
||||
```js
|
||||
@ -37,53 +37,16 @@ import React from 'react';
|
||||
import { storiesOf } from '@storybook/react-native';
|
||||
import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
|
||||
|
||||
storiesOf('Button', module)
|
||||
.addDecorator(
|
||||
withBackgrounds([
|
||||
{ name: 'twitter', value: '#00aced', default: true },
|
||||
{ name: 'facebook', value: '#3b5998' },
|
||||
])
|
||||
)
|
||||
.add('with text', () => <Text>Click me</Text>);
|
||||
```
|
||||
|
||||
You can add the backgrounds to all stories with `addDecorator` in `.storybook/config.js`:
|
||||
|
||||
```js
|
||||
import { addDecorator } from '@storybook/react-native'; // <- or your storybook framework
|
||||
import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
|
||||
|
||||
addDecorator(
|
||||
withBackgrounds([
|
||||
{ name: 'twitter', value: '#00aced', default: true },
|
||||
{ name: 'facebook', value: '#3b5998' },
|
||||
])
|
||||
);
|
||||
```
|
||||
|
||||
If you want to override backgrounds for a single story or group of stories, pass the `backgrounds` parameter:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react-native';
|
||||
addDecorator(withBackgrounds);
|
||||
|
||||
storiesOf('Button', module)
|
||||
.addParameters({
|
||||
backgrounds: [
|
||||
{ name: 'red', value: '#F44336' },
|
||||
{ name: 'blue', value: '#2196F3', default: true },
|
||||
{ name: 'dark', value: '#222222' },
|
||||
{ name: 'light', value: '#eeeeee', default: true },
|
||||
],
|
||||
})
|
||||
.add('with text', () => <button>Click me</button>);
|
||||
.add('with text', () => <Text>Click me</Text>);
|
||||
```
|
||||
|
||||
If you don't want to use backgrounds for a story, you can set the `backgrounds` parameter to `[]`, or use `{ disable: true }` to skip the addon:
|
||||
|
||||
```js
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react-native';
|
||||
|
||||
storiesOf('Button', module).add('with text', () => <button>Click me</button>, {
|
||||
backgrounds: { disable: true },
|
||||
});
|
||||
```
|
||||
See [web backgrounds addon](../backgrounds#usage) for detailed usage and the [crna-kitchen-sink app](../../examples-native/crna-kitchen-sink) for more examples.
|
||||
|
BIN
addons/ondevice-backgrounds/docs/demo.gif
Normal file
BIN
addons/ondevice-backgrounds/docs/demo.gif
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.9 MiB |
Binary file not shown.
Before Width: | Height: | Size: 230 KiB |
@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@storybook/addon-ondevice-backgrounds",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"description": "A storybook addon to show different backgrounds for your preview",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "A react-native storybook addon to show different backgrounds for your preview",
|
||||
"keywords": [
|
||||
"addon",
|
||||
"background",
|
||||
@ -24,7 +24,7 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"prop-types": "^15.7.2"
|
||||
},
|
||||
|
@ -1,16 +1,14 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View, Text } from 'react-native';
|
||||
import Events from './constants';
|
||||
import Events from '@storybook/core-events';
|
||||
import Swatch from './Swatch';
|
||||
import Constants from './constants';
|
||||
|
||||
const defaultBackground = {
|
||||
name: 'default',
|
||||
value: 'transparent',
|
||||
};
|
||||
|
||||
const instructionsHtml = `
|
||||
const codeSample = `
|
||||
import { storiesOf } from '@storybook/react-native';
|
||||
import { withBackgrounds } from '@storybook/addon-ondevice-backgrounds';
|
||||
|
||||
addDecorator(withBackgrounds);
|
||||
|
||||
storiesOf('First Component', module)
|
||||
.addParameters({
|
||||
@ -19,7 +17,7 @@ storiesOf('First Component', module)
|
||||
{ name: 'cool', value: 'deepskyblue' },
|
||||
],
|
||||
})
|
||||
.add("First Button", () => <button>Click me</button>);
|
||||
.add("First Button", () => <Button>Click me</Button>);
|
||||
`.trim();
|
||||
|
||||
const Instructions = () => (
|
||||
@ -33,81 +31,51 @@ const Instructions = () => (
|
||||
<Text>
|
||||
Below is an example of how to add the background decorator to your story definition.
|
||||
</Text>
|
||||
<Text>{instructionsHtml}</Text>
|
||||
<Text>{codeSample}</Text>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default class BackgroundPanel extends Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = { backgrounds: [] };
|
||||
}
|
||||
setBackgroundFromSwatch = background => {
|
||||
this.props.channel.emit(Constants.UPDATE_BACKGROUND, background);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { channel } = this.props;
|
||||
|
||||
this.onSet = channel.on(Events.SET, data => {
|
||||
const backgrounds = [...data];
|
||||
|
||||
this.setState({ backgrounds });
|
||||
});
|
||||
|
||||
this.onUnset = channel.on(Events.UNSET, () => {
|
||||
this.setState({ backgrounds: [] });
|
||||
});
|
||||
this.props.channel.on(Events.SELECT_STORY, this.onStorySelected);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
const { channel } = this.props;
|
||||
channel.removeListener(Events.SET, this.onSet);
|
||||
channel.removeListener(Events.UNSET, this.onUnset);
|
||||
this.props.channel.removeListener(Events.SELECT_STORY, this.onStorySelected);
|
||||
}
|
||||
|
||||
setBackgroundFromSwatch = background => {
|
||||
this.update(background);
|
||||
onStorySelected = selection => {
|
||||
this.setState({ selection });
|
||||
};
|
||||
|
||||
update(background) {
|
||||
const { channel } = this.props;
|
||||
channel.emit(Events.UPDATE_BACKGROUND, background);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { active } = this.props;
|
||||
const { backgrounds = [] } = this.state;
|
||||
const { active, api } = this.props;
|
||||
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
if (!backgrounds.length) return <Instructions />;
|
||||
|
||||
const hasDefault = backgrounds.filter(x => x.default).length;
|
||||
if (!hasDefault) backgrounds.push(defaultBackground);
|
||||
const story = api
|
||||
.store()
|
||||
.getStoryAndParameters(this.state.selection.kind, this.state.selection.story);
|
||||
const backgrounds = story.parameters[Constants.PARAM_KEY];
|
||||
|
||||
return (
|
||||
<View>
|
||||
{backgrounds.map(({ value, name }) => (
|
||||
<View key={`${name} ${value}`}>
|
||||
<Swatch value={value} name={name} setBackground={this.setBackgroundFromSwatch} />
|
||||
</View>
|
||||
))}
|
||||
{backgrounds ? (
|
||||
backgrounds.map(({ value, name }) => (
|
||||
<View key={`${name} ${value}`}>
|
||||
<Swatch value={value} name={name} setBackground={this.setBackgroundFromSwatch} />
|
||||
</View>
|
||||
))
|
||||
) : (
|
||||
<Instructions />
|
||||
)}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
}
|
||||
BackgroundPanel.propTypes = {
|
||||
active: PropTypes.bool.isRequired,
|
||||
api: PropTypes.shape({
|
||||
getQueryParam: PropTypes.func,
|
||||
setQueryParams: PropTypes.func,
|
||||
}).isRequired,
|
||||
channel: PropTypes.shape({
|
||||
emit: PropTypes.func,
|
||||
on: PropTypes.func,
|
||||
removeListener: PropTypes.func,
|
||||
}),
|
||||
};
|
||||
BackgroundPanel.defaultProps = {
|
||||
channel: undefined,
|
||||
};
|
||||
|
@ -5,4 +5,5 @@ export default {
|
||||
SET: `${ADDON_ID}:set`,
|
||||
UNSET: `${ADDON_ID}:unset`,
|
||||
UPDATE_BACKGROUND: `${ADDON_ID}:update`,
|
||||
PARAM_KEY: 'backgrounds',
|
||||
};
|
||||
|
@ -1,28 +1,25 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import PropTypes from 'prop-types';
|
||||
import Events from './constants';
|
||||
import Constants from './constants';
|
||||
|
||||
export default class Container extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { background: props.initialBackground || '' };
|
||||
this.onBackgroundChange = this.onBackgroundChange.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { channel } = this.props;
|
||||
// Listen to the notes and render it.
|
||||
channel.on(Events.UPDATE_BACKGROUND, this.onBackgroundChange);
|
||||
channel.on(Constants.UPDATE_BACKGROUND, this.onBackgroundChange);
|
||||
}
|
||||
|
||||
// This is some cleanup tasks when the Notes panel is unmounting.
|
||||
componentWillUnmount() {
|
||||
const { channel } = this.props;
|
||||
channel.removeListener(Events.UPDATE_BACKGROUND, this.onBackgroundChange);
|
||||
channel.removeListener(Constants.UPDATE_BACKGROUND, this.onBackgroundChange);
|
||||
}
|
||||
|
||||
onBackgroundChange(background) {
|
||||
onBackgroundChange = (background) => {
|
||||
this.setState({ background });
|
||||
}
|
||||
|
||||
|
@ -1,48 +1,32 @@
|
||||
# Storybook Addon On Device Knobs
|
||||
# Storybook Knobs Addon for react-native
|
||||
|
||||
Storybook Addon On Device Knobs allow you to edit React props dynamically using the Storybook UI.
|
||||
You can also use Knobs as a dynamic variable inside stories in [Storybook](https://storybook.js.org).
|
||||
Storybook Knobs Addon allows you to edit react props using the Storybook UI using variables inside stories in [Storybook](https://storybook.js.org).
|
||||
|
||||
[Framework Support](https://github.com/storybooks/storybook/blob/master/ADDONS_SUPPORT.md)
|
||||
|
||||
This is how Knobs look like:
|
||||
|
||||
[](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)
|
||||
|
||||
**This addon is a wrapper for addon [@storybook/addon-knobs](https://github.com/storybooks/storybook/blob/master/addons/knobs).
|
||||
Refer to its documentation to understand how to use knobs**
|
||||
|
||||
|
||||
## Getting Started
|
||||
|
||||
First of all, you need to install knobs into your project.
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add @storybook/addon-ondevice-knobs @storybook/addon-knobs --dev
|
||||
yarn add -D @storybook/addon-ondevice-knobs @storybook/addon-knobs
|
||||
```
|
||||
|
||||
Then create a file called `rn-addons.js` in your storybook config.
|
||||
## Configuration
|
||||
|
||||
Create a file called `rn-addons.js` in your storybook config.
|
||||
|
||||
Add following content to it:
|
||||
|
||||
```js
|
||||
import '@storybook/addon-ondevice-knobs/register';
|
||||
```
|
||||
> `@storybook/addon-ondevice-knobs` use register only.
|
||||
|
||||
|
||||
Then import `rn-addons.js` next to your `getStorybookUI` call.
|
||||
|
||||
```js
|
||||
import './rn-addons';
|
||||
```
|
||||
|
||||
Now, write your stories with knobs.
|
||||
|
||||
**Refer to [@storybook/addon-knobs](https://github.com/storybooks/storybook/blob/master/addons/knobs) to learn how to write stories.**
|
||||
|
||||
**Note:** you'll still have to install `@storybook/addon-knobs` as well and import `withKnobs` and all knob types _(e.g. `select`, `text` etc)_ from that module.
|
||||
|
||||
```js
|
||||
// Example
|
||||
import { withKnobs, text, boolean, number } from '@storybook/addon-knobs';
|
||||
|
||||
// Write your story...
|
||||
```
|
||||
See [@storybook/addon-knobs](https://github.com/storybooks/storybook/blob/master/addons/knobs) to learn how to write stories with knobs and the [crna-kitchen-sink app](../../examples-native/crna-kitchen-sink) for more examples.
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-ondevice-knobs",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Display storybook story knobs on your deviced.",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -21,8 +21,8 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"deep-equal": "^1.0.1",
|
||||
"prop-types": "^15.7.2",
|
||||
|
@ -1,20 +1,18 @@
|
||||
# Storybook Addon On Device Notes
|
||||
# Storybook Notes Addon for react-native
|
||||
|
||||
Storybook Addon On Device Notes allows you to write notes (text or markdown) for your stories in [Storybook](https://storybook.js.org).
|
||||
|
||||
[Framework Support](https://github.com/storybooks/storybook/blob/master/ADDONS_SUPPORT.md)
|
||||
The Notes Addon allows you to write notes (text or markdown) for your stories in [Storybook](https://storybook.js.org).
|
||||
|
||||

|
||||
|
||||
### Getting Started
|
||||
|
||||
**NOTE: Documentation on master branch is for alpha version, stable release is on [master](https://github.com/storybooks/storybook/tree/master/addons/)**
|
||||
## Installation
|
||||
|
||||
```sh
|
||||
yarn add -D @storybook/addon-ondevice-notes
|
||||
```
|
||||
|
||||
Then create a file called `rn-addons.js` in your storybook config.
|
||||
## Configuration
|
||||
|
||||
Create a file called `rn-addons.js` in your storybook config.
|
||||
|
||||
Add following content to it:
|
||||
|
||||
@ -28,17 +26,9 @@ Then import `rn-addons.js` next to your `getStorybookUI` call.
|
||||
import './rn-addons';
|
||||
```
|
||||
|
||||
Then add the `withNotes` decorator to all stories in your `config.js`:
|
||||
## Usage
|
||||
|
||||
```js
|
||||
// Import from @storybook/X where X is your framework
|
||||
import { addDecorator } from '@storybook/react-native';
|
||||
import { withNotes } from '@storybook/addon-ondevice-notes';
|
||||
|
||||
addDecorator(withNotes);
|
||||
```
|
||||
|
||||
You can use the `notes` parameter to add a note to each story:
|
||||
Use the `notes` parameter to add a note to stories:
|
||||
|
||||
```js
|
||||
import { storiesOf } from '@storybook/react-native';
|
||||
@ -49,3 +39,5 @@ storiesOf('Component', module).add('with some emoji', () => <Component />, {
|
||||
notes: 'A very simple component',
|
||||
});
|
||||
```
|
||||
|
||||
See the [crna-kitchen-sink app](../../examples-native/crna-kitchen-sink) for more examples.
|
||||
|
@ -1,11 +1,12 @@
|
||||
{
|
||||
"name": "@storybook/addon-ondevice-notes",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"description": "Write notes for your Storybook stories.",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Write notes for your react-native Storybook stories.",
|
||||
"keywords": [
|
||||
"addon",
|
||||
"notes",
|
||||
"storybook"
|
||||
"storybook",
|
||||
"react-native"
|
||||
],
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@ -19,7 +20,7 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"prop-types": "^15.7.2",
|
||||
"react-native-simple-markdown": "^1.1.0"
|
||||
|
@ -1,42 +0,0 @@
|
||||
import addons from '@storybook/addons';
|
||||
import { withNotes } from '..';
|
||||
|
||||
addons.getChannel = jest.fn();
|
||||
|
||||
describe('Storybook Addon Notes', () => {
|
||||
it('should inject text from `notes` parameter', () => {
|
||||
const channel = { emit: jest.fn() };
|
||||
addons.getChannel.mockReturnValue(channel);
|
||||
|
||||
const getStory = jest.fn();
|
||||
const context = { parameters: { notes: 'hello' } };
|
||||
|
||||
withNotes(getStory, context);
|
||||
expect(channel.emit).toHaveBeenCalledWith('storybook/notes/add_notes', 'hello');
|
||||
expect(getStory).toHaveBeenCalledWith(context);
|
||||
});
|
||||
|
||||
it('should inject text even if no `notes` parameter is set to reset the addon', () => {
|
||||
const channel = { emit: jest.fn() };
|
||||
addons.getChannel.mockReturnValue(channel);
|
||||
|
||||
const getStory = jest.fn();
|
||||
const context = {};
|
||||
|
||||
withNotes(getStory, context);
|
||||
expect(channel.emit).toHaveBeenCalled();
|
||||
expect(getStory).toHaveBeenCalledWith(context);
|
||||
});
|
||||
|
||||
it('should inject markdown from `notes.markdown` parameter', () => {
|
||||
const channel = { emit: jest.fn() };
|
||||
addons.getChannel.mockReturnValue(channel);
|
||||
|
||||
const getStory = jest.fn();
|
||||
const context = { parameters: { notes: { markdown: '# hello' } } };
|
||||
|
||||
withNotes(getStory, context);
|
||||
expect(channel.emit).toHaveBeenCalledWith('storybook/notes/add_notes', '# hello');
|
||||
expect(getStory).toHaveBeenCalledWith(context);
|
||||
});
|
||||
});
|
@ -1,34 +1,3 @@
|
||||
import addons, { makeDecorator } from '@storybook/addons';
|
||||
|
||||
export const withNotes = makeDecorator({
|
||||
name: 'withNotes',
|
||||
parameterName: 'notes',
|
||||
wrapper: (getStory, context, { options, parameters }) => {
|
||||
const channel = addons.getChannel();
|
||||
|
||||
const storyOptions = parameters || options;
|
||||
|
||||
if (!storyOptions) {
|
||||
channel.emit('storybook/notes/add_notes', '');
|
||||
|
||||
return getStory(context);
|
||||
}
|
||||
|
||||
const { text, markdown } =
|
||||
typeof storyOptions === 'string' ? { text: storyOptions } : storyOptions;
|
||||
|
||||
if (!text && !markdown) {
|
||||
throw new Error('You must set of one of `text` or `markdown` on the `notes` parameter');
|
||||
}
|
||||
|
||||
channel.emit('storybook/notes/add_notes', text || markdown);
|
||||
|
||||
return getStory(context);
|
||||
},
|
||||
});
|
||||
|
||||
export const withMarkdownNotes = (text, options) =>
|
||||
withNotes({
|
||||
markdown: text,
|
||||
markdownOptions: options,
|
||||
});
|
||||
if (__DEV__) {
|
||||
console.log("import '@storybook/addon-ondevice-notes/register' to register the notes addon");
|
||||
}
|
@ -1,65 +1,54 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { View } from 'react-native';
|
||||
import Markdown from 'react-native-simple-markdown';
|
||||
import addons from '@storybook/addons';
|
||||
import Events from '@storybook/core-events';
|
||||
|
||||
export class Notes extends React.Component {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
this.state = { text: '' };
|
||||
this.onAddNotes = this.onAddNotes.bind(this);
|
||||
}
|
||||
export const PARAM_KEY = `notes`;
|
||||
|
||||
class Notes extends React.Component {
|
||||
setBackgroundFromSwatch = background => {
|
||||
this.props.channel.emit(Constants.UPDATE_BACKGROUND, background);
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { channel } = this.props;
|
||||
// Listen to the notes and render it.
|
||||
channel.on('storybook/notes/add_notes', this.onAddNotes);
|
||||
this.props.channel.on(Events.SELECT_STORY, this.onStorySelected);
|
||||
}
|
||||
|
||||
// This is some cleanup tasks when the Notes panel is unmounting.
|
||||
componentWillUnmount() {
|
||||
this.unmounted = true;
|
||||
const { channel } = this.props;
|
||||
channel.removeListener('storybook/notes/add_notes', this.onAddNotes);
|
||||
this.props.channel.removeListener(Events.SELECT_STORY, this.onStorySelected);
|
||||
}
|
||||
|
||||
onAddNotes(text) {
|
||||
this.setState({ text });
|
||||
}
|
||||
onStorySelected = selection => {
|
||||
this.setState({ selection });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { active } = this.props;
|
||||
const { text } = this.state;
|
||||
const { active, api } = this.props;
|
||||
|
||||
if (!active) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const story = api
|
||||
.store()
|
||||
.getStoryAndParameters(this.state.selection.kind, this.state.selection.story);
|
||||
const text = story.parameters[PARAM_KEY];
|
||||
|
||||
const textAfterFormatted = text ? text.trim() : '';
|
||||
|
||||
return active ? (
|
||||
return (
|
||||
<View style={{ padding: 10, flex: 1 }}>
|
||||
<Markdown>{textAfterFormatted}</Markdown>
|
||||
</View>
|
||||
) : null;
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Notes.propTypes = {
|
||||
active: PropTypes.bool.isRequired,
|
||||
channel: PropTypes.shape({
|
||||
on: PropTypes.func,
|
||||
emit: PropTypes.func,
|
||||
removeListener: PropTypes.func,
|
||||
}).isRequired,
|
||||
api: PropTypes.shape({
|
||||
on: PropTypes.func,
|
||||
getQueryParam: PropTypes.func,
|
||||
setQueryParams: PropTypes.func,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
addons.register('storybook/notes', api => {
|
||||
const channel = addons.getChannel();
|
||||
addons.addPanel('storybook/notes/panel', {
|
||||
title: 'Notes',
|
||||
// eslint-disable-next-line react/prop-types
|
||||
render: ({ active, key }) => <Notes key={key} channel={channel} api={api} active={active} />,
|
||||
});
|
||||
});
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-options",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Options addon for storybook",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -17,11 +17,12 @@
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/public_api.d.ts",
|
||||
"scripts": {
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
|
@ -3,7 +3,7 @@ import addons, { makeDecorator } from '@storybook/addons';
|
||||
|
||||
import EVENTS from './constants';
|
||||
|
||||
function emitOptions(options) {
|
||||
function emitOptions(options: any) {
|
||||
const channel = addons.getChannel();
|
||||
if (!channel) {
|
||||
throw new Error(
|
||||
@ -21,7 +21,7 @@ function emitOptions(options) {
|
||||
// setOptions function will send Storybook UI options when the channel is
|
||||
// ready. If called before, options will be cached until it can be sent.
|
||||
let globalOptions = {};
|
||||
export const setOptions = deprecate(options => {
|
||||
export const setOptions = deprecate((options: any) => {
|
||||
globalOptions = options;
|
||||
emitOptions(options);
|
||||
}, '`setOptions(options)` is deprecated. Please use the `withOptions(options)` decorator globally.');
|
||||
@ -32,7 +32,7 @@ export const withOptions = makeDecorator({
|
||||
skipIfNoParametersOrOptions: false,
|
||||
wrapper: deprecate((getStory, context, { options: inputOptions, parameters }) => {
|
||||
// do not send hierachy related options over the channel
|
||||
const { hierarchySeparator, hierarchyRootSeparator, ...change } = {
|
||||
const { hierarchySeparator, hierarchyRootSeparator, ...change }: any = {
|
||||
...globalOptions,
|
||||
...inputOptions,
|
||||
...parameters,
|
||||
@ -48,7 +48,7 @@ export const withOptions = makeDecorator({
|
||||
|
||||
// MUTATION !
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
context.options = {
|
||||
(context as any).options = {
|
||||
...globalOptions,
|
||||
...inputOptions,
|
||||
...parameters,
|
||||
@ -61,7 +61,7 @@ export const withOptions = makeDecorator({
|
||||
...inputOptions,
|
||||
...parameters,
|
||||
},
|
||||
});
|
||||
} as any);
|
||||
}, 'withOptions is deprecated, use addParameters({ options: {} }) instead'),
|
||||
});
|
||||
|
2
addons/options/src/public_api.ts
Normal file
2
addons/options/src/public_api.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export { ADDON_ID } from './constants';
|
||||
export { setOptions, withOptions } from './index';
|
1
addons/options/src/typings.d.ts
vendored
Normal file
1
addons/options/src/typings.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
declare var module: any;
|
9
addons/options/tsconfig.json
Normal file
9
addons/options/tsconfig.json
Normal file
@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
},
|
||||
"include": [
|
||||
"src/**/*"
|
||||
]
|
||||
}
|
@ -387,6 +387,15 @@ initStoryshots({
|
||||
});
|
||||
```
|
||||
|
||||
Or, as a more complex example, if we have a package in our `lerna` project called `app` with the path `./packages/app/src/__tests__/storsyhots.js` and the storybook config directory `./packages/app/.storybook`:
|
||||
|
||||
```js
|
||||
import path from 'path';
|
||||
import initStoryshots from '@storybook/addon-storyshots';
|
||||
|
||||
initStoryshots({ configPath: path.resolve(__dirname, '../../.storybook') });
|
||||
```
|
||||
|
||||
`configPath` can also specify path to the `config.js` itself. In this case, config directory will be
|
||||
a base directory of the `configPath`. It may be useful when the `config.js` for test should differ from the
|
||||
original one. It also may be useful for separating tests to different test configs:
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-storyshots",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "StoryShots is a Jest Snapshot Testing Addon for Storybook.",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -25,17 +25,17 @@
|
||||
"storybook": "start-storybook -p 6006"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"glob": "^7.1.3",
|
||||
"global": "^4.3.2",
|
||||
"jest-specific-snapshot": "^1.0.0",
|
||||
"read-pkg-up": "^4.0.0",
|
||||
"jest-specific-snapshot": "^2.0.0",
|
||||
"read-pkg-up": "^5.0.0",
|
||||
"regenerator-runtime": "^0.12.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"enzyme-to-json": "^3.3.5",
|
||||
"jest-emotion": "^10.0.7",
|
||||
"jest-emotion": "^10.0.10",
|
||||
"react": "^16.8.4"
|
||||
},
|
||||
"publishConfig": {
|
||||
|
@ -1,9 +1,15 @@
|
||||
import 'core-js';
|
||||
import 'core-js/es6/reflect';
|
||||
import 'core-js/es7/reflect';
|
||||
import hasDependency from '../hasDependency';
|
||||
import configure from '../configure';
|
||||
|
||||
function setupAngularJestPreset() {
|
||||
// Needed to prevent "Zone.js has detected that ZoneAwarePromise `(window|global).Promise` has been overwritten."
|
||||
require.requireActual('core-js');
|
||||
require.requireActual('core-js/modules/es6.promise');
|
||||
// require.requireActual('core-js/es6/reflect');
|
||||
// require.requireActual('core-js/es7/reflect');
|
||||
|
||||
// Angular + Jest + Storyshots = Crazy Shit:
|
||||
// We need to require 'jest-preset-angular/setupJest' before any storybook code
|
||||
|
@ -22,7 +22,9 @@ function loadFramework(options) {
|
||||
const loader = loaders.find(frameworkLoader => frameworkLoader.test(options));
|
||||
|
||||
if (!loader) {
|
||||
throw new Error('storyshots is intended only to be used with storybook');
|
||||
throw new Error(
|
||||
"Couldn't find an appropriate framework loader -- do you need to set the `frameowrk` option?"
|
||||
);
|
||||
}
|
||||
|
||||
return loader.load(options);
|
||||
|
@ -3,8 +3,8 @@
|
||||
"include": [
|
||||
"src/**/*.ts"
|
||||
],
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
"rootDir": "./src",
|
||||
"experimentalDecorators": true
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-storyshots-puppeteer",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Image snappshots addition to StoryShots base on puppeteer",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -22,8 +22,8 @@
|
||||
"prepare": "node ../../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/node-logger": "5.1.0-alpha.20",
|
||||
"@storybook/router": "5.1.0-alpha.20",
|
||||
"@storybook/node-logger": "5.1.0-alpha.24",
|
||||
"@storybook/router": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"jest-image-snapshot": "^2.8.1",
|
||||
"puppeteer": "^1.12.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-storysource",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Stories addon for storybook",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -22,10 +22,10 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/router": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/router": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"estraverse": "^4.2.0",
|
||||
"loader-utils": "^1.2.3",
|
||||
|
@ -25,6 +25,7 @@ Then, add following content to .storybook/addons.js
|
||||
```js
|
||||
import '@storybook/addon-viewport/register';
|
||||
```
|
||||
You should now be able to see the viewport addon icon in the the toolbar at the top of the screen.
|
||||
|
||||
## Configuration
|
||||
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 193 KiB After Width: | Height: | Size: 121 KiB |
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-viewport",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook addon to change the viewport size to mobile",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -21,11 +21,11 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "5.1.0-alpha.20",
|
||||
"@storybook/client-logger": "5.1.0-alpha.20",
|
||||
"@storybook/components": "5.1.0-alpha.20",
|
||||
"@storybook/core-events": "5.1.0-alpha.20",
|
||||
"@storybook/theming": "5.1.0-alpha.20",
|
||||
"@storybook/addons": "5.1.0-alpha.24",
|
||||
"@storybook/client-logger": "5.1.0-alpha.24",
|
||||
"@storybook/components": "5.1.0-alpha.24",
|
||||
"@storybook/core-events": "5.1.0-alpha.24",
|
||||
"@storybook/theming": "5.1.0-alpha.24",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
"memoizerific": "^1.11.3",
|
||||
|
33
app/angular/index.d.ts
vendored
33
app/angular/index.d.ts
vendored
@ -1,5 +1,28 @@
|
||||
import { NgModuleMetadata, ICollection } from './dist/client/preview/angular/types';
|
||||
export { moduleMetadata } from './dist/client/preview/angular/decorators';
|
||||
/*
|
||||
* ATTENTION:
|
||||
* - moduleMetadata
|
||||
* - NgModuleMetadata
|
||||
* - ICollection
|
||||
*
|
||||
* These typings are coped out of decorators.d.ts and types.d.ts in order to fix a bug with tsc
|
||||
* It was imported out of dist before which was not the proper way of exporting public API
|
||||
*
|
||||
* This can be fixed by migrating app/angular to typescript
|
||||
*/
|
||||
export declare const moduleMetadata: (
|
||||
metadata: Partial<NgModuleMetadata>
|
||||
) => (storyFn: () => any) => any;
|
||||
|
||||
export interface NgModuleMetadata {
|
||||
declarations?: any[];
|
||||
entryComponents?: any[];
|
||||
imports?: any[];
|
||||
schemas?: any[];
|
||||
providers?: any[];
|
||||
}
|
||||
export interface ICollection {
|
||||
[p: string]: any;
|
||||
}
|
||||
|
||||
export interface IStorybookStory {
|
||||
name: string;
|
||||
@ -36,10 +59,16 @@ export interface IApi {
|
||||
|
||||
declare module '@storybook/angular' {
|
||||
export function storiesOf(kind: string, module: NodeModule): IApi;
|
||||
|
||||
export function setAddon(addon: any): void;
|
||||
|
||||
export function addDecorator(decorator: any): IApi;
|
||||
|
||||
export function addParameters(parameters: any): IApi;
|
||||
|
||||
export function configure(loaders: () => void, module: NodeModule): void;
|
||||
|
||||
export function getStorybook(): IStoribookSection[];
|
||||
|
||||
export function forceReRender(): void;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/angular",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook for Angular: Develop Angular Components in isolation with Hot Reloading.",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
@ -26,8 +26,8 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/core": "5.1.0-alpha.20",
|
||||
"@storybook/node-logger": "5.1.0-alpha.20",
|
||||
"@storybook/core": "5.1.0-alpha.24",
|
||||
"@storybook/node-logger": "5.1.0-alpha.24",
|
||||
"angular2-template-loader": "^0.6.2",
|
||||
"core-js": "^2.6.5",
|
||||
"fork-ts-checker-webpack-plugin": "^0.5.2",
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
EventEmitter,
|
||||
SimpleChanges,
|
||||
SimpleChange,
|
||||
ChangeDetectorRef,
|
||||
} from '@angular/core';
|
||||
import { STORY } from '../app.token';
|
||||
import { NgStory, ICollection } from '../types';
|
||||
@ -31,6 +32,7 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
|
||||
constructor(
|
||||
private cfr: ComponentFactoryResolver,
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
@Inject(STORY) private data: Observable<NgStory>
|
||||
) {}
|
||||
|
||||
@ -38,12 +40,18 @@ export class AppComponent implements OnInit, OnDestroy {
|
||||
this.data.pipe(first()).subscribe((data: NgStory) => {
|
||||
this.target.clear();
|
||||
const compFactory = this.cfr.resolveComponentFactory(data.component);
|
||||
const ref = this.target.createComponent(compFactory);
|
||||
const instance = ref.instance;
|
||||
const componentRef = this.target.createComponent(compFactory);
|
||||
const instance = componentRef.instance;
|
||||
// For some reason, manual change detection ref is only working when getting the ref from the injector (rather than componentRef.changeDetectorRef)
|
||||
const childChangeDetectorRef: ChangeDetectorRef = componentRef.injector.get(
|
||||
ChangeDetectorRef
|
||||
);
|
||||
|
||||
this.subscription = this.data.subscribe(newData => {
|
||||
this.setProps(instance, newData);
|
||||
ref.changeDetectorRef.detectChanges();
|
||||
childChangeDetectorRef.markForCheck();
|
||||
// Must detect changes on the current component in order to update any changes in child component's @HostBinding properties (angular/angular#22560)
|
||||
this.changeDetectorRef.detectChanges();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
2
app/angular/src/client/preview/index.d.ts
vendored
2
app/angular/src/client/preview/index.d.ts
vendored
@ -1,3 +1,5 @@
|
||||
import { IApi, IStoribookSection } from '../../../index';
|
||||
|
||||
export function storiesOf(kind: string, module: NodeModule): IApi;
|
||||
export function setAddon(addon: any): void;
|
||||
export function addDecorator(decorator: any): IApi;
|
||||
|
0
app/angular/src/server/build.js
vendored
Executable file → Normal file
0
app/angular/src/server/build.js
vendored
Executable file → Normal file
0
app/angular/src/server/index.js
vendored
Executable file → Normal file
0
app/angular/src/server/index.js
vendored
Executable file → Normal file
@ -5,6 +5,14 @@
|
||||
],
|
||||
"compileOnSave": false,
|
||||
"compilerOptions": {
|
||||
"rootDir": "./src"
|
||||
"outDir": "dist",
|
||||
"emitDecoratorMetadata": true,
|
||||
"experimentalDecorators": true,
|
||||
"declaration": true,
|
||||
"rootDir": "./src",
|
||||
"lib": [
|
||||
"es2017",
|
||||
"dom"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/ember",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook for Ember: Develop Ember Component in isolation with Hot Reloading.",
|
||||
"homepage": "https://github.com/storybooks/storybook/tree/master/app/ember",
|
||||
"bugs": {
|
||||
@ -24,7 +24,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@ember/test-helpers": "^1.5.0",
|
||||
"@storybook/core": "5.1.0-alpha.20",
|
||||
"@storybook/core": "5.1.0-alpha.24",
|
||||
"common-tags": "^1.8.0",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/html",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook for HTML: View HTML snippets in isolation with Hot Reloading.",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
@ -25,7 +25,7 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/core": "5.1.0-alpha.20",
|
||||
"@storybook/core": "5.1.0-alpha.24",
|
||||
"common-tags": "^1.8.0",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/marko",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook for Marko: Develop Marko Component in isolation with Hot Reloading.",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
@ -26,7 +26,7 @@
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/core": "5.1.0-alpha.20",
|
||||
"@storybook/core": "5.1.0-alpha.24",
|
||||
"common-tags": "^1.8.0",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/mithril",
|
||||
"version": "5.1.0-alpha.20",
|
||||
"version": "5.1.0-alpha.24",
|
||||
"description": "Storybook for Mithril: Develop Mithril Component in isolation.",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
@ -27,7 +27,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/plugin-transform-react-jsx": "^7.3.0",
|
||||
"@storybook/core": "5.1.0-alpha.20",
|
||||
"@storybook/core": "5.1.0-alpha.24",
|
||||
"common-tags": "^1.8.0",
|
||||
"core-js": "^2.6.5",
|
||||
"global": "^4.3.2",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user