Merge branch 'next' into pr/rafaelfbs/6347

This commit is contained in:
Norbert de Langen 2019-04-11 16:02:35 +02:00
commit b327281569
295 changed files with 8569 additions and 12578 deletions

View File

@ -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',
],
},
],
};

View File

@ -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
View File

@ -2,7 +2,7 @@
.teamcity/ @hypnosphi
.github/ @danielduan
/addons/a11y/ @jbovenschen
/addons/a11y/ @jbovenschen @codebyalex
/addons/actions/ @rhalff
/addons/backgrounds/ @ndelangen
/addons/centered/ @kazupon

File diff suppressed because it is too large Load Diff

View File

@ -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"

View File

@ -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: [],
},
],
};

View File

@ -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());
}
);
}

View File

@ -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} />

View File

@ -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}
/>
);

View File

@ -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 ? (

View File

@ -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)

View File

@ -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[];

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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

View File

@ -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",

View File

@ -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);
};

View File

@ -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",

View File

@ -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;
}

View File

@ -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",

View File

@ -12,6 +12,7 @@ const styles = {
innerStyle: {
margin: 'auto',
maxHeight: '100%', // Hack for centering correctly in IE11
overflow: 'auto',
},
};

View File

@ -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"

View File

@ -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",

View File

@ -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"

View File

@ -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",

View File

@ -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",

View File

@ -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"

View File

@ -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;

View 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;

View File

@ -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);

View File

@ -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);

View File

@ -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;

View 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;

View File

@ -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;

View 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;

View File

@ -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();

View File

@ -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} />,
});
});

View File

@ -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
View File

@ -0,0 +1,4 @@
// TODO: following packages need definition files or a TS migration
declare module 'global';
declare module '@storybook/components';

View File

@ -1 +0,0 @@
require('./dist/styles');

13
addons/jest/tsconfig.json Normal file
View File

@ -0,0 +1,13 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src",
"types": ["webpack-env"]
},
"include": [
"src/**/*"
],
"exclude": [
"src/__tests__/**/*"
]
}

View File

@ -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",

View File

@ -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));
}

View File

@ -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);

View File

@ -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,

View File

@ -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',

View 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,
};

View File

@ -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} />,
});
});

View File

@ -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",

View File

@ -6,7 +6,7 @@ Storybook Addon Notes allows you to write notes (text or HTML) for your stories
![Storybook Addon Notes Demo](docs/demo.png)
### 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' />
```

View File

@ -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",

View File

@ -0,0 +1 @@
require('./dist/register.js').default('panel');

View File

@ -1 +1 @@
require('./dist/register.js');
require('./dist/register.js').default('tab');

View File

@ -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} />,
});
});
});
}

View File

@ -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`;

View File

@ -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)
![Storybook Addon Backgrounds Demo](docs/demo.png)
<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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB

View File

@ -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"
},

View File

@ -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,
};

View File

@ -5,4 +5,5 @@ export default {
SET: `${ADDON_ID}:set`,
UNSET: `${ADDON_ID}:unset`,
UPDATE_BACKGROUND: `${ADDON_ID}:update`,
PARAM_KEY: 'backgrounds',
};

View File

@ -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 });
}

View File

@ -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:
[![Storybook Knobs Demo](docs/storybook-knobs-example.png)](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.

View File

@ -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",

View File

@ -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).
![Storybook Addon Notes Demo](docs/demo.png)
### 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.

View File

@ -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"

View File

@ -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);
});
});

View File

@ -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");
}

View File

@ -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} />,
});
});

View File

@ -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"
},

View File

@ -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'),
});

View 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
View File

@ -0,0 +1 @@
declare var module: any;

View File

@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "./src"
},
"include": [
"src/**/*"
]
}

View File

@ -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:

View File

@ -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": {

View File

@ -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

View File

@ -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);

View File

@ -3,8 +3,8 @@
"include": [
"src/**/*.ts"
],
"compileOnSave": false,
"compilerOptions": {
"rootDir": "./src"
"rootDir": "./src",
"experimentalDecorators": true
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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

View File

@ -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",

View File

@ -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;
}

View File

@ -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",

View File

@ -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();
});
});
}

View File

@ -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
View File

0
app/angular/src/server/index.js vendored Executable file → Normal file
View File

View File

@ -5,6 +5,14 @@
],
"compileOnSave": false,
"compilerOptions": {
"rootDir": "./src"
"outDir": "dist",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"declaration": true,
"rootDir": "./src",
"lib": [
"es2017",
"dom"
]
}
}

View File

@ -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",

View File

@ -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",

View File

@ -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",

View File

@ -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