mirror of
https://github.com/storybookjs/storybook.git
synced 2025-03-18 05:02:24 +08:00
pulled release/3.3
This commit is contained in:
commit
ffcd4eb6a5
@ -68,6 +68,7 @@ jobs:
|
||||
command: |
|
||||
cd examples/vue-kitchen-sink
|
||||
yarn build-storybook
|
||||
|
||||
- run:
|
||||
name: "Run react kitchen-sink"
|
||||
command: |
|
||||
@ -198,6 +199,54 @@ jobs:
|
||||
command: |
|
||||
yarn test --coverage --runInBand --core
|
||||
yarn coverage
|
||||
cli:
|
||||
working_directory: /tmp/storybook
|
||||
docker:
|
||||
- image: andthensome/docker-node-rsync
|
||||
environment:
|
||||
BASH_ENV: ~/.bashrc
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- dependencies-{{ checksum "yarn.lock" }}
|
||||
- dependencies-
|
||||
- run:
|
||||
name: "Install dependencies"
|
||||
command: |
|
||||
yarn install
|
||||
- run:
|
||||
name: "Bootstrapping"
|
||||
command: |
|
||||
yarn bootstrap --core
|
||||
- run:
|
||||
name: "Testing CLI"
|
||||
command: |
|
||||
yarn test --cli
|
||||
cli-latest-cra:
|
||||
working_directory: /tmp/storybook
|
||||
docker:
|
||||
- image: andthensome/docker-node-rsync
|
||||
environment:
|
||||
BASH_ENV: ~/.bashrc
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
keys:
|
||||
- dependencies-{{ checksum "yarn.lock" }}
|
||||
- dependencies-
|
||||
- run:
|
||||
name: "Install dependencies"
|
||||
command: |
|
||||
yarn install
|
||||
- run:
|
||||
name: "Bootstrapping"
|
||||
command: |
|
||||
yarn bootstrap --core
|
||||
- run:
|
||||
name: "Testing CLI with latest CR(N)A"
|
||||
command: |
|
||||
yarn test-latest-cra
|
||||
deploy:
|
||||
<<: *defaults
|
||||
steps:
|
||||
@ -217,9 +266,7 @@ workflows:
|
||||
requires:
|
||||
- build
|
||||
- docs
|
||||
- lint:
|
||||
requires:
|
||||
- build
|
||||
- unit-test:
|
||||
requires:
|
||||
- build
|
||||
- lint
|
||||
- unit-test
|
||||
- cli
|
||||
- cli-latest-cra
|
||||
|
@ -5,7 +5,7 @@ node_modules
|
||||
addons/**/example/**
|
||||
app/**/demo/**
|
||||
docs/public
|
||||
|
||||
lib/cli/test
|
||||
*.bundle.js
|
||||
*.js.map
|
||||
|
||||
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,6 +2,7 @@ node_modules
|
||||
*.log
|
||||
.idea
|
||||
.vscode
|
||||
*.sw*
|
||||
npm-shrinkwrap.json
|
||||
dist
|
||||
.tern-port
|
||||
|
50
.mailmap
Normal file
50
.mailmap
Normal file
@ -0,0 +1,50 @@
|
||||
# --- instructions --- #
|
||||
|
||||
# Add your account in this format:
|
||||
Your name here <yourname@example.com> # github:my-github-account, npm:my-npm-account, twitter:my-twitter-handle
|
||||
|
||||
# supported:
|
||||
# github, npm, twitter, website
|
||||
|
||||
# --- list ----------- #
|
||||
|
||||
Aaron Mc Adam <aaron@aaronmcadam.com>
|
||||
Aruna Herath <aruna@kadira.io> <arunabherath@gmail.com>
|
||||
Arunoda Susiripala <arunoda.susiripala@gmail.com> Arunoda Susiripala <arunoda.susiripala@gmail.com>
|
||||
Benedikt D Valdez <benediktvaldez@users.noreply.github.com> Benedikt D Valdez <benediktvaldez@users.noreply.github.com>
|
||||
Daniel Duan <dduan@squarespace.com> <dduan@yahoo.com>
|
||||
Daniel James <daniel@thzinc.com> <djames@syncromatics.com>
|
||||
Danny Andrews <danny-andrews@users.noreply.github.com> danny@ownlocal.com>
|
||||
Dustin Kane <dkane@athenahealth.com> <dustinpkane@gmail.com>
|
||||
Eli Sherer <eli.sherer@gmail.com> elish <elish@payoneer.com>
|
||||
Evgeny Kochetkov <evgeny.kochetkov@me.com> Evgeny Kochetkov <evgenykochetkov@users.noreply.github.com>
|
||||
Fabien Bernard <fabien0102@hotmail.com> Fabien BERNARD <fabien0102@hotmail.com>
|
||||
Fernando Daciuk <f.daciuk@gmail.com> <fdaciuk@users.noreply.github.com>
|
||||
Greenkeeper <support@greenkeeper.io> greenkeeper[bot] <greenkeeper[bot]@users.noreply.github.com>
|
||||
Greenkeeper <support@greenkeeper.io> greenkeeperio-bot <support@greenkeeper.io>
|
||||
Jason Schloer <jschloer@Jasons-Mac-Pro.local> jschloer <jschloer@terragotech.com>
|
||||
Jean-Michel Francois <jmfrancois@talend.com> Jean-Michel FRANCOIS <jmfrancois@talend.com>
|
||||
Jeff Carbonella <jeff.carbonella@gmail.com> <jeff@contactually.com>
|
||||
Jeff Knaggs <jeef3@users.noreply.github.com> <mail@jeef3.com>
|
||||
Jordan Gensler <jordan.gensler@airbnb.com> <jordangens@gmail.com>
|
||||
Kanitkorn Sujautra <k.sujautra@gmail.com> Kanitkorn S <lukyth@users.noreply.github.com>
|
||||
Kent C. Dodds <kent@doddsfamily.us> <kent+github@doddsfamily.us>
|
||||
larry <bshy522@gmail.com> <larry@yunify.com>
|
||||
Madushan Nishantha <j.l.madushan@gmail.com> <madushan1000@users.noreply.github.com>
|
||||
Marie-Laure Thuret <mthuret@users.noreply.github.com> mthuret <marielaure.thuret@algolia.com>
|
||||
Max Hodges <max@whiterabbitjapan.com> MaxHodges <max@whiterabbitjapan.com>
|
||||
Michael Shilman <shilman@lab80.co> <shilman@users.noreply.github.com>
|
||||
Michael Shilman <shilman@lab80.co> <michael@lab80.co>
|
||||
Muhammed Thanish <mnmtanish@gmail.com> <mnmtanish@users.noreply.github.com>
|
||||
Ned Schwartz <ned@theinterned.net> Ned Schwartz <ned@theinterned.net>
|
||||
Joe Nelson <Joe.Nelson@regence.com> Nelson, Joe <Joe.Nelson@regence.com>
|
||||
Nikolay Kozhuharenko <Nikolay.Kozhuharenko@gmail.com> Nikolay <Nikolay.Kozhuharenko@gmail.com>
|
||||
Norbert de Langen <ndelangen@me.com> # github:ndelangen, npm:ndelangen, twitter:norbertdelangen
|
||||
Oleg Proskurin <regx@usul.su> UsulPro <regx@usul.su>
|
||||
Orta <orta.therox@gmail.com> orta <orta.therox@gmail.com>
|
||||
Ritesh Kumar <ritz078@users.noreply.github.com> Ritesh Kumar <rkritesh078@gmail.com>
|
||||
Sylvain Bannier <sylvain.bannier@smile.fr> Sylvain BANNIER <sylvain.bannier@smile.fr>
|
||||
Tom Coleman <tom@percolatestudio.com> Tom Coleman <tom@thesnail.org>
|
||||
Trevor Eyre <trevoreyre@gmail.com> # github:TrevorEyre, twitter:trevor_eyre
|
||||
William Castandet <wcastand@gmail.com> wcastand <wcastand@gmail.com>
|
||||
Xavier Cazalot <xavier.cazalot@gmail.com> xavcz <xavier.cazalot@gmail.com>
|
145
CHANGELOG.md
145
CHANGELOG.md
@ -1,101 +1,104 @@
|
||||
# 3.2.16
|
||||
# 3.3.0-alpha.2
|
||||
|
||||
2017-November-15
|
||||
2017-October-03
|
||||
|
||||
#### Features
|
||||
|
||||
- Add addon-a11y to monorepo [#2292](https://github.com/storybooks/storybook/pull/2292)
|
||||
- Ability for custom storyshots testFunctions to utilise "snapshot per story file" [#1841](https://github.com/storybooks/storybook/pull/1841)
|
||||
- Viewport Addon [#1753](https://github.com/storybooks/storybook/pull/1753)
|
||||
- More detailed props table [#1485](https://github.com/storybooks/storybook/pull/1485)
|
||||
- RN: Add accessibility labels to OnDeviceUI [#1780](https://github.com/storybooks/storybook/pull/1780)
|
||||
- Have Stories on each level of hierarchy [#1763](https://github.com/storybooks/storybook/pull/1763)
|
||||
- Viewport Addon [#1740](https://github.com/storybooks/storybook/pull/1740)
|
||||
- Generate snapshot per story file [#1584](https://github.com/storybooks/storybook/pull/1584)
|
||||
- addon-links: add `LinkTo` component, and `hrefTo` function [#1829](https://github.com/storybooks/storybook/pull/1829)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Addon actions: replace eval with function name assignment [#2321](https://github.com/storybooks/storybook/pull/2321)
|
||||
- FIX propwarning on basebutton && ADD style prop on basebutton [#2305](https://github.com/storybooks/storybook/pull/2305)
|
||||
- React-native: fix drawer width [#2300](https://github.com/storybooks/storybook/pull/2300)
|
||||
- CLI: Use actions in sample stories for vue + fix them in SFC_VUE template [#1867](https://github.com/storybooks/storybook/pull/1867)
|
||||
- Improve rendering of 'types' in addon-actions [#1887](https://github.com/storybooks/storybook/pull/1887)
|
||||
- Circular json can possibly hang [#1881](https://github.com/storybooks/storybook/pull/1881)
|
||||
- Use HtmlWebpackPlugin to import all assets (importing chunks in order) [#1775](https://github.com/storybooks/storybook/pull/1775)
|
||||
- Fix preview scrolling [#1782](https://github.com/storybooks/storybook/pull/1782)
|
||||
- Search box: make found options selectable with click [#1697](https://github.com/storybooks/storybook/pull/1697)
|
||||
- Fix Docgen in static builds for Info [#1725](https://github.com/storybooks/storybook/pull/1725)
|
||||
- Return empty array when Array knob is empty [#1811](https://github.com/storybooks/storybook/pull/1811)
|
||||
|
||||
#### Documentation
|
||||
|
||||
- Make dependencies more deterministic [#1703](https://github.com/storybooks/storybook/pull/1703)
|
||||
- Synced changes from new-docs to CONTRIBUTING.md [#1911](https://github.com/storybooks/storybook/pull/1911)
|
||||
- Fix incorrect yarn command in docs [#1758](https://github.com/storybooks/storybook/pull/1758)
|
||||
|
||||
#### Maintenance
|
||||
|
||||
- Add Previews of deployed examples via Netlify [#2304](https://github.com/storybooks/storybook/pull/2304)
|
||||
- Drop "Install latest yarn version" step on CI [#1910](https://github.com/storybooks/storybook/pull/1910)
|
||||
- CLI: A more human-friendly message for undetected project types [#1825](https://github.com/storybooks/storybook/pull/1825)
|
||||
- CLI: handle promise rejections [#1826](https://github.com/storybooks/storybook/pull/1826)
|
||||
- Add tests for CLI [#1767](https://github.com/storybooks/storybook/pull/1767)
|
||||
- Yarn workspaces [#1810](https://github.com/storybooks/storybook/pull/1810)
|
||||
- Knobs: allow arrays in object knob proptypes [#1701](https://github.com/storybooks/storybook/pull/1701)
|
||||
- Deprecate confusing option names [#1692](https://github.com/storybooks/storybook/pull/1692)
|
||||
- A CLI for running specific tests suites, like bootstrap CLI [#1752](https://github.com/storybooks/storybook/pull/1752)
|
||||
- Remove check for sender on channel. [#1407](https://github.com/storybooks/storybook/pull/1407)
|
||||
- Exit with code 1 if `start-storybook --smoke-test` fails [#1851](https://github.com/storybooks/storybook/pull/1851)
|
||||
- Refactor CLI [#1840](https://github.com/storybooks/storybook/pull/1840)
|
||||
- Refactor knobs - no longer include all runtimes [#1832](https://github.com/storybooks/storybook/pull/1832)
|
||||
- Added addon-knobs to crna and vanilla react native. [#1636](https://github.com/storybooks/storybook/pull/1636)
|
||||
|
||||
#### Dependency Upgrades
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
30 upgrades
|
||||
</summary>
|
||||
- Add config for dependencies.io [#1770](https://github.com/storybooks/storybook/pull/1770)
|
||||
|
||||
- Update 5 dependencies from npm [#2312](https://github.com/storybooks/storybook/pull/2312)
|
||||
- Upgraded gatsby-link in `docs` from `1.6.27` to `1.6.28` [#2311](https://github.com/storybooks/storybook/pull/2311)
|
||||
- Upgraded gatsby-plugin-sharp in `docs` from `1.6.20` to `1.6.21` [#2311](https://github.com/storybooks/storybook/pull/2311)
|
||||
- Upgraded gatsby-remark-images in `docs` from `1.5.31` to `1.5.32` [#2311](https://github.com/storybooks/storybook/pull/2311)
|
||||
- Upgraded gatsby in `docs` from `1.9.108` to `1.9.112` [#2308](https://github.com/storybooks/storybook/pull/2308)
|
||||
- Upgraded gatsby-link in `docs` from `1.6.26` to `1.6.27` [#2308](https://github.com/storybooks/storybook/pull/2308)
|
||||
- Upgraded gatsby-remark-copy-linked-files in `docs` from `1.5.20` to `1.5.21` [#2308](https://github.com/storybooks/storybook/pull/2308)
|
||||
- Upgraded gatsby-transformer-remark in `docs` from `1.7.20` to `1.7.21` [#2308](https://github.com/storybooks/storybook/pull/2308)
|
||||
- Upgraded react-textarea-autosize in `addons/events` from `5.2.0` to `5.2.1` [#2309](https://github.com/storybooks/storybook/pull/2309)
|
||||
- Upgraded react-datetime in `addons/knobs` from `2.10.3` to `2.11.0` [#2309](https://github.com/storybooks/storybook/pull/2309)
|
||||
- Upgraded react-textarea-autosize in `addons/knobs` from `5.2.0` to `5.2.1` [#2309](https://github.com/storybooks/storybook/pull/2309)
|
||||
- Upgraded react-textarea-autosize in `addons/comments` from `5.2.0` to `5.2.1` [#2309](https://github.com/storybooks/storybook/pull/2309)
|
||||
- Upgraded moment in `addons/knobs` from `2.19.1` to `2.19.2` [#2293](https://github.com/storybooks/storybook/pull/2293)
|
||||
- Upgraded moment in `addons/comments` from `2.19.1` to `2.19.2` [#2293](https://github.com/storybooks/storybook/pull/2293)
|
||||
- Upgraded gatsby in `docs` from `1.9.100` to `1.9.108` [#2294](https://github.com/storybooks/storybook/pull/2294)
|
||||
- Upgraded gatsby-link in `docs` from `1.6.24` to `1.6.26` [#2294](https://github.com/storybooks/storybook/pull/2294)
|
||||
- Upgraded gatsby-plugin-sharp in `docs` from `1.6.19` to `1.6.20` [#2294](https://github.com/storybooks/storybook/pull/2294)
|
||||
- Upgraded gatsby-remark-autolink-headers in `docs` from `1.4.7` to `1.4.8` [#2294](https://github.com/storybooks/storybook/pull/2294)
|
||||
- Upgraded gatsby-remark-copy-linked-files in `docs` from `1.5.16` to `1.5.20` [#2294](https://github.com/storybooks/storybook/pull/2294)
|
||||
- Upgraded gatsby-remark-images in `docs` from `1.5.30` to `1.5.31` [#2294](https://github.com/storybooks/storybook/pull/2294)
|
||||
- Upgraded gatsby-source-filesystem in `docs` from `1.5.7` to `1.5.8` [#2294](https://github.com/storybooks/storybook/pull/2294)
|
||||
- Upgraded gatsby-transformer-remark in `docs` from `1.7.19` to `1.7.20` [#2294](https://github.com/storybooks/storybook/pull/2294)
|
||||
- Update lint-staged to 5.0.0 [#2291](https://github.com/storybooks/storybook/pull/2291)
|
||||
- Upgraded eslint in `/` from `4.10.0` to `4.11.0` [#2290](https://github.com/storybooks/storybook/pull/2290)
|
||||
- Upgraded puppeteer in `/` from `0.12.0` to `0.13.0` [#2290](https://github.com/storybooks/storybook/pull/2290)
|
||||
- Update 6 dependencies from npm [#2286](https://github.com/storybooks/storybook/pull/2286)
|
||||
- Update React to 16.1.0 [#2285](https://github.com/storybooks/storybook/pull/2285)
|
||||
- Update 4 dependencies from npm [#2284](https://github.com/storybooks/storybook/pull/2284)
|
||||
- use @storybook published deprecated dependencies [#2314](https://github.com/storybooks/storybook/pull/2314)
|
||||
- Update inquirer to 4.0.0 [#2298](https://github.com/storybooks/storybook/pull/2298)
|
||||
# 3.3.0-alpha.0
|
||||
|
||||
</details>
|
||||
|
||||
# 3.2.15
|
||||
|
||||
2017-November-10
|
||||
2017-September-06
|
||||
|
||||
#### Features
|
||||
|
||||
- Optimizing for iphone x [#2260](https://github.com/storybooks/storybook/pull/2260)
|
||||
- Fix accessibility warnings [#2270](https://github.com/storybooks/storybook/pull/2270)
|
||||
- Viewport addon: simulate device sizes in preview window [#1753](https://github.com/storybooks/storybook/pull/1753)
|
||||
- CLI: Add codemod for deprecated addon-links and addon-actions from app [#1368](https://github.com/storybooks/storybook/pull/1368)
|
||||
- Info addon: More detailed props table [#1485](https://github.com/storybooks/storybook/pull/1485)
|
||||
- React native: Add accessibility labels to OnDeviceUI [#1780](https://github.com/storybooks/storybook/pull/1780)
|
||||
- Stories panel: Stories on each hierarchy level [#1763](https://github.com/storybooks/storybook/pull/1763)
|
||||
- Storyshots: Generate snapshot per story file [#1584](https://github.com/storybooks/storybook/pull/1584)
|
||||
- CLI: Add support for Vue projects using Nuxt [#1794](https://github.com/storybooks/storybook/pull/1794)
|
||||
|
||||
#### Bug Fixes
|
||||
|
||||
- Fix propTypes in addon-background [#2279](https://github.com/storybooks/storybook/pull/2279)
|
||||
- Addon-info: allow duplicate displayNames [#2269](https://github.com/storybooks/storybook/pull/2269)
|
||||
- Fix browser navigation [#2261](https://github.com/storybooks/storybook/pull/2261)
|
||||
- Import chunks/assets in correct order using HtmlWebpackPlugin [#1775](https://github.com/storybooks/storybook/pull/1775)
|
||||
- Fix preview scrolling [#1782](https://github.com/storybooks/storybook/pull/1782)
|
||||
- Height aligned 2 buttons in manager's header [#1769](https://github.com/storybooks/storybook/pull/1769)
|
||||
- Search box: make found options selectable with click [#1697](https://github.com/storybooks/storybook/pull/1697)
|
||||
- Info addon: Fix Docgen in static builds [#1725](https://github.com/storybooks/storybook/pull/1725)
|
||||
- Knobs: allow arrays in object knob proptypes [#1701](https://github.com/storybooks/storybook/pull/1701)
|
||||
|
||||
#### Documentation
|
||||
|
||||
- Improve linkTo documentation [#1793](https://github.com/storybooks/storybook/pull/1793)
|
||||
- Add carbon to examples page [#1764](https://github.com/storybooks/storybook/pull/1764)
|
||||
- Minor grammar fixes and clarification to Vue documentation [#1756](https://github.com/storybooks/storybook/pull/1756)
|
||||
- Fix incorrect yarn command in docs [#1758](https://github.com/storybooks/storybook/pull/1758)
|
||||
- Add storybook-chrome-screenshot to addon gallery [#1761](https://github.com/storybooks/storybook/pull/1761)
|
||||
- Fixing typo on VueJS withNotes Example [#1787](https://github.com/storybooks/storybook/pull/1787)
|
||||
|
||||
#### Maintenance
|
||||
|
||||
- Fixes to build scripts for Windows. [#2051](https://github.com/storybooks/storybook/pull/2051)
|
||||
- Update dependencies.yml to include batch updates for docs dependencies [#2252](https://github.com/storybooks/storybook/pull/2252)
|
||||
- Deprecate confusing option names [#1692](https://github.com/storybooks/storybook/pull/1692)
|
||||
- A CLI for running specific tests suites, like bootstrap CLI [#1752](https://github.com/storybooks/storybook/pull/1752)
|
||||
- Remove check for sender on channel. [#1407](https://github.com/storybooks/storybook/pull/1407)
|
||||
- Use yarn instead of NPM [#1703](https://github.com/storybooks/storybook/pull/1703)
|
||||
- Add config for dependencies.io [#1770](https://github.com/storybooks/storybook/pull/1770)
|
||||
- Added addon-knobs to crna and vanilla react native. [#1636](https://github.com/storybooks/storybook/pull/1636)
|
||||
- Fixed Jest warnings [#1744](https://github.com/storybooks/storybook/pull/1744)
|
||||
- Smoke test master [#1801](https://github.com/storybooks/storybook/pull/1801)
|
||||
|
||||
#### Dependency Upgrades
|
||||
|
||||
<details>
|
||||
<summary>
|
||||
11 PRs
|
||||
</summary>
|
||||
|
||||
- Update 4 dependencies from npm [#2267](https://github.com/storybooks/storybook/pull/2267)
|
||||
- Update 8 dependencies from npm [#2262](https://github.com/storybooks/storybook/pull/2262)
|
||||
- Update 3 dependencies from npm [#2257](https://github.com/storybooks/storybook/pull/2257)
|
||||
- Update babel-eslint in / from 8.0.1 to 8.0.2 [#2253](https://github.com/storybooks/storybook/pull/2253)
|
||||
- 3 packages updated by dependencies.io [#2251](https://github.com/storybooks/storybook/pull/2251)
|
||||
- Update devDependencies [#2232](https://github.com/storybooks/storybook/pull/2232)
|
||||
- Update react-textarea-autosize to 5.1.0 [#2233](https://github.com/storybooks/storybook/pull/2233)
|
||||
- Update insert-css to 2.0.0 [#2234](https://github.com/storybooks/storybook/pull/2234)
|
||||
- Update file-loader to 1.1.5 [#2236](https://github.com/storybooks/storybook/pull/2236)
|
||||
- Update read-pkg-up to 3.0.0 [#2237](https://github.com/storybooks/storybook/pull/2237)
|
||||
- Update react-modal to 3.1.0 [#2238](https://github.com/storybooks/storybook/pull/2238)
|
||||
|
||||
</details>
|
||||
- Upgrade root dependencies and sync with packages [#1802](https://github.com/storybooks/storybook/pull/1802)
|
||||
- Update jest to the latest version 🚀 [#1799](https://github.com/storybooks/storybook/pull/1799)
|
||||
- Update eslint-plugin-jest to the latest version 🚀 [#1795](https://github.com/storybooks/storybook/pull/1795)
|
||||
- Update lerna to the latest version 🚀 [#1768](https://github.com/storybooks/storybook/pull/1768)
|
||||
|
||||
# 3.2.14
|
||||
|
||||
|
205
CONTRIBUTING.md
205
CONTRIBUTING.md
@ -22,7 +22,7 @@ No software is bug free. So, if you got an issue, follow these steps:
|
||||
|
||||
To test your project against the current latest version of storybook, you can clone the repository and link it with `yarn`. Try following these steps:
|
||||
|
||||
1. Download the latest version of this project, and build it:
|
||||
#### 1. Download the latest version of this project, and build it:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/storybooks/storybook.git
|
||||
@ -31,46 +31,87 @@ yarn install
|
||||
yarn bootstrap --core
|
||||
```
|
||||
|
||||
2. Link `storybook` and any other required dependencies:
|
||||
The bootstrap command will ask which sections of the codebase you want to bootstrap. Unless you're going to work with ReactNative or the Documentation, you can keep the default.
|
||||
|
||||
You can also pick directly from CLI:
|
||||
|
||||
yarn bootstrap --core
|
||||
|
||||
#### 2a. Run unit tests
|
||||
|
||||
You can use one of the example projects in `examples/` to develop on.
|
||||
|
||||
This command will list all the suites and options for running tests.
|
||||
|
||||
```sh
|
||||
cd app/react
|
||||
yarn link
|
||||
|
||||
cd <your-project>
|
||||
yarn link @storybook/react
|
||||
|
||||
# repeat with whichever other parts of the monorepo you are using.
|
||||
yarn test
|
||||
```
|
||||
|
||||
### Reproductions
|
||||
_Note that in order to run the tests fro ReactNative, you must have bootstrapped with ReactNative enabled_
|
||||
|
||||
The best way to help figure out an issue you are having is to produce a minimal reproduction against the `master` branch.
|
||||
|
||||
A good way to do that is using the example `cra-kitchen-sink` app embedded in this repository:
|
||||
You can also pick suites from CLI:
|
||||
|
||||
```sh
|
||||
# Download and build this repository:
|
||||
git clone https://github.com/storybooks/storybook.git
|
||||
cd storybook
|
||||
yarn install
|
||||
yarn bootstrap --core
|
||||
|
||||
cd examples/cra-kitchen-sink
|
||||
|
||||
# make changes to try and reproduce the problem, such as adding components + stories
|
||||
yarn storybook
|
||||
|
||||
# see if you can see the problem, if so, commit it:
|
||||
git checkout "branch-describing-issue"
|
||||
git add -A
|
||||
git commit -m "reproduction for issue #123"
|
||||
|
||||
# fork the storybook repo to your account, then add the resulting remote
|
||||
git remote add <your-username> https://github.com/<your-username>/storybook.git
|
||||
git push -u <your-username> master
|
||||
yarn test --core
|
||||
```
|
||||
|
||||
In order to run ALL unit tests, you must have bootstrapped the react-native
|
||||
|
||||
#### 2b. Run e2e tests for CLI
|
||||
|
||||
If you made any changes to `lib/cli` package, the easiest way to verify that it doesn't break anything is to run e2e tests:
|
||||
|
||||
yarn test --cli
|
||||
|
||||
This will run a bash script located at `lib/cli/test/run_tests.sh`. It will copy the contents of `fixtures` into a temporary `run` directory, run `getstorybook` in each of the subdirectories, and check that storybook starts successfully using `yarn storybook --smoke-test`.
|
||||
|
||||
After that, the `run` directory content will be compared with `snapshots`. You can update the snapshots by passing an `--update` flag:
|
||||
|
||||
yarn test --cli --update
|
||||
|
||||
In that case, please check the git diff before commiting to make sure it only contains the intended changes.
|
||||
|
||||
#### 2c. Link `storybook` and any other required dependencies:
|
||||
|
||||
If you want to test your own existing project using the github version of storybook, you need to `link` the packages you use in your project.
|
||||
|
||||
````sh
|
||||
cd app/react
|
||||
yarn link
|
||||
|
||||
cd <your-project>
|
||||
yarn link @storybook/react
|
||||
|
||||
# repeat with whichever other parts of the monorepo you are using.
|
||||
```
|
||||
|
||||
### Reproductions
|
||||
|
||||
The best way to help figure out an issue you are having is to produce a minimal reproduction against the `master` branch.
|
||||
|
||||
A good way to do that is using the example `cra-kitchen-sink` app embedded in this repository:
|
||||
|
||||
```sh
|
||||
# Download and build this repository:
|
||||
git clone https://github.com/storybooks/storybook.git
|
||||
cd storybook
|
||||
yarn install
|
||||
yarn bootstrap --core
|
||||
|
||||
# make changes to try and reproduce the problem, such as adding components + stories
|
||||
cd examples/cra-kitchen-sink
|
||||
yarn storybook
|
||||
|
||||
# see if you can see the problem, if so, commit it:
|
||||
git checkout "branch-describing-issue"
|
||||
git add -A
|
||||
git commit -m "reproduction for issue #123"
|
||||
|
||||
# fork the storybook repo to your account, then add the resulting remote
|
||||
git remote add <your-username> https://github.com/<your-username>/storybook.git
|
||||
git push -u <your-username> master
|
||||
````
|
||||
|
||||
If you follow that process, you can then link to the github repository in the issue. See <https://github.com/storybooks/storybook/issues/708#issuecomment-290589886> for an example.
|
||||
|
||||
**NOTE**: If your issue involves a webpack config, create-react-app will prevent you from modifying the _app's_ webpack config, however you can still modify storybook's to mirror your app's version of storybook. Alternatively, use `yarn eject` in the CRA app to get a modifiable webpack config.
|
||||
@ -186,51 +227,95 @@ If an issue is a `bug`, and it doesn't have a clear reproduction that you have p
|
||||
|
||||
> If you want to work on a UI feature, refer to the [Storybook UI](https://github.com/storybooks/storybook/tree/master/lib/ui) page.
|
||||
|
||||
This project written in ES2016+ syntax so, we need to transpile it before use.
|
||||
So run the following command:
|
||||
### Prerequisites
|
||||
|
||||
```sh
|
||||
yarn dev
|
||||
```
|
||||
Please have the **_latest_** stable versions of the following on your machine
|
||||
|
||||
This will watch files and transpile in watch mode.
|
||||
- node
|
||||
- yarn
|
||||
|
||||
### Linking
|
||||
### Initial Setup
|
||||
|
||||
First of all link this repo with:
|
||||
If you run into trouble here, make sure your node, npm, and **_yarn_** are on the latest versions (yarn at least v1.0.0).
|
||||
|
||||
```sh
|
||||
yarn link
|
||||
```
|
||||
1. `cd ~` (optional)
|
||||
2. `git clone https://github.com/storybooks/storybook.git` _bonus_: use your own fork for this step
|
||||
3. `cd storybook`
|
||||
4. `yarn`
|
||||
5. `yarn bootstrap --core`
|
||||
6. `yarn test --core`
|
||||
7. `yarn dev` _You must have this running for your changes to show up_
|
||||
|
||||
In order to test features you add, you may need to link the local copy of this repo.
|
||||
For that we need a sample project. Let's create it.
|
||||
#### Bootstrapping everything
|
||||
|
||||
```sh
|
||||
yarn global add create-react-app getstorybook
|
||||
create-react-app my-demo-app
|
||||
cd my-demo-app
|
||||
getstorybook
|
||||
```
|
||||
_This method is slow_
|
||||
|
||||
> It's pretty important to create a very simple sample project like above.
|
||||
> Otherwise some of the functionality won't work because of linking.
|
||||
1. `yarn bootstrap --all`
|
||||
2. Have a beer 🍺
|
||||
3. `yarn test` (to verify everything worked)
|
||||
|
||||
Then link storybook inside the sample project with:
|
||||
### Working with the kitchen sink apps
|
||||
|
||||
```sh
|
||||
yarn link @storybook/react
|
||||
```
|
||||
Within the `examples` folder of the Storybook repo, you will find kitchen sink examples of storybook implementations for the various platforms that storybook supports.
|
||||
|
||||
### Getting Changes
|
||||
Not only do these show many of the options and addons available, they are also automatically linked to all the development packages. We highly encourage you to use these to develop/test contributions on.
|
||||
|
||||
After you've done any change, you need to run the `yarn storybook` command every time to see those changes.
|
||||
#### React and Vue
|
||||
|
||||
1. `yarn storybook`
|
||||
2. Verify that your local version works
|
||||
|
||||
### Working with your own app
|
||||
|
||||
#### Linking Storybook
|
||||
|
||||
Storybook is broken up into sub-projects that you can install as you need them. For this example we will be working with `@storybook/react`.
|
||||
**Note:** You need to `yarn link` from inside the sub project you are working on **_NOT_** the storybook root directory
|
||||
|
||||
1. `cd app/react`
|
||||
2. `yarn link`
|
||||
|
||||
#### Connecting Your App To Storybook
|
||||
|
||||
**_Note:_** If you aren't seeing addons after linking storybook, you probably have a versioning issue which can be fixed by simply linking each addon you want to use.
|
||||
This applies for the kitchen sink apps as well as your own projects.
|
||||
|
||||
_Make sure `yarn dev` is running_
|
||||
|
||||
##### 1. Setup storybook in your project
|
||||
|
||||
First we are going to install storyboook, then we are going to link `@storybook/react` into our project. This will replace `node_modules/@storybook/react` with a symlink to our local version of storybook.
|
||||
|
||||
1. `getstorybook`
|
||||
2. `yarn storybook`
|
||||
3. Verify that your local version works
|
||||
|
||||
##### 2. Link
|
||||
|
||||
**_Note_**: This process is the same for `@storybook/vue`, `@storybook/addon-foo`, etc
|
||||
|
||||
1. Go to your storybook _root_ directory
|
||||
2. `yarn dev`
|
||||
3. Wait until the output stops (changes you make will be transpiled into dist and logged here)
|
||||
4. Go to your storybook-sandbox-app directory
|
||||
5. `yarn link @storybook/react`
|
||||
6. `yarn storybook`
|
||||
|
||||
#### Verify your local version is working
|
||||
|
||||
You should now have a working storybook dev environment up and running. To verify this you can make changes to the following file:
|
||||
|
||||
`open app/react/src/client/manager/preview.js`
|
||||
|
||||
Save and go to `http://localhost:9009` (or wherever storybook is running)
|
||||
|
||||
If you don't see the changes rerun `yarn storybook` again in your sandbox app
|
||||
|
||||
## Release Guide
|
||||
|
||||
This section is for Storybook maintainers who will be creating releases. It assumes:
|
||||
|
||||
- yarn >= 1.0.0 (otherwise you should pass a -- before command arguments)
|
||||
- yarn >= 1.0.0
|
||||
- you've yarn linked `pr-log` from <https://github.com/storybooks/pr-log/pull/2>
|
||||
|
||||
The current manual release sequence is as follows:
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-actions",
|
||||
"version": "3.2.16",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "Action Logger addon for storybook",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
@ -21,9 +21,8 @@
|
||||
"storybook": "start-storybook -p 9001"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "^3.2.16",
|
||||
"@storybook/addons": "^3.3.0-alpha.2",
|
||||
"deep-equal": "^1.0.1",
|
||||
"json-stringify-safe": "^5.0.1",
|
||||
"prop-types": "^15.6.0",
|
||||
"react-inspector": "^2.2.1",
|
||||
"uuid": "^3.1.0"
|
||||
|
@ -5,7 +5,7 @@ import style from './style';
|
||||
|
||||
class ActionLogger extends Component {
|
||||
getActionData() {
|
||||
return this.props.actions.map((action, i) => this.renderAction(action, i));
|
||||
return this.props.actions.map(action => this.renderAction(action));
|
||||
}
|
||||
|
||||
renderAction(action) {
|
||||
@ -15,7 +15,8 @@ class ActionLogger extends Component {
|
||||
<div style={style.countwrap}>{action.count > 1 && counter}</div>
|
||||
<div style={style.inspector}>
|
||||
<Inspector
|
||||
showNonenumerable
|
||||
sortObjectKeys
|
||||
showNonenumerable={false}
|
||||
name={action.data.name}
|
||||
data={action.data.args || action.data}
|
||||
/>
|
||||
|
@ -3,6 +3,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import deepEqual from 'deep-equal';
|
||||
import { CYCLIC_KEY, isObject, retrocycle } from '../../util';
|
||||
|
||||
import ActionLoggerComponent from '../../components/ActionLogger/';
|
||||
import { EVENT_ID } from '../../';
|
||||
@ -23,10 +24,12 @@ export default class ActionLogger extends React.Component {
|
||||
}
|
||||
|
||||
addAction(action) {
|
||||
action.data.args = action.data.args.map(arg => JSON.parse(arg)); // eslint-disable-line
|
||||
action.data.args = action.data.args.map(arg => retrocycle(arg)); // eslint-disable-line
|
||||
const isCyclic = !!action.data.args.find(arg => isObject(arg) && arg[CYCLIC_KEY]);
|
||||
const actions = [...this.state.actions];
|
||||
const previous = actions.length && actions[0];
|
||||
if (previous && deepEqual(previous.data, action.data, { strict: true })) {
|
||||
|
||||
if (previous && !isCyclic && deepEqual(previous.data, action.data, { strict: true })) {
|
||||
previous.count++; // eslint-disable-line
|
||||
} else {
|
||||
action.count = 1; // eslint-disable-line
|
||||
|
@ -1,21 +1,14 @@
|
||||
/* eslint-disable no-underscore-dangle */
|
||||
|
||||
import addons from '@storybook/addons';
|
||||
import stringify from 'json-stringify-safe';
|
||||
import uuid from 'uuid/v1';
|
||||
import { EVENT_ID } from './';
|
||||
|
||||
function _format(arg) {
|
||||
if (arg && typeof arg.preventDefault !== 'undefined') {
|
||||
return stringify('[SyntheticEvent]');
|
||||
}
|
||||
return stringify(arg);
|
||||
}
|
||||
import { decycle } from './util';
|
||||
|
||||
export function action(name) {
|
||||
// eslint-disable-next-line no-unused-vars, func-names
|
||||
const handler = function(..._args) {
|
||||
const args = Array.from(_args).map(_format);
|
||||
const args = Array.from(_args).map(arg => JSON.stringify(decycle(arg)));
|
||||
const channel = addons.getChannel();
|
||||
const id = uuid();
|
||||
channel.emit(EVENT_ID, {
|
||||
|
@ -23,5 +23,17 @@ describe('preview', () => {
|
||||
expect(channel.emit.mock.calls[0][1].id).toBe('42');
|
||||
expect(channel.emit.mock.calls[1][1].id).toBe('24');
|
||||
});
|
||||
it('should be able to handle cyclic object without hanging', () => {
|
||||
const cyclicObject = {
|
||||
propertyA: {
|
||||
innerPropertyA: {},
|
||||
},
|
||||
propertyB: 'b',
|
||||
};
|
||||
cyclicObject.propertyA.innerPropertyA = cyclicObject;
|
||||
|
||||
expect(() => JSON.stringify(cyclicObject)).toThrow();
|
||||
expect(() => action('foo')(cyclicObject)).not.toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
129
addons/actions/src/util.js
Normal file
129
addons/actions/src/util.js
Normal file
@ -0,0 +1,129 @@
|
||||
export const CLASS_NAME_KEY = '$___storybook.className';
|
||||
export const CYCLIC_KEY = '$___storybook.isCyclic';
|
||||
|
||||
export function muteProperty(key, value) {
|
||||
return Object.defineProperty(value, key, { enumerable: false });
|
||||
}
|
||||
|
||||
export function isObject(value) {
|
||||
return Object.prototype.toString.call(value) === '[object Object]';
|
||||
}
|
||||
|
||||
export function createFakeConstructor(obj) {
|
||||
function FakeConstructor(data) {
|
||||
Object.assign(this, data);
|
||||
}
|
||||
|
||||
Object.defineProperty(FakeConstructor, 'name', {
|
||||
value: obj[CLASS_NAME_KEY],
|
||||
});
|
||||
|
||||
delete obj[CLASS_NAME_KEY]; // eslint-disable-line no-param-reassign
|
||||
|
||||
return new FakeConstructor(obj);
|
||||
}
|
||||
|
||||
export function reviver(key, value) {
|
||||
if (isObject(value) && value[CLASS_NAME_KEY]) {
|
||||
return createFakeConstructor(value);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
// Based on: https://github.com/douglascrockford/JSON-js/blob/master/cycle.js
|
||||
export function decycle(object, depth = 15) {
|
||||
const objects = new WeakMap();
|
||||
let isCyclic = false;
|
||||
|
||||
return (function derez(value, path, _depth) {
|
||||
let oldPath;
|
||||
let obj;
|
||||
|
||||
if (Object(value) === value && _depth > depth) {
|
||||
return `[${value.constructor.name}...]`;
|
||||
}
|
||||
|
||||
if (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
!(value instanceof Boolean) &&
|
||||
!(value instanceof Date) &&
|
||||
!(value instanceof Number) &&
|
||||
!(value instanceof RegExp) &&
|
||||
!(value instanceof String)
|
||||
) {
|
||||
oldPath = objects.get(value);
|
||||
if (oldPath !== undefined) {
|
||||
isCyclic = true;
|
||||
|
||||
return { $ref: oldPath };
|
||||
}
|
||||
|
||||
objects.set(value, path);
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
obj = [];
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
obj[i] = derez(value[i], `${path}[${i}]`, _depth + 1);
|
||||
}
|
||||
} else {
|
||||
obj = { [CLASS_NAME_KEY]: value.constructor ? value.constructor.name : 'Object' };
|
||||
|
||||
Object.keys(value).forEach(name => {
|
||||
obj[name] = derez(value[name], `${path}[${JSON.stringify(name)}]`, _depth + 1);
|
||||
});
|
||||
}
|
||||
|
||||
if (_depth === 0 && isObject(value) && isCyclic) {
|
||||
obj[CYCLIC_KEY] = true;
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
return value;
|
||||
})(object, '$', 0);
|
||||
}
|
||||
|
||||
export function retrocycle(json) {
|
||||
const pathReg = /^\$(?:\[(?:\d+|"(?:[^\\"\u0000-\u001f]|\\([\\"/bfnrt]|u[0-9a-zA-Z]{4}))*")])*$/;
|
||||
|
||||
const $ = JSON.parse(json, reviver);
|
||||
|
||||
(function rez(value) {
|
||||
if (value && typeof value === 'object') {
|
||||
if (Array.isArray(value)) {
|
||||
for (let i = 0; i < value.length; i += 1) {
|
||||
const item = value[i];
|
||||
if (item && typeof item === 'object') {
|
||||
const path = item.$ref;
|
||||
if (typeof path === 'string' && pathReg.test(path)) {
|
||||
value[i] = eval(path); // eslint-disable-line no-eval, no-param-reassign
|
||||
} else {
|
||||
rez(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Object.keys(value).forEach(name => {
|
||||
const item = value[name];
|
||||
|
||||
if (typeof item === 'object' && item !== null) {
|
||||
const path = item.$ref;
|
||||
|
||||
if (typeof path === 'string' && pathReg.test(path)) {
|
||||
value[name] = eval(path); // eslint-disable-line no-eval, no-param-reassign
|
||||
} else {
|
||||
rez(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
})($);
|
||||
|
||||
muteProperty(CYCLIC_KEY, $);
|
||||
|
||||
return $;
|
||||
}
|
@ -1,6 +1,10 @@
|
||||
{
|
||||
"name": "@storybook/addon-centered",
|
||||
<<<<<<< HEAD
|
||||
"version": "3.2.16",
|
||||
=======
|
||||
"version": "3.3.0-alpha.2",
|
||||
>>>>>>> d997c5724823ef51c644c6135f8ad619d893eb0a
|
||||
"description": "Storybook decorator to center components",
|
||||
"license": "MIT",
|
||||
"author": "Muhammed Thanish <mnmtanish@gmail.com>",
|
||||
|
@ -1,7 +0,0 @@
|
||||
// Use the line below to register this addon
|
||||
// import '@storybook/addon-comments/register';
|
||||
import '@storybook/addon-actions/register';
|
||||
import '@kadira/storybook-database-cloud/register';
|
||||
|
||||
import { init } from '../src/manager';
|
||||
init();
|
Binary file not shown.
Before Width: | Height: | Size: 151 KiB |
@ -1,3 +0,0 @@
|
||||
const preview = require('./dist/preview');
|
||||
|
||||
preview.init();
|
@ -1,30 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
const buttonStyles = {
|
||||
border: '1px solid #eee',
|
||||
borderRadius: 3,
|
||||
backgroundColor: '#FFFFFF',
|
||||
cursor: 'pointer',
|
||||
fontSize: 15,
|
||||
padding: '3px 10px',
|
||||
};
|
||||
|
||||
const Button = ({ children, onClick, style = {} }) => (
|
||||
<button style={{ ...buttonStyles, ...style }} onClick={onClick}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
Button.defaultProps = {
|
||||
onClick: () => {},
|
||||
style: {},
|
||||
};
|
||||
|
||||
Button.propTypes = {
|
||||
children: PropTypes.string.isRequired,
|
||||
onClick: PropTypes.func,
|
||||
style: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
export default Button;
|
@ -1,95 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import { window } from 'global';
|
||||
import Textarea from 'react-textarea-autosize';
|
||||
import marked from 'marked';
|
||||
import style from './style';
|
||||
|
||||
const renderer = new marked.Renderer();
|
||||
renderer.heading = text => text;
|
||||
|
||||
marked.setOptions({
|
||||
renderer,
|
||||
gfm: true,
|
||||
tables: false,
|
||||
breaks: true,
|
||||
pedantic: false,
|
||||
sanitize: true,
|
||||
smartLists: true,
|
||||
smartypants: false,
|
||||
});
|
||||
|
||||
export default class CommentForm extends Component {
|
||||
constructor(props, ...args) {
|
||||
super(props, ...args);
|
||||
this.state = { text: '' };
|
||||
}
|
||||
|
||||
onChange(e) {
|
||||
const text = e.target.value;
|
||||
this.setState({ text });
|
||||
}
|
||||
|
||||
onSubmit() {
|
||||
const { addComment } = this.props;
|
||||
const text = this.state.text.trim();
|
||||
if (!text || text === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
addComment(marked(text));
|
||||
this.setState({ text: '' });
|
||||
}
|
||||
|
||||
openLogin() {
|
||||
const signInUrl = `https://hub.getstorybook.io/sign-in?redirectUrl=${encodeURIComponent(
|
||||
window.location.href
|
||||
)}`;
|
||||
window.location.href = signInUrl;
|
||||
}
|
||||
|
||||
handleKeyDown(e) {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
e.preventDefault();
|
||||
this.onSubmit();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
if (!this.props.user) {
|
||||
return (
|
||||
<div style={style.wrapper}>
|
||||
<Textarea style={style.input} disabled />
|
||||
<button style={style.submitButton} onClick={() => this.openLogin()}>
|
||||
Sign in with Storybook Hub
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { text } = this.state;
|
||||
return (
|
||||
<div style={style.wrapper}>
|
||||
<Textarea
|
||||
style={style.input}
|
||||
onChange={e => this.onChange(e)}
|
||||
onKeyDown={e => this.handleKeyDown(e)}
|
||||
placeholder="Add your comment..."
|
||||
value={text}
|
||||
/>
|
||||
<button style={style.submitButton} onClick={() => this.onSubmit()}>
|
||||
Submit
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentForm.defaultProps = {
|
||||
user: null,
|
||||
addComment: () => {},
|
||||
};
|
||||
CommentForm.propTypes = {
|
||||
user: PropTypes.object, // eslint-disable-line react/forbid-prop-types
|
||||
addComment: PropTypes.func,
|
||||
};
|
@ -1,39 +0,0 @@
|
||||
const button = {
|
||||
boxSizing: 'border-box',
|
||||
height: 30,
|
||||
border: 'none',
|
||||
outline: 'none',
|
||||
background: '#fafafa',
|
||||
padding: '7px 15px',
|
||||
fontSize: 12,
|
||||
lineHeight: 1,
|
||||
color: 'rgba(0, 0, 0, 0.5)',
|
||||
};
|
||||
|
||||
export default {
|
||||
wrapper: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
borderTop: '1px solid rgb(234, 234, 234)',
|
||||
},
|
||||
submitButton: {
|
||||
...button,
|
||||
cursor: 'pointer',
|
||||
borderRadius: '0 0 4px 0',
|
||||
},
|
||||
input: {
|
||||
flex: 1,
|
||||
boxSizing: 'border-box',
|
||||
height: 30,
|
||||
maxHeight: 70,
|
||||
border: 'none',
|
||||
borderRadius: '0 0 0 4px',
|
||||
outline: 'none',
|
||||
padding: '5px 10px',
|
||||
fontSize: 13,
|
||||
lineHeight: 1.6,
|
||||
color: 'rgba(0, 0, 0, 0.8)',
|
||||
fontFamily: 'sans-serif',
|
||||
resize: 'none',
|
||||
},
|
||||
};
|
@ -1,51 +0,0 @@
|
||||
export default `
|
||||
.comment-content p {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.comment-content pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
padding: 5px 8px;
|
||||
color: #000;
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 0.94em;
|
||||
border-radius: 3px;
|
||||
background-color: #F8F8F8;
|
||||
border: 1px solid #f1f1f1;
|
||||
}
|
||||
|
||||
.comment-content pre code {
|
||||
border: 0px !important;
|
||||
background: transparent !important;
|
||||
line-height: 1.3em;
|
||||
}
|
||||
|
||||
.comment-content code {
|
||||
padding: 0 3px 0 3px;
|
||||
color: #000;
|
||||
font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace;
|
||||
font-size: 0.94em;
|
||||
border-radius: 3px;
|
||||
background-color: #F8F8F8;
|
||||
border: 1px solid #f1f1f1;
|
||||
}
|
||||
|
||||
.comment-content blockquote {
|
||||
color: #666666;
|
||||
margin: 3px 0;
|
||||
padding-left: 12px;
|
||||
border-left: 0.5em #EEE solid;
|
||||
}
|
||||
|
||||
.comment-content ul, .comment-content ol {
|
||||
margin: 1em 0;
|
||||
padding: 0 0 0 2em;
|
||||
}
|
||||
|
||||
.comment-content a {
|
||||
color: #0645ad;
|
||||
text-decoration: none;
|
||||
}
|
||||
`;
|
@ -1,65 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import style from './style';
|
||||
import CommentItem from '../CommentItem';
|
||||
|
||||
export default class CommentList extends Component {
|
||||
componentDidMount() {
|
||||
this.wrapper.scrollTop = this.wrapper.scrollHeight;
|
||||
}
|
||||
|
||||
componentDidUpdate(prev) {
|
||||
if (this.props.comments.length !== prev.comments.length) {
|
||||
this.wrapper.scrollTop = this.wrapper.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { comments } = this.props;
|
||||
|
||||
if (comments.length === 0) {
|
||||
return (
|
||||
<div
|
||||
ref={el => {
|
||||
this.wrapper = el;
|
||||
}}
|
||||
style={style.wrapper}
|
||||
>
|
||||
<div style={style.noComments}>No Comments Yet!</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={el => {
|
||||
this.wrapper = el;
|
||||
}}
|
||||
style={style.wrapper}
|
||||
>
|
||||
{comments.map(comment => (
|
||||
<CommentItem
|
||||
key={comment.id}
|
||||
comment={comment}
|
||||
ownComment={comment.userId === (this.props.user && this.props.user.id)}
|
||||
deleteComment={() => this.props.deleteComment(comment.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CommentList.defaultProps = {
|
||||
comments: [],
|
||||
user: null,
|
||||
deleteComment: () => {},
|
||||
};
|
||||
|
||||
CommentList.propTypes = {
|
||||
comments: PropTypes.arrayOf(PropTypes.object),
|
||||
user: PropTypes.shape({
|
||||
id: PropTypes.number,
|
||||
}),
|
||||
deleteComment: PropTypes.func,
|
||||
};
|
@ -1,12 +0,0 @@
|
||||
export default {
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
overflow: 'auto',
|
||||
padding: '7px 15px',
|
||||
},
|
||||
noComments: {
|
||||
fontFamily: 'sans-serif',
|
||||
fontSize: 13,
|
||||
padding: '10px 0',
|
||||
},
|
||||
};
|
@ -1,45 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import CommentList from '../CommentList';
|
||||
import CommentForm from '../CommentForm';
|
||||
import style from './style';
|
||||
|
||||
export default function CommentsPanel(props) {
|
||||
if (props.loading) {
|
||||
return (
|
||||
<div style={style.wrapper}>
|
||||
<div style={style.message}>loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (props.appNotAvailable) {
|
||||
const appsUrl = 'https://hub.getstorybook.io/apps';
|
||||
return (
|
||||
<div style={style.wrapper}>
|
||||
<div style={style.message}>
|
||||
<a style={style.button} href={appsUrl}>
|
||||
Create an app for this repo on Storybook Hub
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div style={style.wrapper}>
|
||||
<CommentList key="list" {...props} />
|
||||
<CommentForm key="form" {...props} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
CommentsPanel.defaultProps = {
|
||||
loading: false,
|
||||
appNotAvailable: false,
|
||||
};
|
||||
|
||||
CommentsPanel.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
appNotAvailable: PropTypes.bool,
|
||||
};
|
@ -1,29 +0,0 @@
|
||||
export default {
|
||||
wrapper: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
position: 'relative',
|
||||
},
|
||||
message: {
|
||||
flex: 1,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
fontFamily: 'sans-serif',
|
||||
color: 'rgb(68, 68, 68)',
|
||||
fontSize: 11,
|
||||
letterSpacing: 1,
|
||||
textDecoration: 'none',
|
||||
textTransform: 'uppercase',
|
||||
},
|
||||
button: {
|
||||
textDecoration: 'none',
|
||||
color: '#444',
|
||||
padding: '10px 18px',
|
||||
background: 'rgb(240, 240, 240)',
|
||||
borderRadius: 5,
|
||||
textTransform: 'none',
|
||||
fontSize: 12,
|
||||
},
|
||||
};
|
@ -1,236 +0,0 @@
|
||||
import deepEquals from 'deep-equal';
|
||||
import { EventEmitter } from 'events';
|
||||
|
||||
export default class DataStore {
|
||||
constructor(db) {
|
||||
this.db = db;
|
||||
this.currentStory = null;
|
||||
this.callbacks = [];
|
||||
this.cache = {};
|
||||
this.users = {};
|
||||
this.user = null;
|
||||
|
||||
this.eventStore = new EventEmitter();
|
||||
}
|
||||
|
||||
addToCache(currentStory, comments) {
|
||||
const key = this.getStoryKey(currentStory);
|
||||
this.cache[key] = {
|
||||
comments,
|
||||
addedAt: Date.now(),
|
||||
};
|
||||
}
|
||||
|
||||
getFromCache(currentStory) {
|
||||
const key = this.getStoryKey(currentStory);
|
||||
const item = this.cache[key];
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
comments: [],
|
||||
invalidated: false,
|
||||
};
|
||||
}
|
||||
|
||||
const { comments } = item;
|
||||
let invalidated = false;
|
||||
|
||||
// invalid caches created 60 minutes ago.
|
||||
if (Date.now() - item.addedAt > 1000 * 60) {
|
||||
delete this.cache[key];
|
||||
invalidated = true;
|
||||
}
|
||||
|
||||
return { comments, invalidated };
|
||||
}
|
||||
|
||||
reloadCurrentComments() {
|
||||
if (this.stopReloading) {
|
||||
clearInterval(this.stopReloading);
|
||||
}
|
||||
|
||||
this.stopReloading = setInterval(
|
||||
() => {
|
||||
this.loadUsers().then(() => this.loadComments());
|
||||
},
|
||||
1000 * 60 // Reload for every minute
|
||||
);
|
||||
}
|
||||
|
||||
setCurrentStory(sbKind, sbStory) {
|
||||
this.currentStory = { sbKind, sbStory };
|
||||
|
||||
// We don't need to do anything if the there's no loggedIn user.
|
||||
// if (!this.user) return;
|
||||
|
||||
this.reloadCurrentComments();
|
||||
const item = this.getFromCache(this.currentStory);
|
||||
|
||||
if (item) {
|
||||
this.fireComments(item.comments);
|
||||
// if the cache invalidated we need to load comments again.
|
||||
if (item.invalidated) {
|
||||
return this.loadUsers().then(() => this.loadComments());
|
||||
}
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// load comments for the first time.
|
||||
// TODO: send a null and handle the loading part in the UI side.
|
||||
this.eventStore.emit('loading', true);
|
||||
this.fireComments([]);
|
||||
this.loadUsers()
|
||||
.then(() => this.loadComments())
|
||||
.then(() => {
|
||||
this.eventStore.emit('loading', false);
|
||||
return Promise.resolve(null);
|
||||
});
|
||||
|
||||
return this.currentStory;
|
||||
}
|
||||
|
||||
setCurrentUser(user) {
|
||||
this.user = user;
|
||||
}
|
||||
|
||||
loadUsers() {
|
||||
const query = {};
|
||||
const options = { limit: 1e6 };
|
||||
return this.db.persister.getAppInfo().then(info => {
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
return this.db
|
||||
.getCollection('users')
|
||||
.get(query, options)
|
||||
.then(users => {
|
||||
this.users = users.reduce((newUsers, user) => {
|
||||
const usersObj = {
|
||||
...newUsers,
|
||||
};
|
||||
usersObj[user.id] = user;
|
||||
return usersObj;
|
||||
}, {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadComments() {
|
||||
const currentStory = { ...this.currentStory };
|
||||
const query = currentStory;
|
||||
const options = { limit: 1e6 };
|
||||
return this.db.persister.getAppInfo().then(info => {
|
||||
if (!info) {
|
||||
return null;
|
||||
}
|
||||
return this.db
|
||||
.getCollection('comments')
|
||||
.get(query, options)
|
||||
.then(comments => {
|
||||
// add to cache
|
||||
this.addToCache(currentStory, comments);
|
||||
|
||||
// set comments only if we are on the relavant story
|
||||
if (deepEquals(currentStory, this.currentStory)) {
|
||||
this.fireComments(comments);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getStoryKey(currentStory) {
|
||||
return `${currentStory.sbKind}:::${currentStory.sbStory}`;
|
||||
}
|
||||
|
||||
fireComments(comments) {
|
||||
this.callbacks.forEach(callback => {
|
||||
// link user to the comment directly
|
||||
const commentsWithUser = comments.map(comment =>
|
||||
Object.assign({}, comment, { user: this.users[comment.userId] })
|
||||
);
|
||||
callback(commentsWithUser);
|
||||
});
|
||||
}
|
||||
|
||||
onComments(cb) {
|
||||
this.callbacks.push(cb);
|
||||
const stop = () => {
|
||||
const index = this.callbacks.indexOf(cb);
|
||||
this.callbacks.splice(index, 1);
|
||||
};
|
||||
|
||||
return stop;
|
||||
}
|
||||
|
||||
addPendingComment(comment) {
|
||||
// Add the pending comment.
|
||||
const pendingComment = { ...comment, loading: true };
|
||||
const { comments: existingComments } = this.getFromCache(this.currentStory);
|
||||
const updatedComments = existingComments.concat(pendingComment);
|
||||
|
||||
this.fireComments(updatedComments);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
setDeletedComment(commentId) {
|
||||
const { comments } = this.getFromCache(this.currentStory);
|
||||
const deleted = comments.find(c => c.id === commentId);
|
||||
if (deleted) {
|
||||
deleted.loading = true;
|
||||
}
|
||||
this.fireComments(comments);
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
addAuthorToTheDatabase() {
|
||||
if (this.users[this.user.id]) {
|
||||
// user exists in the DB.
|
||||
return Promise.resolve(null);
|
||||
}
|
||||
|
||||
// add user to the local cache
|
||||
this.users[this.user.id] = this.user;
|
||||
|
||||
// add user to the actual collection
|
||||
return this.db.getCollection('users').set(this.user);
|
||||
}
|
||||
|
||||
// NOTE the "sbProtected" makes sure only the author can modify
|
||||
// or delete a comment after its saved on the cloud database.
|
||||
addCommentToDatabase(comment) {
|
||||
const doc = {
|
||||
...comment,
|
||||
...this.currentStory,
|
||||
...this.currentStory,
|
||||
sbProtected: true,
|
||||
};
|
||||
|
||||
return this.db.getCollection('comments').set(doc);
|
||||
}
|
||||
|
||||
deleteCommentOnDatabase(commentId) {
|
||||
const query = { id: commentId };
|
||||
return this.db.getCollection('comments').del(query);
|
||||
}
|
||||
|
||||
addComment(comment) {
|
||||
return this.addAuthorToTheDatabase()
|
||||
.then(() => this.addPendingComment(comment))
|
||||
.then(() => this.addCommentToDatabase(comment))
|
||||
.then(() => this.loadUsers())
|
||||
.then(() => this.loadComments());
|
||||
}
|
||||
|
||||
deleteComment(commentId) {
|
||||
return this.setDeletedComment(commentId)
|
||||
.then(() => this.deleteCommentOnDatabase(commentId))
|
||||
.then(() => this.loadComments());
|
||||
}
|
||||
|
||||
onLoading(cb) {
|
||||
this.eventStore.on('loading', cb);
|
||||
return () => {
|
||||
this.eventStore.removeListener('loading', cb);
|
||||
};
|
||||
}
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
import DataStore from './dataStore';
|
||||
|
||||
const user = {
|
||||
id: 'user-id',
|
||||
name: 'user-name',
|
||||
};
|
||||
|
||||
const dbGetUsers = jest.fn(() => Promise.resolve([user]));
|
||||
const dbSetUsers = jest.fn(a => Promise.resolve(a));
|
||||
const dbGetComments = jest.fn(a => Promise.resolve(a));
|
||||
const dbSetComments = jest.fn(a => Promise.resolve(a));
|
||||
|
||||
const db = {
|
||||
getCollection(namespace) {
|
||||
switch (namespace) {
|
||||
case 'users': {
|
||||
return {
|
||||
get: dbGetUsers,
|
||||
set: dbSetUsers,
|
||||
};
|
||||
}
|
||||
case 'comments': {
|
||||
return {
|
||||
get: dbGetComments,
|
||||
set: dbSetComments,
|
||||
};
|
||||
}
|
||||
default: {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
},
|
||||
persister: {
|
||||
getAppInfo() {
|
||||
return Promise.resolve(true);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const theStore = new DataStore(db);
|
||||
|
||||
describe('DataStore', () => {
|
||||
it('set current story - when user not logged in', () => {
|
||||
theStore.setCurrentStory('Components', 'CommentList - No Comments');
|
||||
|
||||
expect(theStore.currentStory).toEqual({
|
||||
sbKind: 'Components',
|
||||
sbStory: 'CommentList - No Comments',
|
||||
});
|
||||
});
|
||||
|
||||
it('set current user', () => {
|
||||
theStore.setCurrentUser({
|
||||
id: 'user-id',
|
||||
name: 'user-name',
|
||||
});
|
||||
|
||||
expect(theStore.user).toEqual({ id: 'user-id', name: 'user-name' });
|
||||
});
|
||||
|
||||
it('set current story - when user already logged in', () => {
|
||||
theStore.setCurrentStory('Components', 'CommentList - No Comments');
|
||||
|
||||
expect(theStore.currentStory).toEqual({
|
||||
sbKind: 'Components',
|
||||
sbStory: 'CommentList - No Comments',
|
||||
});
|
||||
});
|
||||
|
||||
it('add comment', async () => {
|
||||
theStore.setCurrentStory('Components', 'CommentList - No Comments');
|
||||
const comment = {
|
||||
text: 'sample comment',
|
||||
time: 1476435982029,
|
||||
userId: 'user-id',
|
||||
};
|
||||
|
||||
await theStore.addComment(comment);
|
||||
|
||||
expect(dbGetComments).toHaveBeenCalled();
|
||||
expect(dbSetComments).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('onComments', () => {
|
||||
const callback = comments => comments;
|
||||
const stopper = theStore.onComments(callback);
|
||||
expect(theStore.callbacks).toContain(callback);
|
||||
|
||||
expect(stopper).not.toThrow();
|
||||
expect(theStore.callbacks).not.toContain(callback);
|
||||
});
|
||||
});
|
@ -1,113 +0,0 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React, { Component } from 'react';
|
||||
import addons from '@storybook/addons';
|
||||
import CommentsPanel from '../../components/CommentsPanel/';
|
||||
import DataStore from './dataStore';
|
||||
|
||||
export default class Container extends Component {
|
||||
constructor(props, ...args) {
|
||||
super(props, ...args);
|
||||
this.state = {
|
||||
user: null,
|
||||
comments: [],
|
||||
loading: true,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const db = addons.getDatabase();
|
||||
this.store = new DataStore(db);
|
||||
this.stopListeningToComments = this.store.onComments(comments => {
|
||||
this.setState({ comments });
|
||||
});
|
||||
|
||||
// Clear the current notes on every story change.
|
||||
this.stopListeningOnStory = this.props.api.onStory((kind, story) => {
|
||||
// set the current selection
|
||||
this.store.setCurrentStory(kind, story);
|
||||
});
|
||||
|
||||
this.stopListingStoreLoading = this.store.onLoading(loading => {
|
||||
this.setState({ loading });
|
||||
});
|
||||
|
||||
this.init();
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.stopListeningToComments();
|
||||
this.stopListeningOnStory();
|
||||
this.stopListingStoreLoading();
|
||||
}
|
||||
|
||||
getAppInfo(persister) {
|
||||
return persister
|
||||
.getAppInfo()
|
||||
.then(appInfo => Promise.resolve(appInfo), () => Promise.resolve(null));
|
||||
}
|
||||
|
||||
init() {
|
||||
const db = addons.getDatabase();
|
||||
|
||||
if (typeof db.persister.getUser !== 'function') {
|
||||
throw new Error('unable to get user info');
|
||||
}
|
||||
|
||||
this.setState({ loading: true });
|
||||
db.persister
|
||||
.getUser()
|
||||
.then(u => Promise.resolve(u), () => Promise.resolve(null))
|
||||
.then(user => {
|
||||
if (user) {
|
||||
this.store.setCurrentUser(user);
|
||||
this.setState({ user });
|
||||
} else {
|
||||
this.setState({ user: null });
|
||||
}
|
||||
return this.getAppInfo(db.persister);
|
||||
})
|
||||
.then(appInfo => {
|
||||
const updatedState = { loading: false };
|
||||
if (!appInfo) {
|
||||
updatedState.appNotAvailable = true;
|
||||
}
|
||||
this.setState(updatedState);
|
||||
});
|
||||
}
|
||||
|
||||
addComment(text) {
|
||||
const time = Date.now();
|
||||
const { user } = this.state;
|
||||
|
||||
const comment = {
|
||||
text,
|
||||
time,
|
||||
userId: user.id,
|
||||
};
|
||||
|
||||
this.store.addComment(comment);
|
||||
}
|
||||
|
||||
deleteComment(commentId) {
|
||||
this.store.deleteComment(commentId);
|
||||
}
|
||||
|
||||
render() {
|
||||
const props = {
|
||||
user: this.state.user,
|
||||
comments: this.state.comments,
|
||||
loading: this.state.loading,
|
||||
appNotAvailable: this.state.appNotAvailable,
|
||||
deleteComment: commentId => this.deleteComment(commentId),
|
||||
addComment: text => this.addComment(text),
|
||||
};
|
||||
|
||||
return <CommentsPanel {...props} />;
|
||||
}
|
||||
}
|
||||
|
||||
Container.propTypes = {
|
||||
api: PropTypes.shape({
|
||||
onStory: PropTypes.func.isRequired,
|
||||
}).isRequired,
|
||||
};
|
@ -1,16 +0,0 @@
|
||||
/* eslint import/prefer-default-export:0 */
|
||||
|
||||
import React from 'react';
|
||||
import addons from '@storybook/addons';
|
||||
import CommentsPanel from './containers/CommentsPanel';
|
||||
import { ADDON_ID, PANEL_ID } from '../shared';
|
||||
|
||||
export function init() {
|
||||
addons.register(ADDON_ID, api => {
|
||||
// add 'Comments' panel
|
||||
addons.addPanel(PANEL_ID, {
|
||||
title: 'Comments',
|
||||
render: () => <CommentsPanel api={api} />,
|
||||
});
|
||||
});
|
||||
}
|
@ -1 +0,0 @@
|
||||
export const init = () => {};
|
@ -1,3 +0,0 @@
|
||||
// addons, panels and events get unique names using a prefix
|
||||
export const ADDON_ID = 'storybooks/addon-comments';
|
||||
export const PANEL_ID = `${ADDON_ID}/comments-panel`;
|
@ -1,85 +0,0 @@
|
||||
import React from 'react';
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import Button from '../index';
|
||||
import CommentForm from '../manager/components/CommentForm';
|
||||
import CommentList from '../manager/components/CommentList';
|
||||
import CommentsPanel from '../manager/components/CommentsPanel';
|
||||
|
||||
const userObj = {
|
||||
avatar: 'http://www.gravatar.com/avatar/?d=identicon',
|
||||
name: 'User A',
|
||||
};
|
||||
|
||||
const commentsList = [
|
||||
{
|
||||
loading: false,
|
||||
user: {
|
||||
avatar: 'http://www.gravatar.com/avatar/?d=identicon',
|
||||
name: 'User A',
|
||||
},
|
||||
time: 'Wed Oct 12 2016 13:36:59 GMT+0530 (IST)',
|
||||
text: 'Lorem ipsum dolor sit amet, <pre><code>Ut odio massa, rutrum et purus id.</code></pre>',
|
||||
},
|
||||
{
|
||||
loading: false,
|
||||
user: {
|
||||
avatar: 'http://www.gravatar.com/avatar/?d=identicon',
|
||||
name: 'User B',
|
||||
},
|
||||
time: 'Wed Oct 12 2016 13:38:46 GMT+0530 (IST)',
|
||||
text:
|
||||
'Vivamus tortor nisi, <b>efficitur</b> in rutrum <em>ac</em>, tempor <code>et mauris</code>. In et rutrum enim',
|
||||
},
|
||||
{
|
||||
loading: true,
|
||||
user: {
|
||||
avatar: 'http://www.gravatar.com/avatar/?d=identicon',
|
||||
name: 'User C',
|
||||
},
|
||||
time: 'Wed Oct 12 2016 13:38:55 GMT+0530 (IST)',
|
||||
text: 'sample comment 3',
|
||||
},
|
||||
];
|
||||
|
||||
storiesOf('Button', module)
|
||||
.add('default view', () => <Button onClick={action('button clicked')}>Hello</Button>)
|
||||
.add('some emojies as the text', () => <Button>😀 😎 👍 💯</Button>)
|
||||
.add('custom styles', () => {
|
||||
const style = {
|
||||
fontSize: 20,
|
||||
textTransform: 'uppercase',
|
||||
color: '#FF8833',
|
||||
};
|
||||
return <Button style={style}>Hello</Button>;
|
||||
});
|
||||
|
||||
storiesOf('Components', module)
|
||||
.add('CommentForm', () => <CommentForm addComment={action('addComment')} />)
|
||||
.add('CommentList - No Comments', () => <CommentList comments={[]} />)
|
||||
.add('CommentList - with comments', () => (
|
||||
<CommentList user={userObj} comments={commentsList} deleteComment={action('deleteComment')} />
|
||||
))
|
||||
.add('CommentPanel - not loggedIn', () => <CommentsPanel />)
|
||||
.add('CommentPanel - app not available', () => (
|
||||
<CommentsPanel user={userObj} appNotAvailable={{}} />
|
||||
))
|
||||
.add('CommentPanel - loggedIn with no comments', () => (
|
||||
<CommentsPanel
|
||||
user={userObj}
|
||||
loading={false}
|
||||
comments={[]}
|
||||
addComment={action('addComment')}
|
||||
deleteComment={action('deleteComment')}
|
||||
/>
|
||||
))
|
||||
.add('CommentPanel - loggedIn with has comments', () => (
|
||||
<CommentsPanel
|
||||
user={userObj}
|
||||
loading={false}
|
||||
comments={commentsList}
|
||||
addComment={action('addComment')}
|
||||
deleteComment={action('deleteComment')}
|
||||
/>
|
||||
));
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-events",
|
||||
"version": "3.2.16",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "Add events to your Storybook stories.",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -20,7 +20,7 @@
|
||||
"storybook": "start-storybook -p 6006"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "^3.2.16",
|
||||
"@storybook/addons": "^3.3.0-alpha.2",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"format-json": "^1.0.3",
|
||||
"prop-types": "^15.6.0",
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-graphql",
|
||||
"version": "3.2.16",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "Storybook addon to display the GraphiQL IDE",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-info",
|
||||
"version": "3.2.16",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "A Storybook addon to show additional information for your stories.",
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
@ -14,8 +14,8 @@
|
||||
"storybook": "start-storybook -p 9010"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "^3.2.16",
|
||||
"@storybook/components": "^3.2.16",
|
||||
"@storybook/addons": "^3.3.0-alpha.2",
|
||||
"@storybook/components": "^3.3.0-alpha.2",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"global": "^4.3.2",
|
||||
"marksy": "^2.0.0",
|
||||
|
@ -2,7 +2,10 @@
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { Table, Td, Th } from '@storybook/components';
|
||||
import PropVal from './PropVal';
|
||||
import PrettyPropType from './types/PrettyPropType';
|
||||
|
||||
const PropTypesMap = new Map();
|
||||
|
||||
@ -13,41 +16,10 @@ Object.keys(PropTypes).forEach(typeName => {
|
||||
PropTypesMap.set(type.isRequired, typeName);
|
||||
});
|
||||
|
||||
const stylesheet = {
|
||||
propTable: {
|
||||
marginLeft: -10,
|
||||
borderSpacing: '10px 5px',
|
||||
borderCollapse: 'separate',
|
||||
},
|
||||
};
|
||||
|
||||
const isNotEmpty = obj => obj && obj.props && Object.keys(obj.props).length > 0;
|
||||
|
||||
const renderDocgenPropType = propType => {
|
||||
if (!propType) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const { name } = propType;
|
||||
|
||||
switch (name) {
|
||||
case 'arrayOf':
|
||||
return `${propType.value.name}[]`;
|
||||
case 'instanceOf':
|
||||
return propType.value;
|
||||
case 'union':
|
||||
return propType.raw;
|
||||
case 'signature':
|
||||
return propType.raw;
|
||||
default:
|
||||
return name;
|
||||
}
|
||||
};
|
||||
|
||||
const hasDocgen = type => isNotEmpty(type.__docgenInfo);
|
||||
|
||||
const boolToString = value => (value ? 'yes' : 'no');
|
||||
|
||||
const propsFromDocgen = type => {
|
||||
const props = {};
|
||||
const docgenInfoProps = type.__docgenInfo.props;
|
||||
@ -59,8 +31,8 @@ const propsFromDocgen = type => {
|
||||
|
||||
props[property] = {
|
||||
property,
|
||||
propType: renderDocgenPropType(propType),
|
||||
required: boolToString(docgenInfoProp.required),
|
||||
propType,
|
||||
required: docgenInfoProp.required,
|
||||
description: docgenInfoProp.description,
|
||||
defaultValue: defaultValueDesc.value,
|
||||
};
|
||||
@ -75,21 +47,15 @@ const propsFromPropTypes = type => {
|
||||
if (type.propTypes) {
|
||||
Object.keys(type.propTypes).forEach(property => {
|
||||
const typeInfo = type.propTypes[property];
|
||||
const required = boolToString(typeInfo.isRequired === undefined);
|
||||
const description =
|
||||
type.__docgenInfo && type.__docgenInfo.props && type.__docgenInfo.props[property]
|
||||
? type.__docgenInfo.props[property].description
|
||||
: null;
|
||||
const required = typeInfo.isRequired === undefined;
|
||||
const docgenInfo =
|
||||
type.__docgenInfo && type.__docgenInfo.props && type.__docgenInfo.props[property];
|
||||
const description = docgenInfo ? docgenInfo.description : null;
|
||||
let propType = PropTypesMap.get(typeInfo) || 'other';
|
||||
|
||||
if (propType === 'other') {
|
||||
if (
|
||||
type.__docgenInfo &&
|
||||
type.__docgenInfo.props &&
|
||||
type.__docgenInfo.props[property] &&
|
||||
type.__docgenInfo.props[property].type
|
||||
) {
|
||||
propType = type.__docgenInfo.props[property].type.name;
|
||||
if (docgenInfo && docgenInfo.type) {
|
||||
propType = docgenInfo.type.name;
|
||||
}
|
||||
}
|
||||
|
||||
@ -116,6 +82,23 @@ const propsFromPropTypes = type => {
|
||||
return props;
|
||||
};
|
||||
|
||||
export const multiLineText = input => {
|
||||
if (!input) return input;
|
||||
const text = String(input);
|
||||
const arrayOfText = text.split(/\r?\n|\r/g);
|
||||
const isSingleLine = arrayOfText.length < 2;
|
||||
return isSingleLine
|
||||
? text
|
||||
: arrayOfText.map((
|
||||
lineOfText,
|
||||
i // note: lineOfText is the closest we will get to a unique key
|
||||
) => (
|
||||
<span key={lineOfText}>
|
||||
{i > 0 && <br />} {lineOfText}
|
||||
</span>
|
||||
));
|
||||
};
|
||||
|
||||
export default function PropTable(props) {
|
||||
const { type, maxPropObjectKeys, maxPropArrayLength, maxPropStringLength } = props;
|
||||
|
||||
@ -137,34 +120,38 @@ export default function PropTable(props) {
|
||||
};
|
||||
|
||||
return (
|
||||
<table style={stylesheet.propTable}>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>property</th>
|
||||
<th>propType</th>
|
||||
<th>required</th>
|
||||
<th>default</th>
|
||||
<th>description</th>
|
||||
<Th bordered>property</Th>
|
||||
<Th bordered>propType</Th>
|
||||
<Th bordered>required</Th>
|
||||
<Th bordered>default</Th>
|
||||
<Th bordered>description</Th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{array.map(row => (
|
||||
<tr key={row.property}>
|
||||
<td>{row.property}</td>
|
||||
<td>{row.propType}</td>
|
||||
<td>{row.required}</td>
|
||||
<td>
|
||||
<Td bordered code>
|
||||
{row.property}
|
||||
</Td>
|
||||
<Td bordered code>
|
||||
<PrettyPropType propType={row.propType} />
|
||||
</Td>
|
||||
<Td bordered>{row.required ? 'yes' : '-'}</Td>
|
||||
<Td bordered>
|
||||
{row.defaultValue === undefined ? (
|
||||
'-'
|
||||
) : (
|
||||
<PropVal val={row.defaultValue} {...propValProps} />
|
||||
)}
|
||||
</td>
|
||||
<td>{row.description}</td>
|
||||
</Td>
|
||||
<Td bordered>{multiLineText(row.description)}</Td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</Table>
|
||||
);
|
||||
}
|
||||
|
||||
|
34
addons/info/src/components/PropTable.test.js
Normal file
34
addons/info/src/components/PropTable.test.js
Normal file
@ -0,0 +1,34 @@
|
||||
import renderer from 'react-test-renderer';
|
||||
|
||||
import { multiLineText } from './PropTable';
|
||||
|
||||
describe('PropTable', () => {
|
||||
describe('multiLineText', () => {
|
||||
const singleLine = 'Foo bar baz';
|
||||
const unixMultiLineText = 'foo \n bar \n baz';
|
||||
const windowsMultiLineText = 'foo \r bar \r baz';
|
||||
|
||||
it('should return a blank string for a null input', () => {
|
||||
expect(multiLineText(null)).toBe(null);
|
||||
});
|
||||
it('should return a blank string for an undefined input', () => {
|
||||
expect(multiLineText(undefined)).toBe(undefined);
|
||||
});
|
||||
it('should cast a number to a string', () => {
|
||||
expect(multiLineText(1)).toBe('1');
|
||||
});
|
||||
it('should return its input for a single line of text', () => {
|
||||
expect(multiLineText(singleLine)).toBe(singleLine);
|
||||
});
|
||||
it('should return an array for unix multiline text', () => {
|
||||
expect(multiLineText(unixMultiLineText)).toHaveLength(3);
|
||||
});
|
||||
it('should return an array for windows multiline text', () => {
|
||||
expect(multiLineText(windowsMultiLineText)).toHaveLength(3);
|
||||
});
|
||||
it('should have 2 br tags for 3 lines of text', () => {
|
||||
const tree = renderer.create(multiLineText(unixMultiLineText)).toJSON();
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
@ -0,0 +1,20 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PropTable multiLineText should have 2 br tags for 3 lines of text 1`] = `
|
||||
Array [
|
||||
<span>
|
||||
|
||||
foo
|
||||
</span>,
|
||||
<span>
|
||||
<br />
|
||||
|
||||
bar
|
||||
</span>,
|
||||
<span>
|
||||
<br />
|
||||
|
||||
baz
|
||||
</span>,
|
||||
]
|
||||
`;
|
6
addons/info/src/components/types/.eslintrc.js
Normal file
6
addons/info/src/components/types/.eslintrc.js
Normal file
@ -0,0 +1,6 @@
|
||||
// TODO remove this file once https://github.com/yannickcr/eslint-plugin-react/issues/1389 is solved
|
||||
const warn = 1;
|
||||
|
||||
module.exports = {
|
||||
rules: { 'react/no-typos': warn },
|
||||
};
|
20
addons/info/src/components/types/ArrayOf.js
Normal file
20
addons/info/src/components/types/ArrayOf.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
|
||||
import PrettyPropType from './PrettyPropType';
|
||||
import { TypeInfo } from './proptypes';
|
||||
|
||||
const ArrayOf = ({ propType }) => (
|
||||
<span>
|
||||
<span>[</span>
|
||||
<span>
|
||||
<PrettyPropType propType={propType.value} />
|
||||
</span>
|
||||
<span>]</span>
|
||||
</span>
|
||||
);
|
||||
|
||||
ArrayOf.propTypes = {
|
||||
propType: TypeInfo.isRequired,
|
||||
};
|
||||
|
||||
export default ArrayOf;
|
8
addons/info/src/components/types/Enum.js
Normal file
8
addons/info/src/components/types/Enum.js
Normal file
@ -0,0 +1,8 @@
|
||||
import React from 'react';
|
||||
import { TypeInfo } from './proptypes';
|
||||
|
||||
const Enum = ({ propType }) => <span>{propType.value.map(({ value }) => value).join(' | ')}</span>;
|
||||
|
||||
Enum.propTypes = {
|
||||
propType: TypeInfo.isRequired,
|
||||
};
|
10
addons/info/src/components/types/InstanceOf.js
Normal file
10
addons/info/src/components/types/InstanceOf.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { TypeInfo } from './proptypes';
|
||||
|
||||
const InstanceOf = ({ propType }) => <span>{propType.value}</span>;
|
||||
|
||||
InstanceOf.propTypes = {
|
||||
propType: TypeInfo.isRequired,
|
||||
};
|
||||
|
||||
export default InstanceOf;
|
5
addons/info/src/components/types/Object.js
Normal file
5
addons/info/src/components/types/Object.js
Normal file
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const ObjectType = () => <span>{}</span>;
|
||||
|
||||
export default ObjectType;
|
18
addons/info/src/components/types/ObjectOf.js
Normal file
18
addons/info/src/components/types/ObjectOf.js
Normal file
@ -0,0 +1,18 @@
|
||||
import React from 'react';
|
||||
|
||||
import PrettyPropType from './PrettyPropType';
|
||||
import { TypeInfo } from './proptypes';
|
||||
|
||||
const ObjectOf = ({ propType }) => (
|
||||
<span>
|
||||
{'{[<key>]: '}
|
||||
<PrettyPropType propType={propType.value} />
|
||||
{'}'}
|
||||
</span>
|
||||
);
|
||||
|
||||
ObjectOf.propTypes = {
|
||||
propType: TypeInfo.isRequired,
|
||||
};
|
||||
|
||||
export default ObjectOf;
|
5
addons/info/src/components/types/ObjectType.js
Normal file
5
addons/info/src/components/types/ObjectType.js
Normal file
@ -0,0 +1,5 @@
|
||||
import React from 'react';
|
||||
|
||||
const ObjectType = () => <span>{}</span>;
|
||||
|
||||
export default ObjectType;
|
10
addons/info/src/components/types/OneOf.js
Normal file
10
addons/info/src/components/types/OneOf.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { TypeInfo } from './proptypes';
|
||||
|
||||
const OneOf = ({ propType }) => <span>{propType.value.map(({ value }) => value).join(' | ')}</span>;
|
||||
|
||||
OneOf.propTypes = {
|
||||
propType: TypeInfo.isRequired,
|
||||
};
|
||||
|
||||
export default OneOf;
|
25
addons/info/src/components/types/OneOfType.js
Normal file
25
addons/info/src/components/types/OneOfType.js
Normal file
@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
|
||||
import PrettyPropType from './PrettyPropType';
|
||||
import { TypeInfo } from './proptypes';
|
||||
|
||||
const OneOfType = ({ propType }) => {
|
||||
const { length } = propType.value;
|
||||
return (
|
||||
<span>
|
||||
{propType.value
|
||||
.map((value, i) => [
|
||||
<PrettyPropType
|
||||
key={`${value.name}${value.value ? `-${value.value}` : ''}`}
|
||||
propType={value}
|
||||
/>,
|
||||
i < length - 1 ? <span> | </span> : null,
|
||||
])
|
||||
.reduce((acc, tuple) => acc.concat(tuple), [])}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
OneOfType.propTypes = {
|
||||
propType: TypeInfo.isRequired,
|
||||
};
|
||||
export default OneOfType;
|
57
addons/info/src/components/types/PrettyPropType.js
Normal file
57
addons/info/src/components/types/PrettyPropType.js
Normal file
@ -0,0 +1,57 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import ObjectType from './ObjectType';
|
||||
import Shape from './Shape';
|
||||
import OneOfType from './OneOfType';
|
||||
import ArrayOf from './ArrayOf';
|
||||
import ObjectOf from './ObjectOf';
|
||||
import OneOf from './OneOf';
|
||||
import InstanceOf from './InstanceOf';
|
||||
import Signature from './Signature';
|
||||
|
||||
import { TypeInfo } from './proptypes';
|
||||
|
||||
// propType -> Component map - these are a bit more complex prop types to display
|
||||
const propTypeComponentMap = new Map([
|
||||
['shape', Shape],
|
||||
['union', OneOfType],
|
||||
['arrayOf', ArrayOf],
|
||||
['objectOf', ObjectOf],
|
||||
// Might be overkill to have below proptypes as separate components *shrug*
|
||||
['object', ObjectType],
|
||||
['enum', OneOf],
|
||||
['instanceOf', InstanceOf],
|
||||
['signature', Signature],
|
||||
]);
|
||||
|
||||
const PrettyPropType = props => {
|
||||
const { propType, depth } = props;
|
||||
if (!propType) {
|
||||
return <span>unknown</span>;
|
||||
}
|
||||
|
||||
const { name } = propType || {};
|
||||
|
||||
if (propTypeComponentMap.has(name)) {
|
||||
const Component = propTypeComponentMap.get(name);
|
||||
return <Component propType={propType} depth={depth} />;
|
||||
}
|
||||
|
||||
// Otherwise, propType does not have a dedicated component, display proptype name by default
|
||||
return <span>{name}</span>;
|
||||
};
|
||||
|
||||
PrettyPropType.displayName = 'PrettyPropType';
|
||||
|
||||
PrettyPropType.defaultProps = {
|
||||
propType: null,
|
||||
depth: 1,
|
||||
};
|
||||
|
||||
PrettyPropType.propTypes = {
|
||||
propType: TypeInfo,
|
||||
depth: PropTypes.number,
|
||||
};
|
||||
|
||||
export default PrettyPropType;
|
31
addons/info/src/components/types/PropertyLabel.js
Normal file
31
addons/info/src/components/types/PropertyLabel.js
Normal file
@ -0,0 +1,31 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
const styles = {
|
||||
hasProperty: {
|
||||
whiteSpace: 'nowrap',
|
||||
},
|
||||
};
|
||||
|
||||
const PropertyLabel = ({ property, required }) => {
|
||||
if (!property) return null;
|
||||
|
||||
return (
|
||||
<span style={styles.hasProperty}>
|
||||
{property}
|
||||
{required ? '' : '?'}:{' '}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
PropertyLabel.propTypes = {
|
||||
property: PropTypes.string,
|
||||
required: PropTypes.bool,
|
||||
};
|
||||
|
||||
PropertyLabel.defaultProps = {
|
||||
property: '',
|
||||
required: false,
|
||||
};
|
||||
|
||||
export default PropertyLabel;
|
81
addons/info/src/components/types/Shape.js
Normal file
81
addons/info/src/components/types/Shape.js
Normal file
@ -0,0 +1,81 @@
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
|
||||
import { HighlightButton } from '@storybook/components';
|
||||
import PrettyPropType from './PrettyPropType';
|
||||
import PropertyLabel from './PropertyLabel';
|
||||
|
||||
import { TypeInfo } from './proptypes';
|
||||
|
||||
const MARGIN_SIZE = 15;
|
||||
|
||||
class Shape extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
minimized: false,
|
||||
};
|
||||
}
|
||||
|
||||
handleToggle = () => {
|
||||
this.setState({
|
||||
minimized: !this.state.minimized,
|
||||
});
|
||||
};
|
||||
|
||||
handleMouseEnter = () => {
|
||||
this.setState({ hover: true });
|
||||
};
|
||||
|
||||
handleMouseLeave = () => {
|
||||
this.setState({ hover: false });
|
||||
};
|
||||
|
||||
render() {
|
||||
const { propType, depth } = this.props;
|
||||
return (
|
||||
<span>
|
||||
<HighlightButton
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
highlight={this.state.hover}
|
||||
onClick={this.handleToggle}
|
||||
>
|
||||
{'{'}
|
||||
</HighlightButton>
|
||||
<HighlightButton onClick={this.handleToggle}>...</HighlightButton>
|
||||
{!this.state.minimized &&
|
||||
Object.keys(propType.value).map(childProperty => (
|
||||
<div key={childProperty} style={{ marginLeft: depth * MARGIN_SIZE }}>
|
||||
<PropertyLabel
|
||||
property={childProperty}
|
||||
required={propType.value[childProperty].required}
|
||||
/>
|
||||
<PrettyPropType depth={depth + 1} propType={propType.value[childProperty]} />
|
||||
,
|
||||
</div>
|
||||
))}
|
||||
|
||||
<HighlightButton
|
||||
onMouseEnter={this.handleMouseEnter}
|
||||
onMouseLeave={this.handleMouseLeave}
|
||||
highlight={this.state.hover}
|
||||
onClick={this.handleToggle}
|
||||
>
|
||||
{'}'}
|
||||
</HighlightButton>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Shape.propTypes = {
|
||||
propType: TypeInfo,
|
||||
depth: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
Shape.defaultProps = {
|
||||
propType: null,
|
||||
};
|
||||
|
||||
export default Shape;
|
10
addons/info/src/components/types/Signature.js
Normal file
10
addons/info/src/components/types/Signature.js
Normal file
@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import { TypeInfo } from './proptypes';
|
||||
|
||||
const Signature = ({ propType }) => <span>{propType.raw}</span>;
|
||||
|
||||
Signature.propTypes = {
|
||||
propType: TypeInfo.isRequired,
|
||||
};
|
||||
|
||||
export default Signature;
|
6
addons/info/src/components/types/proptypes.js
Normal file
6
addons/info/src/components/types/proptypes.js
Normal file
@ -0,0 +1,6 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const TypeInfo = PropTypes.shape({
|
||||
name: PropTypes.string,
|
||||
value: PropTypes.any,
|
||||
});
|
@ -7,6 +7,11 @@
|
||||
[](https://now-examples-slackin-nqnzoygycp.now.sh/)
|
||||
[](#backers) [](#sponsors)
|
||||
|
||||
This addon works with Storybook for:
|
||||
[React](https://github.com/storybooks/storybook/tree/master/app/react).
|
||||
[React Native](https://github.com/storybooks/storybook/tree/master/app/react-native).
|
||||
[Vue](https://github.com/storybooks/storybook/tree/master/app/vue).
|
||||
|
||||
* * *
|
||||
|
||||
Storybook Addon Knobs allow you to edit React props dynamically using the Storybook UI.
|
||||
@ -41,7 +46,7 @@ Now, write your stories with knobs.
|
||||
|
||||
```js
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { withKnobs, text, boolean, number } from '@storybook/addon-knobs';
|
||||
import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/react';
|
||||
|
||||
const stories = storiesOf('Storybook Knobs', module);
|
||||
|
||||
@ -54,7 +59,7 @@ stories.add('with a button', () => (
|
||||
<button disabled={boolean('Disabled', false)} >
|
||||
{text('Label', 'Hello Button')}
|
||||
</button>
|
||||
))
|
||||
));
|
||||
|
||||
// Knobs as dynamic variables.
|
||||
stories.add('as dynamic variables', () => {
|
||||
@ -66,6 +71,27 @@ stories.add('as dynamic variables', () => {
|
||||
});
|
||||
```
|
||||
|
||||
> In the case of Vue, use these imports:
|
||||
>
|
||||
> ```js
|
||||
> import { storiesOf } from '@storybook/vue';
|
||||
> import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/vue';
|
||||
> ```
|
||||
>
|
||||
> In the case of React-Native, use these imports:
|
||||
>
|
||||
> ```js
|
||||
> import { storiesOf } from '@storybook/react-native';
|
||||
> import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/react';
|
||||
> ```
|
||||
>
|
||||
> In the case of Angular, use these imports:
|
||||
>
|
||||
> ```js
|
||||
> import { storiesOf } from '@storybook/angular';
|
||||
> import { withKnobs, text, boolean, number } from '@storybook/addon-knobs/angular';
|
||||
> ```
|
||||
|
||||
You can see your Knobs in a Storybook panel as shown below.
|
||||
|
||||

|
||||
@ -93,7 +119,7 @@ Just like that, you can import any other following Knobs:
|
||||
Allows you to get some text from the user.
|
||||
|
||||
```js
|
||||
import { text } from '@storybook/addon-knobs';
|
||||
import { text } from '@storybook/addon-knobs/react';
|
||||
|
||||
const label = 'Your Name';
|
||||
const defaultValue = 'Arunoda Susiripala';
|
||||
@ -106,7 +132,7 @@ const value = text(label, defaultValue);
|
||||
Allows you to get a boolean value from the user.
|
||||
|
||||
```js
|
||||
import { boolean } from '@storybook/addon-knobs';
|
||||
import { boolean } from '@storybook/addon-knobs/react';
|
||||
|
||||
const label = 'Agree?';
|
||||
const defaultValue = false;
|
||||
@ -119,7 +145,7 @@ const value = boolean(label, defaultValue);
|
||||
Allows you to get a number from the user.
|
||||
|
||||
```js
|
||||
import { number } from '@storybook/addon-knobs';
|
||||
import { number } from '@storybook/addon-knobs/react';
|
||||
|
||||
const label = 'Age';
|
||||
const defaultValue = 78;
|
||||
@ -132,7 +158,7 @@ const value = number(label, defaultValue);
|
||||
Allows you to get a number from the user using a range slider.
|
||||
|
||||
```js
|
||||
import { number } from '@storybook/addon-knobs';
|
||||
import { number } from '@storybook/addon-knobs/react';
|
||||
|
||||
const label = 'Temperature';
|
||||
const defaultValue = 73;
|
||||
@ -151,7 +177,7 @@ const value = number(label, defaultValue, options);
|
||||
Allows you to get a colour from the user.
|
||||
|
||||
```js
|
||||
import { color } from '@storybook/addon-knobs';
|
||||
import { color } from '@storybook/addon-knobs/react';
|
||||
|
||||
const label = 'Color';
|
||||
const defaultValue = '#ff00ff';
|
||||
@ -164,7 +190,7 @@ const value = color(label, defaultValue);
|
||||
Allows you to get a JSON object or array from the user.
|
||||
|
||||
```js
|
||||
import { object } from '@storybook/addon-knobs';
|
||||
import { object } from '@storybook/addon-knobs/react';
|
||||
|
||||
const label = 'Styles';
|
||||
const defaultValue = {
|
||||
@ -181,7 +207,7 @@ const value = object(label, defaultValue);
|
||||
Allows you to get an array of strings from the user.
|
||||
|
||||
```js
|
||||
import { array } from '@storybook/addon-knobs';
|
||||
import { array } from '@storybook/addon-knobs/react';
|
||||
|
||||
const label = 'Styles';
|
||||
const defaultValue = ['Red']
|
||||
@ -193,7 +219,7 @@ const value = array(label, defaultValue);
|
||||
> By default it's a comma, but this can be override by passing a separator variable.
|
||||
>
|
||||
> ```js
|
||||
> import { array } from '@storybook/addon-knobs';
|
||||
> import { array } from '@storybook/addon-knobs/react';
|
||||
>
|
||||
> const label = 'Styles';
|
||||
> const defaultValue = ['Red'];
|
||||
@ -206,7 +232,7 @@ const value = array(label, defaultValue);
|
||||
Allows you to get a value from a select box from the user.
|
||||
|
||||
```js
|
||||
import { select } from '@storybook/addon-knobs';
|
||||
import { select } from '@storybook/addon-knobs/react';
|
||||
|
||||
const label = 'Colors';
|
||||
const options = {
|
||||
@ -226,7 +252,7 @@ const value = select(label, options, defaultValue);
|
||||
Allow you to get date (and time) from the user.
|
||||
|
||||
```js
|
||||
import { date } from '@storybook/addon-knobs';
|
||||
import { date } from '@storybook/addon-knobs/react';
|
||||
|
||||
const label = 'Event Date';
|
||||
const defaultValue = new Date('Jan 20 2017');
|
||||
@ -263,7 +289,11 @@ If you feel like this addon is not performing well enough there is an option to
|
||||
Usage:
|
||||
|
||||
```js
|
||||
story.addDecorator(withKnobsOptions({
|
||||
import { storiesOf } from '@storybook/react';
|
||||
|
||||
const stories = storiesOf('Storybook Knobs', module);
|
||||
|
||||
stories.addDecorator(withKnobsOptions({
|
||||
debounce: { wait: number, leading: boolean}, // Same as lodash debounce.
|
||||
timestamps: true // Doesn't emit events while user is typing.
|
||||
}));
|
||||
|
1
addons/knobs/angular.js
vendored
Normal file
1
addons/knobs/angular.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/angular');
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-knobs",
|
||||
"version": "3.2.16",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "Storybook Addon Prop Editor Component",
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
@ -14,7 +14,8 @@
|
||||
"storybook": "start-storybook -p 9010"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "^3.2.16",
|
||||
"@angular/core": "^5.0.0-beta.7",
|
||||
"@storybook/addons": "^3.3.0-alpha.2",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"deep-equal": "^1.0.1",
|
||||
"global": "^4.3.2",
|
||||
@ -28,13 +29,10 @@
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^8.0.53",
|
||||
"@types/react": "^16.0.25",
|
||||
"git-url-parse": "^6.2.2",
|
||||
"raw-loader": "^0.5.1",
|
||||
"style-loader": "^0.19.0",
|
||||
"typescript": "^2.6.1",
|
||||
"typescript-definition-tester": "^0.0.5",
|
||||
"vue": "^2.5.6"
|
||||
"vue": "^2.5.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
|
1
addons/knobs/react.js
vendored
Normal file
1
addons/knobs/react.js
vendored
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/react');
|
117
addons/knobs/src/angular/helpers.js
vendored
Normal file
117
addons/knobs/src/angular/helpers.js
vendored
Normal file
@ -0,0 +1,117 @@
|
||||
/* eslint no-underscore-dangle: 0 */
|
||||
|
||||
import { Component, SimpleChange, ChangeDetectorRef } from '@angular/core';
|
||||
|
||||
const getComponentMetadata = ({ component, props = {} }) => {
|
||||
if (!component || typeof component !== 'function') throw new Error('No valid component provided');
|
||||
|
||||
const componentMeta = component.__annotations__[0] || component.annotations[0];
|
||||
const propsMeta = component.__prop__metadata__ || component.propMetadata || {};
|
||||
const paramsMetadata = component.__parameters__ || component.parameters || [];
|
||||
return {
|
||||
component,
|
||||
props,
|
||||
componentMeta,
|
||||
propsMeta,
|
||||
params: paramsMetadata,
|
||||
};
|
||||
};
|
||||
|
||||
const getAnnotatedComponent = ({ componentMeta, component, params, knobStore, channel }) => {
|
||||
const NewComponent = function NewComponent(cd, ...args) {
|
||||
component.call(this, ...args);
|
||||
this.cd = cd;
|
||||
this.knobChanged = this.knobChanged.bind(this);
|
||||
this.setPaneKnobs = this.setPaneKnobs.bind(this);
|
||||
};
|
||||
NewComponent.prototype = Object.create(component.prototype);
|
||||
NewComponent.__annotations__ = [new Component(componentMeta)];
|
||||
NewComponent.__parameters__ = [[ChangeDetectorRef], ...params];
|
||||
|
||||
NewComponent.prototype.constructor = NewComponent;
|
||||
NewComponent.prototype.ngOnInit = function onInit() {
|
||||
if (component.prototype.ngOnInit) {
|
||||
component.prototype.ngOnInit();
|
||||
}
|
||||
|
||||
channel.on('addon:knobs:knobChange', this.knobChanged);
|
||||
channel.on('addon:knobs:knobClick', this.knobClicked);
|
||||
knobStore.subscribe(this.setPaneKnobs);
|
||||
this.setPaneKnobs();
|
||||
};
|
||||
|
||||
NewComponent.prototype.ngOnDestroy = function onDestroy() {
|
||||
if (component.prototype.ngOnDestroy) {
|
||||
component.prototype.ngOnDestroy();
|
||||
}
|
||||
|
||||
channel.removeListener('addon:knobs:knobChange', this.knobChanged);
|
||||
channel.removeListener('addon:knobs:knobClick', this.knobClicked);
|
||||
knobStore.unsubscribe(this.setPaneKnobs);
|
||||
};
|
||||
|
||||
NewComponent.prototype.ngOnChanges = function onChanges(changes) {
|
||||
if (component.prototype.ngOnChanges) {
|
||||
component.prototype.ngOnChanges(changes);
|
||||
}
|
||||
};
|
||||
|
||||
NewComponent.prototype.setPaneKnobs = function setPaneKnobs(timestamp = +new Date()) {
|
||||
channel.emit('addon:knobs:setKnobs', {
|
||||
knobs: knobStore.getAll(),
|
||||
timestamp,
|
||||
});
|
||||
};
|
||||
|
||||
NewComponent.prototype.knobChanged = function knobChanged(change) {
|
||||
const { name, value } = change;
|
||||
const knobOptions = knobStore.get(name);
|
||||
const oldValue = knobOptions.value;
|
||||
knobOptions.value = value;
|
||||
knobStore.markAllUnused();
|
||||
const lowercasedName = name.toLocaleLowerCase();
|
||||
this[lowercasedName] = value;
|
||||
this.cd.detectChanges();
|
||||
this.ngOnChanges({
|
||||
[lowercasedName]: new SimpleChange(oldValue, value, false),
|
||||
});
|
||||
};
|
||||
|
||||
NewComponent.prototype.knobClicked = function knobClicked(clicked) {
|
||||
const knobOptions = knobStore.get(clicked.name);
|
||||
knobOptions.callback();
|
||||
};
|
||||
|
||||
return NewComponent;
|
||||
};
|
||||
|
||||
const resetKnobs = (knobStore, channel) => {
|
||||
knobStore.reset();
|
||||
channel.emit('addon:knobs:setKnobs', {
|
||||
knobs: knobStore.getAll(),
|
||||
timestamp: false,
|
||||
});
|
||||
};
|
||||
|
||||
export function prepareComponent({ getStory, context, channel, knobStore }) {
|
||||
resetKnobs(knobStore, channel);
|
||||
const { component, componentMeta, props, propsMeta, params } = getComponentMetadata(
|
||||
getStory(context)
|
||||
);
|
||||
|
||||
if (!componentMeta) throw new Error('No component metadata available');
|
||||
|
||||
const AnnotatedComponent = getAnnotatedComponent({
|
||||
componentMeta,
|
||||
component,
|
||||
params,
|
||||
knobStore,
|
||||
channel,
|
||||
});
|
||||
|
||||
return {
|
||||
component: AnnotatedComponent,
|
||||
props,
|
||||
propsMeta,
|
||||
};
|
||||
}
|
39
addons/knobs/src/angular/index.js
vendored
Normal file
39
addons/knobs/src/angular/index.js
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
import addons from '@storybook/addons';
|
||||
|
||||
import { prepareComponent } from './helpers';
|
||||
|
||||
import {
|
||||
knob,
|
||||
text,
|
||||
boolean,
|
||||
number,
|
||||
color,
|
||||
object,
|
||||
array,
|
||||
date,
|
||||
select,
|
||||
button,
|
||||
manager,
|
||||
} from '../base';
|
||||
|
||||
export { knob, text, boolean, number, color, object, array, date, select, button };
|
||||
|
||||
export const angularHandler = (channel, knobStore) => getStory => context =>
|
||||
prepareComponent({ getStory, context, channel, knobStore });
|
||||
|
||||
function wrapperKnobs(options) {
|
||||
const channel = addons.getChannel();
|
||||
manager.setChannel(channel);
|
||||
|
||||
if (options) channel.emit('addon:knobs:setOptions', options);
|
||||
|
||||
return angularHandler(channel, manager.knobStore);
|
||||
}
|
||||
|
||||
export function withKnobs(storyFn, context) {
|
||||
return wrapperKnobs()(storyFn)(context);
|
||||
}
|
||||
|
||||
export function withKnobsOptions(options = {}) {
|
||||
return (storyFn, context) => wrapperKnobs(options)(storyFn)(context);
|
||||
}
|
59
addons/knobs/src/base.js
Normal file
59
addons/knobs/src/base.js
Normal file
@ -0,0 +1,59 @@
|
||||
import KnobManager from './KnobManager';
|
||||
|
||||
export const manager = new KnobManager();
|
||||
|
||||
export function knob(name, options) {
|
||||
return manager.knob(name, options);
|
||||
}
|
||||
|
||||
export function text(name, value) {
|
||||
return manager.knob(name, { type: 'text', value });
|
||||
}
|
||||
|
||||
export function boolean(name, value) {
|
||||
return manager.knob(name, { type: 'boolean', value });
|
||||
}
|
||||
|
||||
export function number(name, value, options = {}) {
|
||||
const defaults = {
|
||||
range: false,
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 1,
|
||||
};
|
||||
|
||||
const mergedOptions = { ...defaults, ...options };
|
||||
|
||||
const finalOptions = {
|
||||
...mergedOptions,
|
||||
type: 'number',
|
||||
value,
|
||||
};
|
||||
|
||||
return manager.knob(name, finalOptions);
|
||||
}
|
||||
|
||||
export function color(name, value) {
|
||||
return manager.knob(name, { type: 'color', value });
|
||||
}
|
||||
|
||||
export function object(name, value) {
|
||||
return manager.knob(name, { type: 'object', value });
|
||||
}
|
||||
|
||||
export function select(name, options, value) {
|
||||
return manager.knob(name, { type: 'select', options, value });
|
||||
}
|
||||
|
||||
export function array(name, value, separator = ',') {
|
||||
return manager.knob(name, { type: 'array', value, separator });
|
||||
}
|
||||
|
||||
export function date(name, value = new Date()) {
|
||||
const proxyValue = value ? value.getTime() : null;
|
||||
return manager.knob(name, { type: 'date', value: proxyValue });
|
||||
}
|
||||
|
||||
export function button(name, callback) {
|
||||
return manager.knob(name, { type: 'button', callback, hideLabel: true });
|
||||
}
|
@ -16,18 +16,16 @@ describe('Array', () => {
|
||||
expect(onChange).toHaveBeenCalledWith(['Fhishing', 'Skiing', 'Dancing']);
|
||||
});
|
||||
|
||||
it('deserializes an Array to an Array', () => {
|
||||
const array = ['a', 'b', 'c'];
|
||||
const deserialized = ArrayType.deserialize(array);
|
||||
it('should change to an empty array when emptied', () => {
|
||||
const onChange = jest.fn();
|
||||
const wrapper = shallow(
|
||||
<Array
|
||||
onChange={onChange}
|
||||
knob={{ name: 'passions', value: ['Fishing', 'Skiing'], separator: ',' }}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(deserialized).toEqual(['a', 'b', 'c']);
|
||||
});
|
||||
|
||||
it('deserializes an Object to an Array', () => {
|
||||
const object = { 1: 'one', 0: 'zero', 2: 'two' };
|
||||
|
||||
const deserialized = ArrayType.deserialize(object);
|
||||
|
||||
expect(deserialized).toEqual(['zero', 'one', 'two']);
|
||||
wrapper.simulate('change', { target: { value: '' } });
|
||||
expect(onChange).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
|
@ -16,14 +16,29 @@ const styles = {
|
||||
color: '#555',
|
||||
};
|
||||
|
||||
const ArrayType = ({ knob, onChange }) => (
|
||||
<Textarea
|
||||
id={knob.name}
|
||||
style={styles}
|
||||
value={knob.value.join(knob.separator)}
|
||||
onChange={e => onChange(e.target.value.split(knob.separator))}
|
||||
/>
|
||||
);
|
||||
function formatArray(value, separator) {
|
||||
if (value === '') {
|
||||
return [];
|
||||
}
|
||||
return value.split(separator);
|
||||
}
|
||||
|
||||
class ArrayType extends React.Component {
|
||||
render() {
|
||||
const { knob, onChange } = this.props;
|
||||
return (
|
||||
<Textarea
|
||||
id={knob.name}
|
||||
ref={c => {
|
||||
this.input = c;
|
||||
}}
|
||||
style={styles}
|
||||
value={knob.value.join(knob.separator)}
|
||||
onChange={e => onChange(formatArray(e.target.value, knob.separator))}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ArrayType.defaultProps = {
|
||||
knob: {},
|
||||
|
@ -1,70 +1,31 @@
|
||||
import { window } from 'global';
|
||||
import deprecate from 'util-deprecate';
|
||||
import addons from '@storybook/addons';
|
||||
import KnobManager from './KnobManager';
|
||||
|
||||
import { vueHandler } from './vue';
|
||||
import { reactHandler } from './react';
|
||||
|
||||
const manager = new KnobManager();
|
||||
import {
|
||||
array,
|
||||
boolean,
|
||||
button,
|
||||
color,
|
||||
date,
|
||||
knob,
|
||||
manager,
|
||||
number,
|
||||
object,
|
||||
select,
|
||||
text,
|
||||
} from './base';
|
||||
|
||||
export function knob(name, options) {
|
||||
return manager.knob(name, options);
|
||||
}
|
||||
export { knob, text, boolean, number, color, object, array, date, button, select };
|
||||
|
||||
export function text(name, value) {
|
||||
return manager.knob(name, { type: 'text', value });
|
||||
}
|
||||
deprecate(() => {},
|
||||
'Using @storybook/addon-knobs directly is discouraged, please use @storybook/addon-knobs/{{framework}}');
|
||||
|
||||
export function boolean(name, value) {
|
||||
return manager.knob(name, { type: 'boolean', value });
|
||||
}
|
||||
|
||||
export function number(name, value, options = {}) {
|
||||
const defaults = {
|
||||
range: false,
|
||||
min: 0,
|
||||
max: 10,
|
||||
step: 1,
|
||||
};
|
||||
|
||||
const mergedOptions = { ...defaults, ...options };
|
||||
|
||||
const finalOptions = {
|
||||
...mergedOptions,
|
||||
type: 'number',
|
||||
value,
|
||||
};
|
||||
|
||||
return manager.knob(name, finalOptions);
|
||||
}
|
||||
|
||||
export function color(name, value) {
|
||||
return manager.knob(name, { type: 'color', value });
|
||||
}
|
||||
|
||||
export function object(name, value) {
|
||||
return manager.knob(name, { type: 'object', value });
|
||||
}
|
||||
|
||||
export function select(name, options, value) {
|
||||
return manager.knob(name, { type: 'select', options, value });
|
||||
}
|
||||
|
||||
export function array(name, value, separator = ',') {
|
||||
return manager.knob(name, { type: 'array', value, separator });
|
||||
}
|
||||
|
||||
export function date(name, value = new Date()) {
|
||||
const proxyValue = value ? value.getTime() : null;
|
||||
return manager.knob(name, { type: 'date', value: proxyValue });
|
||||
}
|
||||
|
||||
export function button(name, callback) {
|
||||
return manager.knob(name, { type: 'button', callback, hideLabel: true });
|
||||
}
|
||||
|
||||
// "Higher order component" / wrapper style API
|
||||
// In 3.3, this will become `withKnobs`, once our decorator API supports it.
|
||||
// See https://github.com/storybooks/storybook/pull/1527
|
||||
// generic higher-order component decorator for all platforms - usage is discouraged
|
||||
// This file Should be removed with 4.0 release
|
||||
function wrapperKnobs(options) {
|
||||
const channel = addons.getChannel();
|
||||
manager.setChannel(channel);
|
||||
|
@ -55,8 +55,8 @@ export default class WrapStory extends React.Component {
|
||||
this.setState({ storyContent: storyFn(context) });
|
||||
}
|
||||
|
||||
knobClicked(knob) {
|
||||
const knobOptions = this.props.knobStore.get(knob.name);
|
||||
knobClicked(clicked) {
|
||||
const knobOptions = this.props.knobStore.get(clicked.name);
|
||||
knobOptions.callback();
|
||||
}
|
||||
|
||||
|
@ -1,11 +1,43 @@
|
||||
import React from 'react';
|
||||
import addons from '@storybook/addons';
|
||||
|
||||
import WrapStory from './WrapStory';
|
||||
|
||||
/**
|
||||
* Handles a react story
|
||||
*/
|
||||
import {
|
||||
knob,
|
||||
text,
|
||||
boolean,
|
||||
number,
|
||||
color,
|
||||
object,
|
||||
array,
|
||||
date,
|
||||
select,
|
||||
button,
|
||||
manager,
|
||||
} from '../base';
|
||||
|
||||
export { knob, text, boolean, number, color, object, array, date, select, button };
|
||||
|
||||
export const reactHandler = (channel, knobStore) => getStory => context => {
|
||||
const initialContent = getStory(context);
|
||||
const props = { context, storyFn: getStory, channel, knobStore, initialContent };
|
||||
return <WrapStory {...props} />;
|
||||
};
|
||||
|
||||
function wrapperKnobs(options) {
|
||||
const channel = addons.getChannel();
|
||||
manager.setChannel(channel);
|
||||
|
||||
if (options) channel.emit('addon:knobs:setOptions', options);
|
||||
|
||||
return reactHandler(channel, manager.knobStore);
|
||||
}
|
||||
|
||||
export function withKnobs(storyFn, context) {
|
||||
return wrapperKnobs()(storyFn)(context);
|
||||
}
|
||||
|
||||
export function withKnobsOptions(options = {}) {
|
||||
return (storyFn, context) => wrapperKnobs(options)(storyFn)(context);
|
||||
}
|
||||
|
@ -1,11 +0,0 @@
|
||||
import * as tt from 'typescript-definition-tester';
|
||||
|
||||
describe('TypeScript definitions', () => {
|
||||
it('should compile against index.d.ts', done => {
|
||||
tt.compileDirectory(
|
||||
`${__dirname}/../example/typescript`,
|
||||
fileName => fileName.match(/\.ts$/),
|
||||
() => done()
|
||||
);
|
||||
});
|
||||
});
|
@ -1,3 +1,21 @@
|
||||
import addons from '@storybook/addons';
|
||||
|
||||
import {
|
||||
knob,
|
||||
text,
|
||||
boolean,
|
||||
number,
|
||||
color,
|
||||
object,
|
||||
array,
|
||||
date,
|
||||
select,
|
||||
button,
|
||||
manager,
|
||||
} from '../base';
|
||||
|
||||
export { knob, text, boolean, number, color, object, array, date, select, button };
|
||||
|
||||
export const vueHandler = (channel, knobStore) => getStory => context => ({
|
||||
data() {
|
||||
return {
|
||||
@ -22,8 +40,8 @@ export const vueHandler = (channel, knobStore) => getStory => context => ({
|
||||
this.$forceUpdate();
|
||||
},
|
||||
|
||||
onKnobClick(knob) {
|
||||
const knobOptions = knobStore.get(knob.name);
|
||||
onKnobClick(clicked) {
|
||||
const knobOptions = knobStore.get(clicked.name);
|
||||
knobOptions.callback();
|
||||
},
|
||||
|
||||
@ -53,3 +71,20 @@ export const vueHandler = (channel, knobStore) => getStory => context => ({
|
||||
knobStore.unsubscribe(this.setPaneKnobs);
|
||||
},
|
||||
});
|
||||
|
||||
function wrapperKnobs(options) {
|
||||
const channel = addons.getChannel();
|
||||
manager.setChannel(channel);
|
||||
|
||||
if (options) channel.emit('addon:knobs:setOptions', options);
|
||||
|
||||
return vueHandler(channel, manager.knobStore);
|
||||
}
|
||||
|
||||
export function withKnobs(storyFn, context) {
|
||||
return wrapperKnobs()(storyFn)(context);
|
||||
}
|
||||
|
||||
export function withKnobsOptions(options = {}) {
|
||||
return (storyFn, context) => wrapperKnobs(options)(storyFn)(context);
|
||||
}
|
||||
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"noImplicitAny": false,
|
||||
"sourceMap": false,
|
||||
"lib": [
|
||||
"es2015",
|
||||
"es2016",
|
||||
"dom"
|
||||
],
|
||||
"jsx": "react"
|
||||
}
|
||||
}
|
1
addons/knobs/vue.js
Normal file
1
addons/knobs/vue.js
Normal file
@ -0,0 +1 @@
|
||||
module.exports = require('./dist/vue');
|
@ -19,6 +19,7 @@ This addon works with Storybook for:
|
||||
## Getting Started
|
||||
|
||||
Install this addon by adding the `@storybook/addon-links` dependency:
|
||||
|
||||
```sh
|
||||
yarn add @storybook/addon-links
|
||||
```
|
||||
@ -57,7 +58,74 @@ linkTo('Toggle') // Links to the first story in the 'Toggle' kind
|
||||
With that, you can link an event in a component to any story in the Storybook.
|
||||
|
||||
- First parameter is the the story kind name (what you named with `storiesOf`).
|
||||
- Second (optional) parameter is the story name (what you named with `.add`). If the second parameter is omitted, the link will point to the first story in the given kind.
|
||||
- Second (optional) parameter is the story name (what you named with `.add`).
|
||||
If the second parameter is omitted, the link will point to the first story in the given kind.
|
||||
|
||||
> You can also pass a function instead for any of above parameter. That function accepts arguments emitted by the event and it should return a string. <br/>
|
||||
> Have a look at [PR86](https://github.com/kadirahq/react-storybook/pull/86) for more information.
|
||||
You can also pass a function instead for any of above parameter. That function accepts arguments emitted by the event and it should return a string:
|
||||
|
||||
```js
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { LinkTo, linkTo } from '@storybook/addon-links';
|
||||
|
||||
storiesOf('Select', module)
|
||||
.add('Index', () => (
|
||||
<select value="Index" onChange={linkTo('Select', e => e.currentTarget.value)}>
|
||||
<option>Index</option>
|
||||
<option>First</option>
|
||||
<option>Second</option>
|
||||
<option>Third</option>
|
||||
</select>
|
||||
))
|
||||
.add('First', () => <LinkTo story="Index">Go back</LinkTo>)
|
||||
.add('Second', () => <LinkTo story="Index">Go back</LinkTo>)
|
||||
.add('Third', () => <LinkTo story="Index">Go back</LinkTo>);
|
||||
```
|
||||
|
||||
## hrefTo function
|
||||
|
||||
If you want to get an URL for a particular story, you may use `hrefTo` function. It returns a promise, which resolves to string containing a relative URL:
|
||||
|
||||
```js
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { hrefTo } from '@storybook/addon-links';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
storiesOf('Href', module)
|
||||
.add('log', () => {
|
||||
hrefTo('Href', 'log').then(action('URL of this story'));
|
||||
|
||||
return <span>See action logger</span>;
|
||||
});
|
||||
```
|
||||
|
||||
## LinkTo component (React only)
|
||||
|
||||
One possible way of using `hrefTo` is to create a component that uses native `a` element, but prevents page reloads on plain left click, so that one can still use default browser methods to open link in new tab.
|
||||
A React implementation of such a component can be imported from `@storybook/addon-links` package:
|
||||
|
||||
```js
|
||||
import { storiesOf } from '@storybook/react';
|
||||
import { LinkTo } from '@storybook/addon-links';
|
||||
|
||||
storiesOf('Link', module)
|
||||
.add('First', () => (
|
||||
<LinkTo story="Second">Go to Second</LinkTo>
|
||||
))
|
||||
.add('Second', () => (
|
||||
<LinkTo story="First">Go to First</LinkTo>
|
||||
));
|
||||
```
|
||||
|
||||
It accepts all the props the `a` element does, plus `story` and `kind`. It the `kind` prop is omitted, the current kind will be preserved.
|
||||
|
||||
```js
|
||||
<LinkTo
|
||||
kind="Toggle"
|
||||
story="off"
|
||||
target="_blank"
|
||||
title="link to second story"
|
||||
style={{color: '#1474f3'}}
|
||||
>Go to Second</LinkTo>
|
||||
```
|
||||
|
||||
To implement such a component for another framework, you need to add special handling for `click` event on native `a` element. See [`RoutedLink` sources](https://github.com/storybooks/storybook/blob/master/lib/components/src/navigation/routed_link.js#L4-L9) for reference.
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-links",
|
||||
"version": "3.2.16",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "Story Links addon for storybook",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
@ -21,7 +21,16 @@
|
||||
"storybook": "start-storybook -p 9001"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "^3.2.16"
|
||||
"@storybook/addons": "^3.3.0-alpha.2",
|
||||
"@storybook/components": "^3.3.0-alpha.2",
|
||||
"global": "^4.3.2",
|
||||
"prop-types": "^15.5.10"
|
||||
},
|
||||
"devDependencies": {
|
||||
"enzyme": "^3.0.0",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"shelljs": "^0.7.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
|
@ -0,0 +1,8 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`LinkTo render should render a link 1`] = `
|
||||
<RoutedLink
|
||||
href="?selectedKind=undefined&selectedStory=undefined"
|
||||
onClick={[Function]}
|
||||
/>
|
||||
`;
|
52
addons/links/src/components/link.js
Normal file
52
addons/links/src/components/link.js
Normal file
@ -0,0 +1,52 @@
|
||||
import React, { PureComponent } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { RoutedLink } from '@storybook/components';
|
||||
import { openLink, hrefTo } from '../preview';
|
||||
|
||||
export default class LinkTo extends PureComponent {
|
||||
constructor(...args) {
|
||||
super(...args);
|
||||
|
||||
this.state = {
|
||||
href: '/',
|
||||
};
|
||||
|
||||
this.handleClick = this.handleClick.bind(this);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateHref(this.props);
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props) {
|
||||
if (props.kind !== this.props.kind || props.story !== this.props.story) {
|
||||
this.updateHref(props);
|
||||
}
|
||||
}
|
||||
|
||||
async updateHref(props) {
|
||||
const href = await hrefTo(props.kind, props.story);
|
||||
this.setState({ href });
|
||||
}
|
||||
|
||||
handleClick() {
|
||||
openLink(this.props);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { kind, story, ...rest } = this.props;
|
||||
|
||||
return <RoutedLink href={this.state.href} onClick={this.handleClick} {...rest} />;
|
||||
}
|
||||
}
|
||||
|
||||
LinkTo.defaultProps = {
|
||||
kind: null,
|
||||
story: null,
|
||||
};
|
||||
|
||||
LinkTo.propTypes = {
|
||||
kind: PropTypes.string,
|
||||
story: PropTypes.string,
|
||||
};
|
40
addons/links/src/components/link.test.js
Normal file
40
addons/links/src/components/link.test.js
Normal file
@ -0,0 +1,40 @@
|
||||
import { shallow } from 'enzyme';
|
||||
import React from 'react';
|
||||
import addons from '@storybook/addons';
|
||||
|
||||
import { EVENT_ID } from '..';
|
||||
import { mockChannel } from '../preview.test';
|
||||
import LinkTo from './link';
|
||||
|
||||
jest.mock('@storybook/addons');
|
||||
|
||||
describe('LinkTo', () => {
|
||||
describe('render', () => {
|
||||
it('should render a link', async () => {
|
||||
const channel = mockChannel();
|
||||
addons.getChannel.mockReturnValue(channel);
|
||||
|
||||
const wrapper = shallow(<LinkTo kind="foo" story="bar" />);
|
||||
await wrapper.instance().updateHref(wrapper.props());
|
||||
wrapper.update();
|
||||
expect(wrapper).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('events', () => {
|
||||
it('should select the kind and story on click', () => {
|
||||
const channel = {
|
||||
emit: jest.fn(),
|
||||
on: jest.fn(),
|
||||
};
|
||||
addons.getChannel.mockReturnValue(channel);
|
||||
|
||||
const wrapper = shallow(<LinkTo kind="foo" story="bar" />);
|
||||
wrapper.simulate('click');
|
||||
expect(channel.emit).toHaveBeenCalledWith(EVENT_ID, {
|
||||
kind: 'foo',
|
||||
story: 'bar',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -1,5 +1,8 @@
|
||||
export const ADDON_ID = 'storybook/links';
|
||||
export const EVENT_ID = `${ADDON_ID}/link-event`;
|
||||
export const REQUEST_HREF_EVENT_ID = `${ADDON_ID}/request-href-event`;
|
||||
export const RECEIVE_HREF_EVENT_ID = `${ADDON_ID}/receive-href-event`;
|
||||
|
||||
export { register } from './manager';
|
||||
export { linkTo } from './preview';
|
||||
export { linkTo, hrefTo } from './preview';
|
||||
export { default as LinkTo } from './components/link';
|
||||
|
@ -1,11 +1,29 @@
|
||||
import { location } from 'global';
|
||||
import addons from '@storybook/addons';
|
||||
import { ADDON_ID, EVENT_ID } from './';
|
||||
import { ADDON_ID, EVENT_ID, REQUEST_HREF_EVENT_ID, RECEIVE_HREF_EVENT_ID } from './';
|
||||
|
||||
export function register() {
|
||||
addons.register(ADDON_ID, api => {
|
||||
const channel = addons.getChannel();
|
||||
channel.on(EVENT_ID, selection => {
|
||||
api.selectStory(selection.kind, selection.story);
|
||||
if (selection.kind != null) {
|
||||
api.selectStory(selection.kind, selection.story);
|
||||
} else {
|
||||
api.selectInCurrentKind(selection.story);
|
||||
}
|
||||
});
|
||||
channel.on(REQUEST_HREF_EVENT_ID, selection => {
|
||||
const params =
|
||||
selection.kind != null
|
||||
? {
|
||||
selectedKind: selection.kind,
|
||||
selectedStory: selection.story,
|
||||
}
|
||||
: {
|
||||
selectedStory: selection.story,
|
||||
};
|
||||
const urlState = api.getUrlState(params);
|
||||
channel.emit(RECEIVE_HREF_EVENT_ID, location.pathname + urlState.url);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
@ -1,12 +1,21 @@
|
||||
import addons from '@storybook/addons';
|
||||
import { EVENT_ID } from './';
|
||||
import { EVENT_ID, REQUEST_HREF_EVENT_ID, RECEIVE_HREF_EVENT_ID } from './';
|
||||
|
||||
export function linkTo(kind, story) {
|
||||
return (...args) => {
|
||||
const resolvedKind = typeof kind === 'function' ? kind(...args) : kind;
|
||||
const resolvedStory = typeof story === 'function' ? story(...args) : story;
|
||||
export const openLink = params => addons.getChannel().emit(EVENT_ID, params);
|
||||
|
||||
const valueOrCall = args => value => (typeof value === 'function' ? value(...args) : value);
|
||||
|
||||
export const linkTo = (kind, story) => (...args) => {
|
||||
const resolver = valueOrCall(args);
|
||||
openLink({
|
||||
kind: resolver(kind),
|
||||
story: resolver(story),
|
||||
});
|
||||
};
|
||||
|
||||
export const hrefTo = (kind, story) =>
|
||||
new Promise(resolve => {
|
||||
const channel = addons.getChannel();
|
||||
channel.emit(EVENT_ID, { kind: resolvedKind, story: resolvedStory });
|
||||
};
|
||||
}
|
||||
channel.on(RECEIVE_HREF_EVENT_ID, resolve);
|
||||
channel.emit(REQUEST_HREF_EVENT_ID, { kind, story });
|
||||
});
|
||||
|
61
addons/links/src/preview.test.js
Normal file
61
addons/links/src/preview.test.js
Normal file
@ -0,0 +1,61 @@
|
||||
import addons from '@storybook/addons';
|
||||
import { linkTo, hrefTo } from './preview';
|
||||
import { EVENT_ID, REQUEST_HREF_EVENT_ID, RECEIVE_HREF_EVENT_ID } from './';
|
||||
|
||||
jest.mock('@storybook/addons');
|
||||
|
||||
export const mockChannel = () => {
|
||||
let cb;
|
||||
return {
|
||||
emit(id, payload) {
|
||||
if (id === REQUEST_HREF_EVENT_ID) {
|
||||
cb(`?selectedKind=${payload.kind}&selectedStory=${payload.story}`);
|
||||
}
|
||||
},
|
||||
on(id, callback) {
|
||||
if (id === RECEIVE_HREF_EVENT_ID) {
|
||||
cb = callback;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('preview', () => {
|
||||
describe('linkTo()', () => {
|
||||
it('should select the kind and story provided', () => {
|
||||
const channel = { emit: jest.fn() };
|
||||
addons.getChannel.mockReturnValue(channel);
|
||||
|
||||
const handler = linkTo('kind', 'story');
|
||||
handler();
|
||||
|
||||
expect(channel.emit).toHaveBeenCalledWith(EVENT_ID, {
|
||||
kind: 'kind',
|
||||
story: 'story',
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle functions returning strings', () => {
|
||||
const channel = { emit: jest.fn() };
|
||||
addons.getChannel.mockReturnValue(channel);
|
||||
|
||||
const handler = linkTo((a, b) => a + b, (a, b) => b + a);
|
||||
handler('foo', 'bar');
|
||||
|
||||
expect(channel.emit).toHaveBeenCalledWith(EVENT_ID, {
|
||||
kind: 'foobar',
|
||||
story: 'barfoo',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('hrefTo()', () => {
|
||||
it('should return promise resolved with story href', async () => {
|
||||
const channel = mockChannel();
|
||||
addons.getChannel.mockReturnValue(channel);
|
||||
|
||||
const href = await hrefTo('kind', 'story');
|
||||
expect(href).toBe('?selectedKind=kind&selectedStory=story');
|
||||
});
|
||||
});
|
||||
});
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-notes",
|
||||
"version": "3.2.16",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "Write notes for your Storybook stories.",
|
||||
"keywords": [
|
||||
"addon",
|
||||
@ -19,7 +19,7 @@
|
||||
"storybook": "start-storybook -p 9010"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "^3.2.16",
|
||||
"@storybook/addons": "^3.3.0-alpha.2",
|
||||
"babel-runtime": "^6.26.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
|
@ -5,10 +5,10 @@ setOptions({
|
||||
name: 'CUSTOM-OPTIONS',
|
||||
url: 'https://github.com/storybooks/storybook',
|
||||
// goFullScreen: false,
|
||||
// showLeftPanel: true,
|
||||
showDownPanel: false,
|
||||
// showStoriesPanel: true,
|
||||
showAddonPanel: false,
|
||||
// showSearchBox: false,
|
||||
// downPanelInRight: false,
|
||||
// addonPanelInRight: false,
|
||||
});
|
||||
|
||||
storybook.configure(() => require('./stories'), module);
|
||||
|
@ -56,25 +56,25 @@ setOptions({
|
||||
*/
|
||||
goFullScreen: false,
|
||||
/**
|
||||
* display left panel that shows a list of stories
|
||||
* display panel that shows a list of stories
|
||||
* @type {Boolean}
|
||||
*/
|
||||
showLeftPanel: true,
|
||||
showStoriesPanel: true,
|
||||
/**
|
||||
* display horizontal panel that displays addon configurations
|
||||
* display panel that shows addon configurations
|
||||
* @type {Boolean}
|
||||
*/
|
||||
showDownPanel: true,
|
||||
showAddonPanel: true,
|
||||
/**
|
||||
* display floating search box to search through stories
|
||||
* @type {Boolean}
|
||||
*/
|
||||
showSearchBox: false,
|
||||
/**
|
||||
* show horizontal addons panel as a vertical panel on the right
|
||||
* show addon panel as a vertical panel on the right
|
||||
* @type {Boolean}
|
||||
*/
|
||||
downPanelInRight: false,
|
||||
addonPanelInRight: false,
|
||||
/**
|
||||
* sorts stories
|
||||
* @type {Boolean}
|
||||
@ -101,7 +101,7 @@ setOptions({
|
||||
* id to select an addon panel
|
||||
* @type {String}
|
||||
*/
|
||||
selectedAddonPanel: undefined, // The order of addons in the "Addons Panel" is the same as you import them in 'addons.js'. The first panel will be opened by default as you run Storybook
|
||||
selectedAddonPanel: undefined, // The order of addons in the "Addon panel" is the same as you import them in 'addons.js'. The first panel will be opened by default as you run Storybook
|
||||
});
|
||||
|
||||
storybook.configure(() => require('./stories'), module);
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-options",
|
||||
"version": "3.2.16",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "Options addon for storybook",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
@ -20,7 +20,13 @@
|
||||
"storybook": "start-storybook -p 9001"
|
||||
},
|
||||
"dependencies": {
|
||||
"@storybook/addons": "^3.2.16"
|
||||
"@storybook/addons": "^3.3.0-alpha.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0",
|
||||
"react-test-renderer": "^16.0.0",
|
||||
"shelljs": "^0.7.8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "*",
|
||||
|
@ -147,6 +147,36 @@ Just render the story, don't check the output at all (useful if you just want to
|
||||
|
||||
Like the default, but allows you to specify a set of options for the test renderer. [See for example here](https://github.com/storybooks/storybook/blob/b915b5439786e0edb17d7f5ab404bba9f7919381/examples/test-cra/src/storyshots.test.js#L14-L16).
|
||||
|
||||
### `multiSnapshotWithOptions(options)`
|
||||
|
||||
Like `snapshotWithOptions`, but generate a separate snapshot file for each stories file rather than a single monolithic file (as is the convention in Jest). This makes it dramatically easier to review changes.
|
||||
|
||||
### `shallowSnapshot`
|
||||
|
||||
Take a snapshot of a shallow-rendered version of the component.
|
||||
|
||||
### `getSnapshotFileName`
|
||||
|
||||
Utility function used in `multiSnapshotWithOptions`. This is made available for users who implement custom test functions that also want to take advantage of multi-file storyshots.
|
||||
|
||||
###### Example:
|
||||
|
||||
Let's say we wanted to create a test function for shallow && multi-file snapshots:
|
||||
|
||||
```js
|
||||
import initStoryshots, { getSnapshotFileName } from '@storybook/addon-storyshots';
|
||||
import { shallow } from 'enzyme';
|
||||
import toJson from 'enzyme-to-json';
|
||||
|
||||
initStoryshots({
|
||||
test: ({ story, context }) => {
|
||||
const snapshotFileName = getSnapshotFileName(context);
|
||||
const storyElement = story.render(context);
|
||||
const shallowTree = shallow(storyElement);
|
||||
|
||||
if (snapshotFileName) {
|
||||
expect(toJson(shallowTree)).toMatchSpecificSnapshot(snapshotFileName);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@storybook/addon-storyshots",
|
||||
"version": "3.2.16",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "StoryShots is a Jest Snapshot Testing Addon for Storybook.",
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
@ -11,23 +11,35 @@
|
||||
"scripts": {
|
||||
"build-storybook": "build-storybook",
|
||||
"prepare": "babel ./src --out-dir ./dist",
|
||||
"storybook": "start-storybook -p 6006"
|
||||
"storybook": "start-storybook -p 6006",
|
||||
"example": "jest storyshot.test"
|
||||
},
|
||||
"dependencies": {
|
||||
"babel-runtime": "^6.26.0",
|
||||
"glob": "^7.1.2",
|
||||
"global": "^4.3.2",
|
||||
"jest-specific-snapshot": "^0.2.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"read-pkg-up": "^3.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@storybook/addons": "^3.2.16",
|
||||
"@storybook/channels": "^3.2.16",
|
||||
"@storybook/react": "^3.2.16"
|
||||
"@storybook/addons": "^3.3.0-alpha.2",
|
||||
"@storybook/channels": "^3.3.0-alpha.2",
|
||||
"@storybook/react": "^3.3.0-alpha.2",
|
||||
"babel-cli": "^6.26.0",
|
||||
"babel-jest": "^20.0.3",
|
||||
"babel-plugin-transform-runtime": "^6.23.0",
|
||||
"babel-preset-env": "^1.6.0",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"jest": "^20.0.4",
|
||||
"jest-cli": "^20.0.4",
|
||||
"react": "^16.0.0",
|
||||
"react-dom": "^16.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@storybook/addons": "^3.2.16",
|
||||
"@storybook/channels": "^3.2.16",
|
||||
"@storybook/react": "^3.2.16",
|
||||
"@storybook/addons": "^3.3.0-alpha.2",
|
||||
"@storybook/channels": "^3.3.0-alpha.2",
|
||||
"@storybook/react": "^3.3.0-alpha.2",
|
||||
"babel-core": "^6.26.0",
|
||||
"react": "*",
|
||||
"react-test-renderer": "*"
|
||||
|
@ -1,5 +1,6 @@
|
||||
import path from 'path';
|
||||
import fs from 'fs';
|
||||
import glob from 'glob';
|
||||
import global, { describe, it } from 'global';
|
||||
import readPkgUp from 'read-pkg-up';
|
||||
import addons from '@storybook/addons';
|
||||
@ -7,8 +8,17 @@ import addons from '@storybook/addons';
|
||||
import runWithRequireContext from './require_context';
|
||||
import createChannel from './storybook-channel-mock';
|
||||
import { snapshot } from './test-bodies';
|
||||
import { getPossibleStoriesFiles, getSnapshotFileName } from './utils';
|
||||
|
||||
export { snapshotWithOptions, snapshot, shallowSnapshot, renderOnly } from './test-bodies';
|
||||
export {
|
||||
snapshot,
|
||||
multiSnapshotWithOptions,
|
||||
snapshotWithOptions,
|
||||
shallowSnapshot,
|
||||
renderOnly,
|
||||
} from './test-bodies';
|
||||
|
||||
export { getSnapshotFileName };
|
||||
|
||||
let storybook;
|
||||
let configPath;
|
||||
@ -50,6 +60,7 @@ export default function testStorySnapshots(options = {}) {
|
||||
runWithRequireContext(content, contextOpts);
|
||||
} else if (isRNStorybook) {
|
||||
storybook = require.requireActual('@storybook/react-native');
|
||||
|
||||
configPath = path.resolve(options.configPath || 'storybook');
|
||||
require.requireActual(configPath);
|
||||
} else {
|
||||
@ -64,6 +75,10 @@ export default function testStorySnapshots(options = {}) {
|
||||
const suite = options.suite || options.suit || 'Storyshots';
|
||||
const stories = storybook.getStorybook();
|
||||
|
||||
if (stories.length === 0) {
|
||||
throw new Error('storyshots found 0 stories');
|
||||
}
|
||||
|
||||
// Added not to break existing storyshots configs (can be removed in a future major release)
|
||||
// eslint-disable-next-line
|
||||
options.storyNameRegex = options.storyNameRegex || options.storyRegex;
|
||||
@ -72,13 +87,15 @@ export default function testStorySnapshots(options = {}) {
|
||||
|
||||
// eslint-disable-next-line
|
||||
for (const group of stories) {
|
||||
if (options.storyKindRegex && !group.kind.match(options.storyKindRegex)) {
|
||||
const { fileName, kind } = group;
|
||||
|
||||
if (options.storyKindRegex && !kind.match(options.storyKindRegex)) {
|
||||
// eslint-disable-next-line
|
||||
continue;
|
||||
}
|
||||
|
||||
describe(suite, () => {
|
||||
describe(group.kind, () => {
|
||||
describe(kind, () => {
|
||||
// eslint-disable-next-line
|
||||
for (const story of group.stories) {
|
||||
if (options.storyNameRegex && !story.name.match(options.storyNameRegex)) {
|
||||
@ -87,11 +104,24 @@ export default function testStorySnapshots(options = {}) {
|
||||
}
|
||||
|
||||
it(story.name, () => {
|
||||
const context = { kind: group.kind, story: story.name };
|
||||
return options.test({ story, context });
|
||||
const context = { fileName, kind, story: story.name };
|
||||
options.test({ story, context });
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('Storyshots Integrity', () => {
|
||||
describe('Abandoned Storyshots', () => {
|
||||
const storyshots = glob.sync('**/*.storyshot');
|
||||
|
||||
const abandonedStoryshots = storyshots.filter(fileName => {
|
||||
const possibleStoriesFiles = getPossibleStoriesFiles(fileName);
|
||||
return !possibleStoriesFiles.some(fs.existsSync);
|
||||
});
|
||||
|
||||
expect(abandonedStoryshots).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
@ -1,12 +1,30 @@
|
||||
import renderer from 'react-test-renderer';
|
||||
import shallow from 'react-test-renderer/shallow';
|
||||
import 'jest-specific-snapshot';
|
||||
import { getSnapshotFileName } from './utils';
|
||||
|
||||
function getRenderedTree(story, context, options) {
|
||||
const storyElement = story.render(context);
|
||||
return renderer.create(storyElement, options).toJSON();
|
||||
}
|
||||
|
||||
export const snapshotWithOptions = options => ({ story, context }) => {
|
||||
const storyElement = story.render(context);
|
||||
const tree = renderer.create(storyElement, options).toJSON();
|
||||
const tree = getRenderedTree(story, context, options);
|
||||
expect(tree).toMatchSnapshot();
|
||||
};
|
||||
|
||||
export const multiSnapshotWithOptions = options => ({ story, context }) => {
|
||||
const tree = getRenderedTree(story, context, options);
|
||||
const snapshotFileName = getSnapshotFileName(context);
|
||||
|
||||
if (!snapshotFileName) {
|
||||
expect(tree).toMatchSnapshot();
|
||||
return;
|
||||
}
|
||||
|
||||
expect(tree).toMatchSpecificSnapshot(snapshotFileName);
|
||||
};
|
||||
|
||||
export const snapshot = snapshotWithOptions({});
|
||||
|
||||
export function shallowSnapshot({ story, context }) {
|
||||
|
25
addons/storyshots/src/utils.js
Normal file
25
addons/storyshots/src/utils.js
Normal file
@ -0,0 +1,25 @@
|
||||
import path from 'path';
|
||||
|
||||
function getStoryshotFile(fileName) {
|
||||
const { dir, name } = path.parse(fileName);
|
||||
return path.format({ dir: path.join(dir, '__snapshots__'), name, ext: '.storyshot' });
|
||||
}
|
||||
|
||||
export function getPossibleStoriesFiles(storyshotFile) {
|
||||
const { dir, name } = path.parse(storyshotFile);
|
||||
|
||||
return [
|
||||
path.format({ dir: path.dirname(dir), name, ext: '.js' }),
|
||||
path.format({ dir: path.dirname(dir), name, ext: '.jsx' }),
|
||||
];
|
||||
}
|
||||
|
||||
export function getSnapshotFileName(context) {
|
||||
const { fileName } = context;
|
||||
|
||||
if (!fileName) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return getStoryshotFile(fileName);
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots Another Button with some emoji 1`] = `
|
||||
<button
|
||||
className="css-1yjiefr"
|
||||
onClick={[Function]}
|
||||
>
|
||||
😀 😎 👍 💯
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Another Button with text 1`] = `
|
||||
<button
|
||||
className="css-1yjiefr"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Hello Button
|
||||
</button>
|
||||
`;
|
@ -0,0 +1,19 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots Button with some emoji 1`] = `
|
||||
<button
|
||||
className="css-1yjiefr"
|
||||
onClick={[Function]}
|
||||
>
|
||||
😀 😎 👍 💯
|
||||
</button>
|
||||
`;
|
||||
|
||||
exports[`Storyshots Button with text 1`] = `
|
||||
<button
|
||||
className="css-1yjiefr"
|
||||
onClick={[Function]}
|
||||
>
|
||||
Hello Button
|
||||
</button>
|
||||
`;
|
@ -0,0 +1,104 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Storyshots Welcome to Storybook 1`] = `
|
||||
<article
|
||||
className="css-1fqbdip"
|
||||
>
|
||||
<h1
|
||||
className="css-nil"
|
||||
>
|
||||
Welcome to storybook
|
||||
</h1>
|
||||
<p>
|
||||
This is a UI component dev environment for your app.
|
||||
</p>
|
||||
<p>
|
||||
We've added some basic stories inside the
|
||||
|
||||
<code
|
||||
className="css-mteq83"
|
||||
>
|
||||
src/stories
|
||||
</code>
|
||||
|
||||
directory.
|
||||
<br />
|
||||
A story is a single state of one or more UI components. You can have as many stories as you want.
|
||||
<br />
|
||||
(Basically a story is like a visual test case.)
|
||||
</p>
|
||||
<p>
|
||||
See these sample
|
||||
|
||||
<a
|
||||
className="css-ca0824"
|
||||
onClick={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
stories
|
||||
</a>
|
||||
|
||||
for a component called
|
||||
|
||||
<code
|
||||
className="css-mteq83"
|
||||
>
|
||||
Button
|
||||
</code>
|
||||
.
|
||||
</p>
|
||||
<p>
|
||||
Just like that, you can add your own components as stories.
|
||||
<br />
|
||||
You can also edit those components and see changes right away.
|
||||
<br />
|
||||
(Try editing the
|
||||
<code
|
||||
className="css-mteq83"
|
||||
>
|
||||
Button
|
||||
</code>
|
||||
stories located at
|
||||
<code
|
||||
className="css-mteq83"
|
||||
>
|
||||
src/stories/index.js
|
||||
</code>
|
||||
.)
|
||||
</p>
|
||||
<p>
|
||||
Usually we create stories with smaller UI components in the app.
|
||||
<br />
|
||||
Have a look at the
|
||||
|
||||
<a
|
||||
className="css-ca0824"
|
||||
href="https://storybook.js.org/basics/writing-stories"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Writing Stories
|
||||
</a>
|
||||
|
||||
section in our documentation.
|
||||
</p>
|
||||
<p
|
||||
className="css-bwdon3"
|
||||
>
|
||||
<b>
|
||||
NOTE:
|
||||
</b>
|
||||
<br />
|
||||
Have a look at the
|
||||
|
||||
<code
|
||||
className="css-mteq83"
|
||||
>
|
||||
.storybook/webpack.config.js
|
||||
</code>
|
||||
|
||||
to add webpack loaders and plugins you are using in this project.
|
||||
</p>
|
||||
</article>
|
||||
`;
|
8
addons/storyshots/stories/storyshot.test.js
Normal file
8
addons/storyshots/stories/storyshot.test.js
Normal file
@ -0,0 +1,8 @@
|
||||
import path from 'path';
|
||||
import initStoryshots, { multiSnapshotWithOptions } from '../src';
|
||||
|
||||
initStoryshots({
|
||||
framework: 'react',
|
||||
configPath: path.join(__dirname, '..', '.storybook'),
|
||||
test: multiSnapshotWithOptions({}),
|
||||
});
|
37
addons/viewport/README.md
Normal file
37
addons/viewport/README.md
Normal file
@ -0,0 +1,37 @@
|
||||
# Storybook Viewport Addon
|
||||
|
||||
Storybook Viewport Addon allows your stories to be displayed in different sizes and layouts in [Storybook](https://storybookjs.org). This helps build responsive components inside of Storybook.
|
||||
|
||||
This addon works with Storybook for: [React](https://github.com/storybooks/storybook/tree/master/app/react) and [Vue](https://github.com/storybooks/storybook/tree/master/app/vue).
|
||||
|
||||

|
||||
|
||||
## Installation
|
||||
|
||||
Install the following npm module:
|
||||
|
||||
npm i --save-dev @storybook/addon-viewport
|
||||
|
||||
or with yarn:
|
||||
|
||||
yarn add -D @storybook/addon-viewport
|
||||
|
||||
## Basic Usage
|
||||
|
||||
Simply import the Storybook Viewport Addon in the `addon.js` file in your `.storybook` directory.
|
||||
|
||||
```js
|
||||
import '@storybook/addon-viewport/register'
|
||||
```
|
||||
|
||||
This will register the Viewport Addon to Storybook and will show up in the action area.
|
||||
|
||||
## FAQ
|
||||
|
||||
#### How do I add a new device?
|
||||
|
||||
Unfortunately, this is currently not supported.
|
||||
|
||||
#### How can I make a custom screen size?
|
||||
|
||||
Unfortunately, this is currently not supported.
|
BIN
addons/viewport/docs/viewport.png
Normal file
BIN
addons/viewport/docs/viewport.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 193 KiB |
22
addons/viewport/package.json
Normal file
22
addons/viewport/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "@storybook/addon-viewport",
|
||||
"version": "3.3.0-alpha.2",
|
||||
"description": "Storybook addon to change the viewport size to mobile",
|
||||
"main": "dist/index.js",
|
||||
"keywords": [
|
||||
"storybook"
|
||||
],
|
||||
"scripts": {
|
||||
"prepare": "node ../../scripts/prepare.js"
|
||||
},
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@storybook/components": "^3.3.0-alpha.2",
|
||||
"global": "^4.3.2",
|
||||
"prop-types": "^15.5.10"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@storybook/addons": "^3.2.0",
|
||||
"react": "*"
|
||||
}
|
||||
}
|
119
addons/viewport/src/components/Panel.js
Normal file
119
addons/viewport/src/components/Panel.js
Normal file
@ -0,0 +1,119 @@
|
||||
import React, { Component } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { baseFonts } from '@storybook/components';
|
||||
import { document } from 'global';
|
||||
|
||||
import { viewports, defaultViewport, resetViewport } from './viewportInfo';
|
||||
import { SelectViewport } from './SelectViewport';
|
||||
import { RotateViewport } from './RotateViewport';
|
||||
|
||||
import * as styles from './styles';
|
||||
|
||||
const storybookIframe = 'storybook-preview-iframe';
|
||||
const containerStyles = {
|
||||
padding: 15,
|
||||
width: '100%',
|
||||
boxSizing: 'border-box',
|
||||
...baseFonts,
|
||||
};
|
||||
|
||||
export class Panel extends Component {
|
||||
static propTypes = {
|
||||
channel: PropTypes.object.isRequired, // eslint-disable-line react/forbid-prop-types
|
||||
};
|
||||
|
||||
constructor(props, context) {
|
||||
super(props, context);
|
||||
this.state = {
|
||||
viewport: defaultViewport,
|
||||
isLandscape: false,
|
||||
};
|
||||
|
||||
this.props.channel.on('addon:viewport:update', this.changeViewport);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.iframe = document.getElementById(storybookIframe);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
this.props.channel.removeListener('addon:viewport:update', this.changeViewport);
|
||||
}
|
||||
|
||||
iframe = undefined;
|
||||
|
||||
changeViewport = viewport => {
|
||||
const { viewport: previousViewport } = this.state;
|
||||
|
||||
if (previousViewport !== viewport) {
|
||||
this.setState(
|
||||
{
|
||||
viewport,
|
||||
isLandscape: false,
|
||||
},
|
||||
this.updateIframe
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
toggleLandscape = () => {
|
||||
const { isLandscape } = this.state;
|
||||
|
||||
this.setState({ isLandscape: !isLandscape }, this.updateIframe);
|
||||
};
|
||||
|
||||
updateIframe = () => {
|
||||
const { viewport: viewportKey, isLandscape } = this.state;
|
||||
const viewport = viewports[viewportKey] || resetViewport;
|
||||
|
||||
if (!this.iframe) {
|
||||
throw new Error('Cannot find Storybook iframe');
|
||||
}
|
||||
|
||||
Object.keys(viewport.styles).forEach(prop => {
|
||||
this.iframe.style[prop] = viewport.styles[prop];
|
||||
});
|
||||
|
||||
if (isLandscape) {
|
||||
this.iframe.style.height = viewport.styles.width;
|
||||
this.iframe.style.width = viewport.styles.height;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { isLandscape, viewport } = this.state;
|
||||
|
||||
const disableDefault = viewport === defaultViewport;
|
||||
const disabledStyles = disableDefault ? styles.disabled : {};
|
||||
|
||||
const buttonStyles = {
|
||||
...styles.button,
|
||||
...disabledStyles,
|
||||
marginTop: 30,
|
||||
padding: 20,
|
||||
};
|
||||
|
||||
return (
|
||||
<div style={containerStyles}>
|
||||
<SelectViewport
|
||||
activeViewport={viewport}
|
||||
onChange={e => this.changeViewport(e.target.value)}
|
||||
/>
|
||||
|
||||
<RotateViewport
|
||||
onClick={this.toggleLandscape}
|
||||
disabled={disableDefault}
|
||||
active={isLandscape}
|
||||
/>
|
||||
|
||||
<button
|
||||
style={buttonStyles}
|
||||
onClick={() => this.changeViewport(defaultViewport)}
|
||||
disabled={disableDefault}
|
||||
>
|
||||
Reset Viewport
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
30
addons/viewport/src/components/RotateViewport.js
Normal file
30
addons/viewport/src/components/RotateViewport.js
Normal file
@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import * as styles from './styles';
|
||||
|
||||
export function RotateViewport({ active, ...props }) {
|
||||
const disabledStyles = props.disabled ? styles.disabled : {};
|
||||
const actionStyles = {
|
||||
...styles.action,
|
||||
...disabledStyles,
|
||||
};
|
||||
return (
|
||||
<div style={styles.row}>
|
||||
<label style={styles.label}>Rotate</label>
|
||||
<button {...props} style={actionStyles}>
|
||||
{active ? 'Vertical' : 'Landscape'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
RotateViewport.propTypes = {
|
||||
disabled: PropTypes.bool,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
active: PropTypes.bool,
|
||||
};
|
||||
|
||||
RotateViewport.defaultProps = {
|
||||
disabled: true,
|
||||
active: false,
|
||||
};
|
26
addons/viewport/src/components/SelectViewport.js
Normal file
26
addons/viewport/src/components/SelectViewport.js
Normal file
@ -0,0 +1,26 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { viewports, defaultViewport } from './viewportInfo';
|
||||
import * as styles from './styles';
|
||||
|
||||
export function SelectViewport({ activeViewport, onChange }) {
|
||||
return (
|
||||
<div style={styles.row}>
|
||||
<label style={styles.label}>Device</label>
|
||||
<select style={styles.action} value={activeViewport} onChange={onChange}>
|
||||
<option value={defaultViewport}>Default</option>
|
||||
{Object.keys(viewports).map(key => (
|
||||
<option value={key} key={key}>
|
||||
{viewports[key].name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
SelectViewport.propTypes = {
|
||||
onChange: PropTypes.func.isRequired,
|
||||
activeViewport: PropTypes.string.isRequired,
|
||||
};
|
30
addons/viewport/src/components/styles.js
Normal file
30
addons/viewport/src/components/styles.js
Normal file
@ -0,0 +1,30 @@
|
||||
export const row = {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
marginBottom: 15,
|
||||
};
|
||||
|
||||
export const label = {
|
||||
width: 80,
|
||||
marginRight: 15,
|
||||
};
|
||||
|
||||
const actionColor = 'rgb(247, 247, 247)';
|
||||
|
||||
export const button = {
|
||||
color: 'rgb(85, 85, 85)',
|
||||
width: '100%',
|
||||
border: `1px solid ${actionColor}`,
|
||||
backgroundColor: actionColor,
|
||||
borderRadius: 3,
|
||||
};
|
||||
|
||||
export const disabled = {
|
||||
opacity: '0.5',
|
||||
cursor: 'not-allowed',
|
||||
};
|
||||
|
||||
export const action = {
|
||||
...button,
|
||||
height: 30,
|
||||
};
|
249
addons/viewport/src/components/tests/Panel.test.js
Normal file
249
addons/viewport/src/components/tests/Panel.test.js
Normal file
@ -0,0 +1,249 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
import { document } from 'global';
|
||||
|
||||
import { Panel } from '../Panel';
|
||||
import { viewports, defaultViewport, resetViewport } from '../viewportInfo';
|
||||
import * as styles from '../styles';
|
||||
|
||||
describe('Viewport/Panel', () => {
|
||||
const props = {
|
||||
channel: {
|
||||
on: jest.fn(),
|
||||
removeListener: jest.fn(),
|
||||
},
|
||||
};
|
||||
|
||||
let subject;
|
||||
|
||||
beforeEach(() => {
|
||||
subject = shallow(<Panel {...props} />);
|
||||
});
|
||||
|
||||
describe('construct', () => {
|
||||
it('creates the initial state', () => {
|
||||
expect(subject.instance().state).toEqual({
|
||||
viewport: defaultViewport,
|
||||
isLandscape: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('listens on the channel', () => {
|
||||
expect(props.channel.on).toHaveBeenCalledWith(
|
||||
'addon:viewport:update',
|
||||
subject.instance().changeViewport
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentDidMount', () => {
|
||||
let previousGet;
|
||||
|
||||
beforeEach(() => {
|
||||
subject.instance().iframe = undefined;
|
||||
previousGet = document.getElementById;
|
||||
document.getElementById = jest.fn(() => 'iframe');
|
||||
subject.instance().componentDidMount();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
document.getElementById = previousGet;
|
||||
});
|
||||
|
||||
it('gets the iframe', () => {
|
||||
expect(subject.instance().iframe).toEqual('iframe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('componentWillUnmount', () => {
|
||||
beforeEach(() => {
|
||||
subject.instance().componentWillUnmount();
|
||||
});
|
||||
|
||||
it('removes the channel listener', () => {
|
||||
expect(props.channel.removeListener).toHaveBeenCalledWith(
|
||||
'addon:viewport:update',
|
||||
subject.instance().changeViewport
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('changeViewport', () => {
|
||||
beforeEach(() => {
|
||||
subject.instance().setState = jest.fn();
|
||||
subject.instance().updateIframe = jest.fn();
|
||||
});
|
||||
|
||||
describe('new viewport', () => {
|
||||
beforeEach(() => {
|
||||
subject.instance().changeViewport(viewports[0]);
|
||||
});
|
||||
|
||||
it('sets the state with the new information', () => {
|
||||
expect(subject.instance().setState).toHaveBeenCalledWith(
|
||||
{
|
||||
viewport: viewports[0],
|
||||
isLandscape: false,
|
||||
},
|
||||
subject.instance().updateIframe
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('same as previous viewport', () => {
|
||||
beforeEach(() => {
|
||||
subject.instance().changeViewport(defaultViewport);
|
||||
});
|
||||
|
||||
it('doesnt update the state', () => {
|
||||
expect(subject.instance().setState).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('toggleLandscape', () => {
|
||||
beforeEach(() => {
|
||||
subject.setState({ isLandscape: false });
|
||||
subject.instance().setState = jest.fn();
|
||||
subject.instance().toggleLandscape();
|
||||
});
|
||||
|
||||
it('updates the landscape to be the opposite', () => {
|
||||
expect(subject.instance().setState).toHaveBeenCalledWith(
|
||||
{ isLandscape: true },
|
||||
subject.instance().updateIframe
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIframe', () => {
|
||||
let iframe;
|
||||
|
||||
describe('no iframe found', () => {
|
||||
beforeEach(() => {
|
||||
subject.instance().iframe = null;
|
||||
});
|
||||
|
||||
it('throws a TypeError', () => {
|
||||
expect(() => {
|
||||
subject.instance().updateIframe();
|
||||
}).toThrow('Cannot find Storybook iframe');
|
||||
});
|
||||
});
|
||||
|
||||
describe('iframe found', () => {
|
||||
beforeEach(() => {
|
||||
iframe = { style: {} };
|
||||
subject.instance().iframe = iframe;
|
||||
});
|
||||
|
||||
it('sets the viewport information on the iframe', () => {
|
||||
subject.instance().updateIframe();
|
||||
expect(subject.instance().iframe.style).toEqual(resetViewport.styles);
|
||||
});
|
||||
|
||||
it('swaps the height/width when in landscape', () => {
|
||||
subject.instance().state.isLandscape = true;
|
||||
subject.instance().updateIframe();
|
||||
|
||||
expect(subject.instance().iframe.style).toEqual(
|
||||
expect.objectContaining({
|
||||
height: resetViewport.styles.width,
|
||||
width: resetViewport.styles.height,
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
describe('reset button', () => {
|
||||
let resetBtn;
|
||||
|
||||
beforeEach(() => {
|
||||
subject.instance().changeViewport = jest.fn();
|
||||
resetBtn = subject.find('button');
|
||||
});
|
||||
|
||||
it('styles the reset button as disabled if viewport is default', () => {
|
||||
expect(resetBtn.props().style).toEqual(expect.objectContaining(styles.disabled));
|
||||
});
|
||||
|
||||
it('enabels the reset button if not default', () => {
|
||||
subject.setState({ viewport: 'iphone6' });
|
||||
|
||||
// Find updated button
|
||||
resetBtn = subject.find('button');
|
||||
|
||||
expect(resetBtn.props().style).toEqual({
|
||||
...styles.button,
|
||||
marginTop: 30,
|
||||
padding: 20,
|
||||
});
|
||||
});
|
||||
|
||||
it('toggles the landscape on click', () => {
|
||||
resetBtn.simulate('click');
|
||||
expect(subject.instance().changeViewport).toHaveBeenCalledWith(defaultViewport);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SelectViewport', () => {
|
||||
let select;
|
||||
|
||||
beforeEach(() => {
|
||||
select = subject.find('SelectViewport');
|
||||
subject.instance().changeViewport = jest.fn();
|
||||
});
|
||||
|
||||
it('passes the activeViewport', () => {
|
||||
expect(select.props()).toEqual(
|
||||
expect.objectContaining({
|
||||
activeViewport: defaultViewport,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('onChange it updates the viewport', () => {
|
||||
const e = { target: { value: 'iphone6' } };
|
||||
select.simulate('change', e);
|
||||
expect(subject.instance().changeViewport).toHaveBeenCalledWith(e.target.value);
|
||||
});
|
||||
});
|
||||
|
||||
describe('RotateView', () => {
|
||||
let toggle;
|
||||
|
||||
beforeEach(() => {
|
||||
toggle = subject.find('RotateViewport');
|
||||
jest.spyOn(subject.instance(), 'toggleLandscape');
|
||||
subject.instance().forceUpdate();
|
||||
});
|
||||
|
||||
it('passes the active prop based on the state of the panel', () => {
|
||||
expect(toggle.props().active).toEqual(subject.state('isLandscape'));
|
||||
});
|
||||
|
||||
describe('is on the default viewport', () => {
|
||||
beforeEach(() => {
|
||||
subject.setState({ viewport: defaultViewport });
|
||||
});
|
||||
|
||||
it('sets the disabled property', () => {
|
||||
expect(toggle.props().disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is on a responsive viewport', () => {
|
||||
beforeEach(() => {
|
||||
subject.setState({ viewport: 'iphone6' });
|
||||
toggle = subject.find('RotateViewport');
|
||||
});
|
||||
|
||||
it('the disabled property is false', () => {
|
||||
expect(toggle.props().disabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
91
addons/viewport/src/components/tests/RotateViewport.test.js
Normal file
91
addons/viewport/src/components/tests/RotateViewport.test.js
Normal file
@ -0,0 +1,91 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme'; // eslint-disable-line import/no-extraneous-dependencies
|
||||
import { RotateViewport } from '../RotateViewport';
|
||||
import * as styles from '../styles';
|
||||
|
||||
describe('Viewport/RotateViewport', () => {
|
||||
let subject;
|
||||
let props;
|
||||
|
||||
beforeEach(() => {
|
||||
props = {
|
||||
onClick: jest.fn(),
|
||||
};
|
||||
|
||||
subject = shallow(<RotateViewport {...props} />);
|
||||
});
|
||||
|
||||
it('has a label', () => {
|
||||
expect(subject.find('label').text()).toEqual('Rotate');
|
||||
});
|
||||
|
||||
describe('button', () => {
|
||||
let btn;
|
||||
|
||||
beforeEach(() => {
|
||||
btn = subject.find('button');
|
||||
});
|
||||
|
||||
it('has a click handler set via props', () => {
|
||||
// note, this shouldn't trigger if disabled, but enzyme doesn't care
|
||||
btn.simulate('click');
|
||||
expect(props.onClick).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('renders the the action styles on the button', () => {
|
||||
expect(btn.props().style).toEqual(expect.objectContaining(styles.action));
|
||||
});
|
||||
|
||||
describe('is active', () => {
|
||||
beforeEach(() => {
|
||||
subject.setProps({ active: true });
|
||||
btn = subject.find('button');
|
||||
});
|
||||
|
||||
it('renders the correct text', () => {
|
||||
expect(btn.text()).toEqual('Vertical');
|
||||
});
|
||||
});
|
||||
|
||||
describe('is inactive', () => {
|
||||
beforeEach(() => {
|
||||
subject.setProps({ active: false });
|
||||
btn = subject.find('button');
|
||||
});
|
||||
|
||||
it('renders the correct text', () => {
|
||||
expect(btn.text()).toEqual('Landscape');
|
||||
});
|
||||
});
|
||||
|
||||
describe('is disabled', () => {
|
||||
beforeEach(() => {
|
||||
subject.setProps({ disabled: true });
|
||||
btn = subject.find('button');
|
||||
});
|
||||
|
||||
it('renders the disabled styles', () => {
|
||||
expect(btn.props().style).toEqual(expect.objectContaining(styles.disabled));
|
||||
});
|
||||
|
||||
it('sets the disabled property on the button', () => {
|
||||
expect(btn.props().disabled).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('is enabled', () => {
|
||||
beforeEach(() => {
|
||||
subject.setProps({ disabled: false });
|
||||
btn = subject.find('button');
|
||||
});
|
||||
|
||||
it('renders the disabled styles', () => {
|
||||
expect(btn.props().style).not.toEqual(expect.objectContaining(styles.disabled));
|
||||
});
|
||||
|
||||
it('does not set the disabled property on the button', () => {
|
||||
expect(btn.props().disabled).toEqual(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user