├── .babelrc ├── .env ├── .eslintrc.json ├── .github └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .npmignore ├── .storybook ├── .babelrc ├── addons.js ├── config.js ├── preview-head.html └── webpack.config.js ├── .travis.yml ├── README.md ├── config ├── env.js ├── jest │ ├── cssTransform.js │ └── fileTransform.js └── polyfills.js ├── package.json ├── renovate.json ├── run ├── src ├── lib │ ├── components │ │ ├── Accordion │ │ │ ├── Accordion.js │ │ │ ├── Accordion.stories.js │ │ │ ├── Accordion.test.js │ │ │ ├── AccordionItem.js │ │ │ └── __snapshots__ │ │ │ │ └── Accordion.test.js.snap │ │ ├── BlockQuote │ │ │ ├── BlockQuote.js │ │ │ ├── BlockQuote.stories.js │ │ │ ├── BlockQuote.test.js │ │ │ └── __snapshots__ │ │ │ │ └── BlockQuote.test.js.snap │ │ ├── Breadcrumb │ │ │ ├── Breadcrumb.js │ │ │ ├── Breadcrumb.stories.js │ │ │ ├── Breadcrumb.test.js │ │ │ ├── BreadcrumbItem.js │ │ │ └── __snapshots__ │ │ │ │ └── Breadcrumb.test.js.snap │ │ ├── Button │ │ │ ├── Button.js │ │ │ ├── Button.stories.js │ │ │ ├── Button.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Button.test.js.snap │ │ ├── Card │ │ │ ├── Card.js │ │ │ ├── Card.stories.js │ │ │ ├── Card.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Card.test.js.snap │ │ ├── CodeBlock │ │ │ ├── CodeBlock.js │ │ │ ├── CodeBlock.stories.js │ │ │ ├── CodeBlock.test.js │ │ │ └── __snapshots__ │ │ │ │ └── CodeBlock.test.js.snap │ │ ├── CodeSnippet │ │ │ ├── CodeSnippet.js │ │ │ ├── CodeSnippet.stories.js │ │ │ ├── CodeSnippet.test.js │ │ │ └── __snapshots__ │ │ │ │ └── CodeSnippet.test.js.snap │ │ ├── ContextualMenu │ │ │ ├── ContextualMenu.js │ │ │ ├── ContextualMenu.stories.js │ │ │ ├── ContextualMenu.test.js │ │ │ ├── ContextualMenuDropdown.js │ │ │ ├── ContextualMenuGroup.js │ │ │ └── __snapshots__ │ │ │ │ └── ContextualMenu.test.js.snap │ │ ├── DividerList │ │ │ ├── DividerList.js │ │ │ ├── DividerList.stories.js │ │ │ ├── DividerList.test.js │ │ │ ├── DividerListItem.js │ │ │ └── __snapshots__ │ │ │ │ └── DividerList.test.js.snap │ │ ├── Footer │ │ │ ├── Footer.js │ │ │ ├── Footer.stories.js │ │ │ ├── Footer.test.js │ │ │ ├── FooterNav.js │ │ │ ├── FooterNavContainer.js │ │ │ └── __snapshots__ │ │ │ │ └── Footer.test.js.snap │ │ ├── Form │ │ │ ├── FieldSet.js │ │ │ ├── Form.js │ │ │ ├── Form.stories.js │ │ │ ├── Form.test.js │ │ │ ├── FormControl.js │ │ │ ├── FormGroup.js │ │ │ ├── FormHelpText.js │ │ │ └── __snapshots__ │ │ │ │ └── Form.test.js.snap │ │ ├── HeadingIcon │ │ │ ├── HeadingIcon.js │ │ │ ├── HeadingIcon.stories.js │ │ │ ├── HeadingIcon.test.js │ │ │ └── __snapshots__ │ │ │ │ └── HeadingIcon.test.js.snap │ │ ├── Image │ │ │ ├── Image.js │ │ │ ├── Image.stories.js │ │ │ ├── Image.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Image.test.js.snap │ │ ├── InlineImages │ │ │ ├── InlineImages.js │ │ │ ├── InlineImages.stories.js │ │ │ ├── InlineImages.test.js │ │ │ └── __snapshots__ │ │ │ │ └── InlineImages.test.js.snap │ │ ├── Input │ │ │ ├── Input.js │ │ │ ├── Input.stories.js │ │ │ ├── Input.test.js │ │ │ ├── Label.js │ │ │ └── __snapshots__ │ │ │ │ └── Input.test.js.snap │ │ ├── Link │ │ │ ├── Link.js │ │ │ ├── Link.stories.js │ │ │ ├── Link.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Link.test.js.snap │ │ ├── List │ │ │ ├── List.js │ │ │ ├── List.stories.js │ │ │ ├── List.test.js │ │ │ ├── ListItem.js │ │ │ └── __snapshots__ │ │ │ │ └── List.test.js.snap │ │ ├── ListTree │ │ │ ├── ListTree.js │ │ │ ├── ListTree.stories.js │ │ │ ├── ListTree.test.js │ │ │ ├── ListTreeGroup.js │ │ │ ├── ListTreeItem.js │ │ │ └── __snapshots__ │ │ │ │ └── ListTree.test.js.snap │ │ ├── Matrix │ │ │ ├── Matrix.js │ │ │ ├── Matrix.stories.js │ │ │ ├── Matrix.test.js │ │ │ ├── MatrixItem.js │ │ │ └── __snapshots__ │ │ │ │ └── Matrix.test.js.snap │ │ ├── MediaObject │ │ │ ├── MediaObject.js │ │ │ ├── MediaObject.stories.js │ │ │ ├── MediaObject.test.js │ │ │ └── __snapshots__ │ │ │ │ └── MediaObject.test.js.snap │ │ ├── Modal │ │ │ ├── Modal.js │ │ │ ├── Modal.stories.js │ │ │ ├── Modal.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Modal.test.js.snap │ │ ├── MutedHeading │ │ │ ├── MutedHeading.js │ │ │ ├── MutedHeading.stories.js │ │ │ ├── MutedHeading.test.js │ │ │ └── __snapshots__ │ │ │ │ └── MutedHeading.test.js.snap │ │ ├── Navigation │ │ │ ├── Navigation.js │ │ │ ├── Navigation.stories.js │ │ │ ├── Navigation.test.js │ │ │ ├── NavigationBanner.js │ │ │ ├── NavigationLink.js │ │ │ └── __snapshots__ │ │ │ │ └── Navigation.test.js.snap │ │ ├── Notification │ │ │ ├── Notification.js │ │ │ ├── Notification.stories.js │ │ │ ├── Notification.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Notification.test.js.snap │ │ ├── Pagination │ │ │ ├── Pagination.js │ │ │ ├── Pagination.stories.js │ │ │ ├── Pagination.test.js │ │ │ ├── PaginationItem.js │ │ │ └── __snapshots__ │ │ │ │ └── Pagination.test.js.snap │ │ ├── SideNav │ │ │ ├── SideNav.js │ │ │ ├── SideNav.stories.js │ │ │ ├── SideNav.test.js │ │ │ ├── SideNavBanner.js │ │ │ ├── SideNavGroup.js │ │ │ ├── SideNavLink.js │ │ │ └── __snapshots__ │ │ │ │ └── SideNav.test.js.snap │ │ ├── Slider │ │ │ ├── Slider.js │ │ │ ├── Slider.stories.js │ │ │ ├── Slider.test.js │ │ │ ├── SliderInput.js │ │ │ └── __snapshots__ │ │ │ │ └── Slider.test.js.snap │ │ ├── SteppedList │ │ │ ├── SteppedList.js │ │ │ ├── SteppedList.stories.js │ │ │ ├── SteppedList.test.js │ │ │ ├── SteppedListItem.js │ │ │ └── __snapshots__ │ │ │ │ └── SteppedList.test.js.snap │ │ ├── Strip │ │ │ ├── Strip.js │ │ │ ├── Strip.stories.js │ │ │ ├── Strip.test.js │ │ │ ├── StripColumn.js │ │ │ ├── StripRow.js │ │ │ └── __snapshots__ │ │ │ │ └── Strip.test.js.snap │ │ ├── Switch │ │ │ ├── Switch.js │ │ │ ├── Switch.stories.js │ │ │ ├── Switch.test.js │ │ │ └── __snapshots__ │ │ │ │ └── Switch.test.js.snap │ │ ├── Table │ │ │ ├── Table.js │ │ │ ├── Table.stories.js │ │ │ ├── Table.test.js │ │ │ ├── TableCell.js │ │ │ ├── TableRow.js │ │ │ └── __snapshots__ │ │ │ │ └── Table.test.js.snap │ │ ├── Tabs │ │ │ ├── Tabs.js │ │ │ ├── Tabs.stories.js │ │ │ ├── Tabs.test.js │ │ │ ├── TabsItem.js │ │ │ └── __snapshots__ │ │ │ │ └── Tabs.test.js.snap │ │ └── ToolTip │ │ │ ├── ToolTip.js │ │ │ ├── ToolTip.stories.js │ │ │ ├── ToolTip.test.js │ │ │ └── __snapshots__ │ │ │ └── ToolTip.test.js.snap │ ├── index.js │ └── utils │ │ ├── ensureValueInRange.js │ │ └── getClassName.js ├── setupTests.js └── tempPolyfills.js └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "production": { 4 | "presets": [ 5 | [ 6 | "es2015", { "modules": false } 7 | ], 8 | "react", 9 | "stage-0" 10 | ], 11 | "plugins": ["transform-es2015-modules-commonjs"], 12 | "ignore": ["**/*.stories.js", "**/*.test.js"] 13 | }, 14 | "test": { 15 | "presets": ["es2015", "react", "stage-0"] 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | PORT=8102 2 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "browser": true, 5 | "jest": true 6 | }, 7 | "plugins": [ 8 | "react" 9 | ], 10 | "rules": { 11 | "react/jsx-filename-extension": [1, { "extensions": [".js", ".jsx"] }], 12 | "react/prefer-stateless-function": 0, 13 | "react/forbid-prop-types": 0, 14 | "function-paren-newline": 0, 15 | "arrow-body-style": 2, 16 | "import/no-extraneous-dependencies": [ 17 | "error", 18 | { 19 | "devDependencies": ["**/*.stories.js", "**/*.test.js" ] 20 | } 21 | ] 22 | } 23 | } -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Done 2 | 3 | [List of work items including drive-bys] 4 | 5 | ## QA 6 | 7 | - Check out this feature branch 8 | - Run the site using the command `./run serve` 9 | - View the site locally in your web browser at: [http://0.0.0.0:8102/](http://0.0.0.0:8102/) 10 | - [List additional steps to QA the new features or prove the bug has been resolved] 11 | 12 | 13 | ## Issue / Card 14 | 15 | [List of links to Github issues/bugs and cards if needed - e.g. `Fixes #1`] 16 | 17 | ## Screenshots 18 | 19 | [if relevant, include a screenshot] 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # [generated] Bracketed sections updated by Yeoman generator 2 | # generator-canonical-webteam@2.2.0 3 | 4 | # [os] OS & editor files 5 | Desktop.ini 6 | Thumbs.db 7 | ._* 8 | *.DS_Store 9 | *~ 10 | \#*\# 11 | .AppleDouble 12 | .LSOverride 13 | .spelling 14 | .vscode 15 | 16 | # [cache] Cache and backup 17 | *.bak 18 | *.pyc 19 | *-cache/ 20 | 21 | # [data] Local data 22 | *.sqlite* 23 | *.log 24 | logs/ 25 | pids 26 | *.pid 27 | *.seed 28 | .*-metadata 29 | 30 | # [deps] Local dependencies 31 | .bundle/ 32 | node_modules/ 33 | vendor/ 34 | bower_components/ 35 | vendor/ 36 | # Normally lockfiles would be committed, but we use yarn instead of NPM for locking dependencies 37 | package-lock.json 38 | 39 | # [build] Built files 40 | /build/ 41 | /parts/ 42 | /prime/ 43 | /stage/ 44 | .snapcraft/ 45 | _site/ 46 | *.*.map 47 | 48 | # [env] Local environment settings 49 | .docker-project 50 | .*.hash 51 | .envrc 52 | env/ 53 | env[23]/ 54 | 55 | # [sass] Files generated by Sass 56 | *.css 57 | 58 | # testing 59 | /coverage 60 | 61 | # misc 62 | .env.local 63 | .env.development.local 64 | .env.test.local 65 | .env.production.local 66 | 67 | # project-specific ignore files 68 | /dist 69 | /storybook-static 70 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /* 2 | !/build 3 | -------------------------------------------------------------------------------- /.storybook/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-0"] 3 | } 4 | -------------------------------------------------------------------------------- /.storybook/addons.js: -------------------------------------------------------------------------------- 1 | import '@storybook/addon-knobs/register'; 2 | import '@storybook/addon-options/register'; 3 | -------------------------------------------------------------------------------- /.storybook/config.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { configure, getStorybook, setAddon, addDecorator } from '@storybook/react'; 3 | import { setDefaults } from '@storybook/addon-info'; 4 | import { withKnobs } from '@storybook/addon-knobs'; 5 | import { setOptions } from '@storybook/addon-options'; 6 | import createPercyAddon from '@percy-io/percy-storybook'; 7 | 8 | const storybookStyling = (storyFn) => { 9 | const style = { 10 | padding: '0 1.25rem', 11 | } 12 | 13 | return ( 14 |
15 | { storyFn() } 16 |
17 | ) 18 | } 19 | 20 | function loadStories() { 21 | // Set custom global decorators 22 | addDecorator(storybookStyling); 23 | addDecorator(withKnobs); 24 | 25 | const req = require.context('../src', true, /.stories.js$/); 26 | req.keys().forEach((filename) => req(filename)); 27 | } 28 | 29 | configure(loadStories, module); 30 | 31 | // addon-info 32 | setDefaults({ 33 | inline: true, // Displays info inline vs click button to view 34 | }); 35 | 36 | // Percy snaps 37 | const { percyAddon, serializeStories } = createPercyAddon(); 38 | setAddon(percyAddon); 39 | serializeStories(getStorybook); 40 | 41 | // override option defaults: 42 | setOptions({ 43 | name: 'Vanilla Framework React', 44 | url: 'https://github.com/vanilla-framework/vanilla-framework-react', 45 | downPanelInRight: true, 46 | }); 47 | -------------------------------------------------------------------------------- /.storybook/preview-head.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.storybook/webpack.config.js: -------------------------------------------------------------------------------- 1 | const FriendlyErrorsPlugin = require('friendly-errors-webpack-plugin') 2 | 3 | module.exports = (storybookBaseConfig, env, defaultConfig) => { 4 | const host = process.env.HOST || 'localhost'; 5 | const port = process.env.PORT || 8102; 6 | storybookBaseConfig.plugins.push(new FriendlyErrorsPlugin({ 7 | compilationSuccessInfo: { 8 | messages: [`Your application is running here: http://${host}:${port}\n`], 9 | }, 10 | })); 11 | storybookBaseConfig.devServer = {quiet: true}; 12 | 13 | return storybookBaseConfig; 14 | }; 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | cache: 5 | directories: 6 | - node_modules 7 | script: 8 | - yarn lint 9 | - yarn jest --env=jsdom 10 | - yarn snapshot 11 | deploy: 12 | provider: pages 13 | skip_cleanup: true 14 | github_token: $github_token 15 | local_dir: storybook-static 16 | on: 17 | branch: master 18 | -------------------------------------------------------------------------------- /config/jest/cssTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // This is a custom Jest transformer turning style imports into empty objects. 4 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 5 | 6 | module.exports = { 7 | process() { 8 | return 'module.exports = {};'; 9 | }, 10 | getCacheKey() { 11 | // The output is always the same. 12 | return 'cssTransform'; 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /config/jest/fileTransform.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const path = require('path'); 4 | 5 | // This is a custom Jest transformer turning file imports into filenames. 6 | // http://facebook.github.io/jest/docs/tutorial-webpack.html 7 | 8 | module.exports = { 9 | process(src, filename) { 10 | return `module.exports = ${JSON.stringify(path.basename(filename))};`; 11 | }, 12 | }; 13 | -------------------------------------------------------------------------------- /config/polyfills.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | if (typeof Promise === 'undefined') { 4 | // Rejection tracking prevents a common issue where React gets into an 5 | // inconsistent state due to an error, but it gets swallowed by a Promise, 6 | // and the user has no idea what causes React's erratic future behavior. 7 | require('promise/lib/rejection-tracking').enable(); 8 | window.Promise = require('promise/lib/es6-extensions.js'); 9 | } 10 | 11 | // fetch() polyfill for making API calls. 12 | require('whatwg-fetch'); 13 | 14 | // Object.assign() is commonly used with React. 15 | // It will use the native implementation if it's present and isn't buggy. 16 | Object.assign = require('object-assign'); 17 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "config:base", 4 | "schedule:monthly", 5 | "group:all" 6 | ], 7 | "packageRules": [ 8 | { 9 | "depTypeList": ["devDependencies"], 10 | "extends": [":automergeMinor"] 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/components/Accordion/Accordion.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | import AccordionItem from './AccordionItem'; 5 | 6 | class Accordion extends React.Component { 7 | constructor(props) { 8 | super(props); 9 | this.toggleSection = this.toggleSection.bind(this); 10 | 11 | this.state = { open: props.selected }; 12 | } 13 | 14 | componentWillReceiveProps(nextProps) { 15 | this.setState({ open: nextProps.selected }); 16 | } 17 | 18 | toggleSection(index) { 19 | const { allowMultiple } = this.props; 20 | let newOpenArray = [...this.state.open]; 21 | 22 | if (newOpenArray.includes(index)) { 23 | newOpenArray = newOpenArray.filter(item => item !== index); 24 | } else { 25 | newOpenArray = allowMultiple ? [...newOpenArray, index] : [index]; 26 | } 27 | 28 | this.setState({ open: newOpenArray }); 29 | } 30 | 31 | render() { 32 | const { open } = this.state; 33 | const accordionItems = React.Children.map(this.props.children, 34 | (child, index) => React.cloneElement(child, { 35 | index, 36 | isOpen: open.includes(index), 37 | onClick: this.toggleSection, 38 | }), 39 | ); 40 | 41 | return ( 42 | 47 | ); 48 | } 49 | } 50 | 51 | Accordion.defaultProps = { 52 | children: null, 53 | selected: [], 54 | allowMultiple: false, 55 | }; 56 | 57 | Accordion.propTypes = { 58 | children: (props, propName, componentName) => { 59 | const prop = props[propName]; 60 | let error = null; 61 | 62 | React.Children.forEach(prop, (child) => { 63 | if (child.type !== AccordionItem) { 64 | error = new Error(`${componentName} children should be of type "AccordionItem".`); 65 | } 66 | }); 67 | 68 | return error; 69 | }, 70 | selected: PropTypes.arrayOf(PropTypes.number), 71 | allowMultiple: PropTypes.bool, 72 | }; 73 | 74 | Accordion.displayName = 'Accordion'; 75 | 76 | export default Accordion; 77 | -------------------------------------------------------------------------------- /src/lib/components/Accordion/AccordionItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const AccordionItem = (props) => { 5 | const { 6 | isOpen, onClick, index, title, children, 7 | } = props; 8 | 9 | return ( 10 |
  • 11 | 22 |
    29 | { children } 30 |
    31 |
  • 32 | ); 33 | }; 34 | 35 | AccordionItem.defaultProps = { 36 | index: 0, 37 | isOpen: false, 38 | onClick: () => 1, 39 | }; 40 | 41 | AccordionItem.propTypes = { 42 | index: PropTypes.number, 43 | title: PropTypes.string.isRequired, 44 | children: PropTypes.element.isRequired, 45 | isOpen: PropTypes.bool, 46 | onClick: PropTypes.func, 47 | }; 48 | 49 | AccordionItem.displayName = 'AccordionItem'; 50 | 51 | export default AccordionItem; 52 | -------------------------------------------------------------------------------- /src/lib/components/Accordion/__snapshots__/Accordion.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` renders Accordion with AccordionItems correctly 1`] = ` 4 | 91 | `; 92 | -------------------------------------------------------------------------------- /src/lib/components/BlockQuote/BlockQuote.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const BlockQuote = (props) => { 5 | if (props.pull) { 6 | return ( 7 |
    8 |

    {props.quote}

    9 | {props.citation} 10 |
    11 | ); 12 | } 13 | 14 | return ( 15 |
    16 |

    17 | "{props.quote}" 18 |

    19 |

    20 | {props.citation} 21 |

    22 |
    23 | ); 24 | }; 25 | 26 | BlockQuote.defaultProps = { 27 | citation: '', 28 | pull: false, 29 | }; 30 | 31 | BlockQuote.propTypes = { 32 | quote: PropTypes.string.isRequired, 33 | citation: PropTypes.string, 34 | pull: PropTypes.bool, 35 | }; 36 | 37 | BlockQuote.displayName = 'BlockQuote'; 38 | 39 | export default BlockQuote; 40 | -------------------------------------------------------------------------------- /src/lib/components/BlockQuote/BlockQuote.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { text, boolean } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import BlockQuote from './BlockQuote'; 7 | 8 | storiesOf('Block Quote', module) 9 | .add('Default', 10 | withInfo('Basic blockquotes and citations.')(() => ( 11 |
    ), 16 | ), 17 | ) 18 | 19 | .add('Pull Quote', 20 | withInfo('Use the pull quote pattern to highlight content from different sources in a visually prominent way.')(() => ( 21 |
    ), 26 | ), 27 | ); 28 | -------------------------------------------------------------------------------- /src/lib/components/BlockQuote/BlockQuote.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import BlockQuote from './BlockQuote'; 4 | 5 | describe('BlockQuote component', () => { 6 | it('should render a basic blockquote correctly', () => { 7 | const blockQuote = ReactTestRenderer.create( 8 |
    , 12 | ); 13 | const json = blockQuote.toJSON(); 14 | expect(json).toMatchSnapshot(); 15 | }); 16 | 17 | it('should render a basic blockquote with citation correctly', () => { 18 | const blockQuote = ReactTestRenderer.create( 19 |
    , 24 | ); 25 | const json = blockQuote.toJSON(); 26 | expect(json).toMatchSnapshot(); 27 | }); 28 | 29 | it('should render a pull quote correctly', () => { 30 | const blockQuote = ReactTestRenderer.create( 31 |
    , 37 | ); 38 | const json = blockQuote.toJSON(); 39 | expect(json).toMatchSnapshot(); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /src/lib/components/BlockQuote/__snapshots__/BlockQuote.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`BlockQuote component should render a basic blockquote correctly 1`] = ` 4 |
    5 |

    6 | 7 | " 8 | 9 | Ubuntu is an ancient African word meaning 'humanity to others'. Ubuntu is an ancient African word meaning 'humanity to others'. 10 | 11 | " 12 | 13 |

    14 |

    15 | 16 | 17 | 18 |

    19 |
    20 | `; 21 | 22 | exports[`BlockQuote component should render a basic blockquote with citation correctly 1`] = ` 23 |
    24 |

    25 | 26 | " 27 | 28 | Ubuntu is an ancient African word meaning 'humanity to others'. Ubuntu is an ancient African word meaning 'humanity to others'. 29 | 30 | " 31 | 32 |

    33 |

    34 | 35 | Canonical 36 | 37 |

    38 |
    39 | `; 40 | 41 | exports[`BlockQuote component should render a pull quote correctly 1`] = ` 42 |
    45 |

    46 | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam 47 |

    48 | 51 | A. N. Author 52 | 53 |
    54 | `; 55 | -------------------------------------------------------------------------------- /src/lib/components/Breadcrumb/Breadcrumb.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Breadcrumb = props => ( 5 |
      6 | { props.children } 7 |
    8 | ); 9 | 10 | Breadcrumb.propTypes = { 11 | children: PropTypes.node.isRequired, 12 | }; 13 | 14 | Breadcrumb.displayName = 'Breadcrumb'; 15 | 16 | export default Breadcrumb; 17 | -------------------------------------------------------------------------------- /src/lib/components/Breadcrumb/Breadcrumb.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { withInfo } from '@storybook/addon-info'; 4 | 5 | import Breadcrumb from './Breadcrumb'; 6 | import BreadcrumbItem from './BreadcrumbItem'; 7 | 8 | storiesOf('Breadcrumb', module) 9 | .add('Default', 10 | withInfo('You can use the breadcrumbs pattern to indicate where the current page sits in the sites navigation. The separators between each item are added via CSS, so you dont have to include them manually.')(() => ( 11 | 12 | Breadcrumb One 13 | Breadcrumb Two 14 | Breadcrumb Three 15 | Breadcrumb Four 16 | ), 17 | ), 18 | ); 19 | -------------------------------------------------------------------------------- /src/lib/components/Breadcrumb/Breadcrumb.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import Breadcrumb from './Breadcrumb'; 4 | import BreadcrumbItem from './BreadcrumbItem'; 5 | 6 | describe('Breadcrumb component', () => { 7 | it('should render a simple Breadcrumb correctly', () => { 8 | const breadcrumb = ReactTestRenderer.create( 9 | 10 | Lorem 11 | Ipsum 12 | Dolor 13 | , 14 | ); 15 | const json = breadcrumb.toJSON(); 16 | expect(json).toMatchSnapshot(); 17 | }); 18 | 19 | it('should render a Breadcrumb without links', () => { 20 | const breadcrumb = ReactTestRenderer.create( 21 | 22 | Lorem 23 | Ipsum 24 | Dolor 25 | , 26 | ); 27 | const json = breadcrumb.toJSON(); 28 | expect(json).toMatchSnapshot(); 29 | }); 30 | }); 31 | -------------------------------------------------------------------------------- /src/lib/components/Breadcrumb/BreadcrumbItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const BreadcrumbItem = props => ( 5 |
  • 6 | { props.children } 7 |
  • 8 | ); 9 | 10 | BreadcrumbItem.propTypes = { 11 | children: PropTypes.node.isRequired, 12 | }; 13 | 14 | BreadcrumbItem.displayName = 'BreadcrumbItem'; 15 | 16 | export default BreadcrumbItem; 17 | -------------------------------------------------------------------------------- /src/lib/components/Breadcrumb/__snapshots__/Breadcrumb.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Breadcrumb component should render a Breadcrumb without links 1`] = ` 4 | 31 | `; 32 | 33 | exports[`Breadcrumb component should render a simple Breadcrumb correctly 1`] = ` 34 | 65 | `; 66 | -------------------------------------------------------------------------------- /src/lib/components/Button/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | 5 | class Button extends React.Component { 6 | constructor() { 7 | super(); 8 | this.onClick = this.onClick.bind(this); 9 | } 10 | 11 | onClick(event) { 12 | const { disabled, onClick } = this.props; 13 | 14 | if (disabled) { 15 | event.preventDefault(); 16 | return; 17 | } 18 | 19 | if (onClick) { 20 | onClick(event); 21 | } 22 | } 23 | 24 | render() { 25 | const { 26 | children, className, value, disabled, href, positive, 27 | negative, brand, neutral, inline, ...otherProps 28 | } = this.props; 29 | const Tag = href ? 'a' : 'button'; 30 | 31 | const classNames = getClassName({ 32 | [className]: className, 33 | 'p-button--base': !(positive || negative || brand || neutral), 34 | 'p-button--neutral': neutral, 35 | 'p-button--positive': positive, 36 | 'p-button--negative': negative, 37 | 'p-button--brand': brand, 38 | 'is-inline': inline, 39 | 'is--disabled': disabled, 40 | }) || undefined; 41 | 42 | return ( 43 | 50 | { value || children } 51 | 52 | ); 53 | } 54 | } 55 | 56 | Button.defaultProps = { 57 | brand: false, 58 | children: null, 59 | className: null, 60 | disabled: false, 61 | href: null, 62 | inline: false, 63 | negative: false, 64 | neutral: false, 65 | onClick: () => null, 66 | positive: false, 67 | value: null, 68 | }; 69 | 70 | Button.propTypes = { 71 | brand: PropTypes.bool, 72 | children: PropTypes.node, 73 | className: PropTypes.string, 74 | disabled: PropTypes.bool, 75 | href: PropTypes.string, 76 | inline: PropTypes.bool, 77 | negative: PropTypes.bool, 78 | neutral: PropTypes.bool, 79 | onClick: PropTypes.func, 80 | positive: PropTypes.bool, 81 | value: PropTypes.string, 82 | }; 83 | 84 | Button.displayName = 'Button'; 85 | 86 | export default Button; 87 | -------------------------------------------------------------------------------- /src/lib/components/Button/Button.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import { mount, shallow } from 'enzyme'; 4 | import Button from './Button'; 5 | 6 | describe('); 30 | expect(button.find('button').hostNodes().length).toBe(1); 31 | expect(button.find('a').hostNodes().length).toBe(0); 32 | }); 33 | 34 | it('should render an anchor element if href exists', () => { 35 | const button = shallow(); 36 | expect(button.find('a').hostNodes().length).toBe(1); 37 | expect(button.find('button').hostNodes().length).toBe(0); 38 | }); 39 | 40 | it('should be the same whether children or value prop is passed through', () => { 41 | const text = 'Value'; 42 | const buttonChild = shallow(); 43 | const buttonValue = shallow(); 53 | 54 | button.find('button').hostNodes().simulate('click'); 55 | expect(onClick).toHaveBeenCalled(); 56 | }); 57 | 58 | it('does not call onClick if it exists and is disabled', () => { 59 | const onClick = jest.fn(); 60 | const button = mount(); 61 | 62 | button.find('button').hostNodes().simulate('click'); 63 | expect(onClick).not.toHaveBeenCalled(); 64 | }); 65 | }); 66 | 67 | -------------------------------------------------------------------------------- /src/lib/components/Button/__snapshots__/Button.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[` 13 | 21 | 29 | 37 | 45 | 53 | 54 | `; 55 | 56 | exports[` 65 | `; 66 | -------------------------------------------------------------------------------- /src/lib/components/Card/Card.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class Card extends React.Component { 5 | constructor() { 6 | super(); 7 | this.getClassString = this.getClassString.bind(this); 8 | this.renderHeader = this.renderHeader.bind(this); 9 | } 10 | 11 | getClassString() { 12 | let classString = 'p-card'; 13 | 14 | if (this.props.highlighted) classString = `${classString} p-card--highlighted`; 15 | if (this.props.overlay) classString = `${classString} p-card--overlay`; 16 | 17 | return classString; 18 | } 19 | 20 | renderHeader() { 21 | const { header, image } = this.props; 22 | 23 | return ( 24 |
    25 | {image ? {image.alt} :

    {header}

    } 26 |
    27 | ); 28 | } 29 | 30 | render() { 31 | return ( 32 |
    33 | { (this.props.header || this.props.image) && this.renderHeader() } 34 | { this.props.title && 35 |

    {this.props.title}

    36 | } 37 | { this.props.children } 38 |
    39 | ); 40 | } 41 | } 42 | 43 | Card.defaultProps = { 44 | header: null, 45 | highlighted: false, 46 | image: null, 47 | overlay: false, 48 | title: null, 49 | }; 50 | 51 | Card.propTypes = { 52 | children: PropTypes.node.isRequired, 53 | header: PropTypes.string, 54 | highlighted: PropTypes.bool, 55 | image: PropTypes.shape({ 56 | src: PropTypes.string, 57 | alt: PropTypes.string, 58 | }), 59 | overlay: PropTypes.bool, 60 | title: PropTypes.string, 61 | }; 62 | 63 | Card.displayName = 'Card'; 64 | 65 | export default Card; 66 | -------------------------------------------------------------------------------- /src/lib/components/Card/Card.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { text, boolean } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import Card from './Card'; 7 | import Strip from '../Strip/Strip'; 8 | import StripColumn from '../Strip/StripColumn'; 9 | import StripRow from '../Strip/StripRow'; 10 | 11 | storiesOf('Card', module) 12 | .add('Default', 13 | withInfo('The purpose of the basic card is to display information, without user interaction.')(() => ( 14 | 20 |

    {text('Text', 'Lorem ipsum dolor sit amet, consectetur adipisicing.')}

    21 |
    ), 22 | ), 23 | ) 24 | 25 | .add('With Header', 26 | withInfo('Card components accept either a header or image prop for an optional header.')(() => ( 27 | 33 |

    {text('Text', 'Lorem ipsum dolor sit amet, consectetur adipisicing.')}

    34 |
    ), 35 | ), 36 | ) 37 | 38 | .add('With Image', 39 | withInfo('Card components accept either a header or image prop for an optional header.')(() => ( 40 | 49 |

    {text('Text', 'Lorem ipsum dolor sit amet, consectetur adipisicing.')}

    50 |
    ), 51 | ), 52 | ) 53 | 54 | .add('Highlighted', 55 | withInfo('The highlighted card should be used when you can interact with the content.')(() => ( 56 | 65 |

    {text('Text', 'Lorem ipsum dolor sit amet, consectetur adipisicing.')}

    66 |
    ), 67 | ), 68 | ) 69 | 70 | .add('Overlay', 71 | withInfo('The purpose of the overlay prop is to make the text visible in conjunction with a Strip image component.')(() => ( 72 | 77 | 78 |
    79 | 80 | 89 |

    {text('Text', 'Lorem ipsum dolor sit amet, consectetur adipisicing.')}

    90 |
    91 |
    92 | 93 | ), 94 | ), 95 | ); 96 | -------------------------------------------------------------------------------- /src/lib/components/Card/Card.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import Card from './Card'; 4 | 5 | describe('Card component', () => { 6 | it('should render a basic Card correctly', () => { 7 | const card = ReactTestRenderer.create( 8 | 9 |

    Lorem ipsum dolor sit amet, consectetur adipisicing.

    10 |
    ); 11 | const json = card.toJSON(); 12 | expect(json).toMatchSnapshot(); 13 | }); 14 | 15 | it('should render Card with an image correctly', () => { 16 | const card = ReactTestRenderer.create( 17 | 24 |

    Lorem ipsum dolor sit amet, consectetur adipisicing.

    25 |
    ); 26 | const json = card.toJSON(); 27 | expect(json).toMatchSnapshot(); 28 | }); 29 | 30 | it('should render a highlighted Card correctly', () => { 31 | const card = ReactTestRenderer.create( 32 | 40 |

    Lorem ipsum dolor sit amet, consectetur adipisicing.

    41 |
    ); 42 | const json = card.toJSON(); 43 | expect(json).toMatchSnapshot(); 44 | }); 45 | 46 | it('should render an overlay Card correctly', () => { 47 | const card = ReactTestRenderer.create( 48 | 56 |

    Lorem ipsum dolor sit amet, consectetur adipisicing.

    57 |
    ); 58 | const json = card.toJSON(); 59 | expect(json).toMatchSnapshot(); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /src/lib/components/Card/__snapshots__/Card.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Card component should render Card with an image correctly 1`] = ` 4 |
    7 |
    10 | Placeholder 14 |
    15 |

    18 | Card title 19 |

    20 |

    21 | Lorem ipsum dolor sit amet, consectetur adipisicing. 22 |

    23 |
    24 | `; 25 | 26 | exports[`Card component should render a basic Card correctly 1`] = ` 27 |
    30 |

    33 | Card title 34 |

    35 |

    36 | Lorem ipsum dolor sit amet, consectetur adipisicing. 37 |

    38 |
    39 | `; 40 | 41 | exports[`Card component should render a highlighted Card correctly 1`] = ` 42 |
    45 |
    48 | Placeholder 52 |
    53 |

    56 | Card title 57 |

    58 |

    59 | Lorem ipsum dolor sit amet, consectetur adipisicing. 60 |

    61 |
    62 | `; 63 | 64 | exports[`Card component should render an overlay Card correctly 1`] = ` 65 |
    68 |
    71 | Placeholder 75 |
    76 |

    79 | Card title 80 |

    81 |

    82 | Lorem ipsum dolor sit amet, consectetur adipisicing. 83 |

    84 |
    85 | `; 86 | -------------------------------------------------------------------------------- /src/lib/components/CodeBlock/CodeBlock.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOMServer from 'react-dom/server'; 3 | import PropTypes from 'prop-types'; 4 | import dedent from 'dedent-js'; 5 | import pretty from 'pretty'; 6 | 7 | const CodeBlock = (props) => { 8 | const { numbered } = props; 9 | let code = props.children; 10 | 11 | if (typeof code === 'string') { 12 | code = dedent(code); 13 | } else { 14 | code = pretty(ReactDOMServer.renderToStaticMarkup(code), { ocd: true }); 15 | } 16 | 17 | if (numbered) { 18 | const codeArray = code.split(/\r?\n/); 19 | code = codeArray.map((line, i) => ( 20 | { codeArray[i] } 21 | )); 22 | } 23 | 24 | return ( 25 |
    26 |       
    27 |         { code }
    28 |       
    29 |     
    30 | ); 31 | }; 32 | 33 | CodeBlock.defaultProps = { 34 | children: '', 35 | numbered: false, 36 | }; 37 | 38 | CodeBlock.propTypes = { 39 | children: PropTypes.node, 40 | numbered: PropTypes.bool, 41 | }; 42 | 43 | CodeBlock.displayName = 'CodeBlock'; 44 | 45 | export default CodeBlock; 46 | -------------------------------------------------------------------------------- /src/lib/components/CodeBlock/CodeBlock.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { boolean } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import CodeBlock from './CodeBlock'; 7 | 8 | storiesOf('Code Block', module) 9 | .add('Default', 10 | withInfo('The Code Block component is used to display a large amount of code. The preferred prop is a single template literal, to preserve formatting. Alternatively, markup can be used but will be formatted automatically. When you refer to code inline with other text, use the tag instead.')(() => ( 11 | 12 | {`this is code sample line 1 13 | this is code sample line 2 14 | this is code sample line 3 15 | 16 | this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 17 | this is code sample line 5`} 18 | ), 19 | ), 20 | ).add('Numbered', 21 | withInfo('The code numbered pattern can be used when displaying large blocks of code to enable users to quickly reference a specific line.')(() => ( 22 | 23 | {`this is code sample line 1 24 | this is code sample line 2 25 | this is code sample line 3 26 | 27 | this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 this is code sample line 4 28 | this is code sample line 5`} 29 | ), 30 | ), 31 | ); 32 | -------------------------------------------------------------------------------- /src/lib/components/CodeBlock/CodeBlock.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import CodeBlock from './CodeBlock'; 4 | 5 | describe('CodeBlock component', () => { 6 | it('should render one line of code correctly', () => { 7 | const codeBlock = ReactTestRenderer.create( 8 | 9 | {'this is a single line of code'} 10 | , 11 | ); 12 | const json = codeBlock.toJSON(); 13 | expect(json).toMatchSnapshot(); 14 | }); 15 | 16 | it('should render multiple lines of code correctly', () => { 17 | const codeBlock = ReactTestRenderer.create( 18 | 19 | {`this is code sample line 1 20 | this is code sample line 2 21 | this is code sample line 3`} 22 | , 23 | ); 24 | const json = codeBlock.toJSON(); 25 | expect(json).toMatchSnapshot(); 26 | }); 27 | 28 | it('should render one line of code correctly with numbered modifier', () => { 29 | const codeBlock = ReactTestRenderer.create( 30 | 31 | {'this is a single line of code'} 32 | , 33 | ); 34 | const json = codeBlock.toJSON(); 35 | expect(json).toMatchSnapshot(); 36 | }); 37 | 38 | it('should render multiple lines of code correctly with numbered modifier', () => { 39 | const codeBlock = ReactTestRenderer.create( 40 | 41 | {`this is code sample line 1 42 | this is code sample line 2 43 | this is code sample line 3`} 44 | , 45 | ); 46 | const json = codeBlock.toJSON(); 47 | expect(json).toMatchSnapshot(); 48 | }); 49 | 50 | it('should display a single blank line if component is empty', () => { 51 | const codeBlock = ReactTestRenderer.create( 52 | , 53 | ); 54 | const json = codeBlock.toJSON(); 55 | expect(json).toMatchSnapshot(); 56 | }); 57 | 58 | it('should render markup correctly', () => { 59 | const codeBlock = ReactTestRenderer.create( 60 | 61 | 62 | Markup Example 63 | 64 | 65 |
    This is an example of using markup in the CodeBlock component
    66 |
    This & should escape
    67 | 68 |
    , 69 | ); 70 | const json = codeBlock.toJSON(); 71 | expect(json).toMatchSnapshot(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/lib/components/CodeBlock/__snapshots__/CodeBlock.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CodeBlock component should display a single blank line if component is empty 1`] = ` 4 |
     7 |   
     8 |     
     9 |   
    10 | 
    11 | `; 12 | 13 | exports[`CodeBlock component should render markup correctly 1`] = ` 14 |
    17 |   
    18 |     <head>
    19 |   <title>Markup Example</title>
    20 | </head>
    21 | <body>
    22 |   <div>This is an example of using markup in the CodeBlock component</div>
    23 |   <div>This & should escape</div>
    24 | </body>
    25 |   
    26 | 
    27 | `; 28 | 29 | exports[`CodeBlock component should render multiple lines of code correctly 1`] = ` 30 |
    33 |   
    34 |     this is code sample line 1
    35 | this is code sample line 2
    36 | this is code sample line 3
    37 |   
    38 | 
    39 | `; 40 | 41 | exports[`CodeBlock component should render multiple lines of code correctly with numbered modifier 1`] = ` 42 |
    45 |   
    46 |     
    49 |       this is code sample line 1
    50 |     
    51 |     
    54 |       this is code sample line 2
    55 |     
    56 |     
    59 |       this is code sample line 3
    60 |     
    61 |   
    62 | 
    63 | `; 64 | 65 | exports[`CodeBlock component should render one line of code correctly 1`] = ` 66 |
    69 |   
    70 |     this is a single line of code
    71 |   
    72 | 
    73 | `; 74 | 75 | exports[`CodeBlock component should render one line of code correctly with numbered modifier 1`] = ` 76 |
    79 |   
    80 |     
    83 |       this is a single line of code
    84 |     
    85 |   
    86 | 
    87 | `; 88 | -------------------------------------------------------------------------------- /src/lib/components/CodeSnippet/CodeSnippet.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | class CodeSnippet extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | this.handleInputClick = this.handleInputClick.bind(this); 8 | this.handleButtonClick = this.handleButtonClick.bind(this); 9 | } 10 | 11 | handleInputClick() { 12 | this.textInput.select(); 13 | } 14 | 15 | handleButtonClick() { 16 | this.textInput.select(); 17 | try { 18 | document.execCommand('copy'); 19 | } catch (err) { 20 | console.warn('Unable to copy'); // eslint-disable-line no-console 21 | } 22 | } 23 | 24 | render() { 25 | return ( 26 |
    27 | { this.textInput = input; }} 33 | /> 34 | 39 |
    40 | ); 41 | } 42 | } 43 | 44 | CodeSnippet.propTypes = { 45 | value: PropTypes.string.isRequired, 46 | }; 47 | 48 | CodeSnippet.displayName = 'CodeSnippet'; 49 | 50 | export default CodeSnippet; 51 | -------------------------------------------------------------------------------- /src/lib/components/CodeSnippet/CodeSnippet.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { text } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import CodeSnippet from './CodeSnippet'; 7 | 8 | storiesOf('Code Snippet', module) 9 | .add('Default', 10 | withInfo('Code snippet should be used when presenting the user with a small snippet of code that they will likely want to copy and paste.')(() => 11 | , 12 | ), 13 | ); 14 | -------------------------------------------------------------------------------- /src/lib/components/CodeSnippet/CodeSnippet.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import CodeSnippet from './CodeSnippet'; 4 | 5 | describe('CodeSnippet component', () => { 6 | it('should compare with a snapshot', () => { 7 | const codesnippet = ReactTestRenderer.create(); 8 | const json = codesnippet.toJSON(); 9 | expect(json).toMatchSnapshot(); 10 | }); 11 | }); 12 | 13 | -------------------------------------------------------------------------------- /src/lib/components/CodeSnippet/__snapshots__/CodeSnippet.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`CodeSnippet component should compare with a snapshot 1`] = ` 4 |
    7 | 13 | 19 |
    20 | `; 21 | -------------------------------------------------------------------------------- /src/lib/components/ContextualMenu/ContextualMenu.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | 5 | import ContextualMenuDropdown from './ContextualMenuDropdown'; 6 | 7 | class ContextualMenu extends React.Component { 8 | constructor() { 9 | super(); 10 | 11 | this.toggleOpen = this.toggleOpen.bind(this); 12 | this.state = { open: false }; 13 | } 14 | 15 | toggleOpen() { 16 | this.setState({ open: !this.state.open }); 17 | } 18 | 19 | render() { 20 | const { 21 | className, children, id, position, ...otherProps 22 | } = this.props; 23 | 24 | const classNames = getClassName({ 25 | [className]: className, 26 | 'p-contextual-menu': true, 27 | [`p-contextual-menu--${position}`]: position, 28 | }) || undefined; 29 | 30 | const toggle = React.Children.map(children, (child) => { 31 | if (child.type !== ContextualMenuDropdown) { 32 | return React.cloneElement(child, { 33 | className: 'p-contextual-menu__toggle', 34 | 'aria-controls': id, 35 | 'aria-expanded': this.state.open, 36 | 'aria-haspopup': 'true', 37 | role: 'link', 38 | onClick: this.toggleOpen, 39 | tabIndex: 0, 40 | }); 41 | } else if (child.type === ContextualMenuDropdown) { 42 | return React.cloneElement(child, { 43 | id, 44 | 'aria-hidden': !this.state.open, 45 | }); 46 | } 47 | return child; 48 | }); 49 | 50 | return ( 51 | 55 | {toggle} 56 | 57 | ); 58 | } 59 | } 60 | 61 | ContextualMenu.defaultProps = { 62 | children: null, 63 | className: null, 64 | position: null, 65 | }; 66 | 67 | ContextualMenu.propTypes = { 68 | children: (props, propName, componentName) => { 69 | const prop = props[propName]; 70 | let error = null; 71 | let count = 0; 72 | 73 | React.Children.forEach(prop, (child) => { 74 | if (child.type === ContextualMenuDropdown) { 75 | count += 1; 76 | } 77 | }); 78 | 79 | if (count !== 1) { 80 | error = new Error(`${componentName} should have exactly one "ContextualMenuDropdown" child.`); 81 | } 82 | 83 | return error; 84 | }, 85 | className: PropTypes.string, 86 | id: PropTypes.string.isRequired, 87 | position: PropTypes.oneOf(['left', 'center']), 88 | }; 89 | 90 | ContextualMenu.displayName = 'ContextualMenu'; 91 | 92 | export default ContextualMenu; 93 | -------------------------------------------------------------------------------- /src/lib/components/ContextualMenu/ContextualMenu.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { select } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import ContextualMenu from './ContextualMenu'; 7 | import ContextualMenuDropdown from './ContextualMenuDropdown'; 8 | import ContextualMenuGroup from './ContextualMenuGroup'; 9 | import Button from '../Button/Button'; 10 | import Link from '../Link/Link'; 11 | 12 | storiesOf('ContextualMenu', module) 13 | .add('Default', 14 | withInfo('A ContextualMenu component can be wrapped around any component (button, link, navigation item etc.) to give it a secondary dropdown menu on click. One of the children must be a ContextualMenuDropdown that contains the links (or other elements) you would like to display. ContextualMenus must be given an "id" prop so the dropdown knows where in the DOM to appear, and it also accepts a "position" prop to determine which side of the element appears (default "right").')(() => ( 15 |
    16 | 17 | 18 | 19 | Link 1 20 | Link 2 21 | Link 3 22 | 23 | 24 |
    ), 25 | ), 26 | ) 27 | 28 | .add('Groups', 29 | withInfo('Dropdown links can be grouped when placed inside a ContextualMenuGroup component, which separates groups by a dividing line.')(() => ( 30 |
    31 | 32 | 33 | 34 | 35 | Link 1 36 | Link 2 37 | 38 | 39 | Link 3 40 | Link 4 41 | 42 | 43 | 44 |
    ), 45 | ), 46 | ) 47 | 48 | .add('Link', 49 | withInfo('ContextualMenus can be applied to any element, including links within paragraph text.')(() => ( 50 |

    51 | Lorem ipsum dolor sit amet, consectetur adipisicing elit. Modi natus atque eligendi 52 | deleniti hic, dolores veritatis reiciendis officiis illo, facere facilis accusamus 53 | similique nulla nesciunt.  54 | 55 | null}>Nemo 56 | 57 | 58 | Learn more 59 | Learn more 60 | 61 | 62 | Home 63 | 64 | 65 | 66 |  reprehenderit officia assumenda error enim. Recusandae 67 | reiciendis ipsum, mollitia illo iusto excepturi alias dolore fugit eligendi nostrum, unde 68 | architecto consequuntur similique quo! Maxime iusto facere, commodi iste fuga officiis. 69 |

    ), 70 | ), 71 | ); 72 | -------------------------------------------------------------------------------- /src/lib/components/ContextualMenu/ContextualMenu.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import { shallow } from 'enzyme'; 4 | import ContextualMenu from './ContextualMenu'; 5 | import ContextualMenuDropdown from './ContextualMenuDropdown'; 6 | import ContextualMenuGroup from './ContextualMenuGroup'; 7 | 8 | describe('', () => { 9 | it('should render a simple ContextualMenu correctly', () => { 10 | const menu = ReactTestRenderer.create( 11 | 12 |
    Menu
    13 | 14 | Link 1 15 | Link 2 16 | Link 3 17 | 18 |
    ); 19 | const json = menu.toJSON(); 20 | expect(json).toMatchSnapshot(); 21 | }); 22 | 23 | it('should render with position prop correctly', () => { 24 | const menu = ReactTestRenderer.create( 25 |
    26 | 27 |
    Menu
    28 | 29 | Link 1 30 | Link 2 31 | Link 3 32 | 33 |
    34 | 35 |
    Menu
    36 | 37 | Link 1 38 | Link 2 39 | Link 3 40 | 41 |
    42 |
    ); 43 | const json = menu.toJSON(); 44 | expect(json).toMatchSnapshot(); 45 | }); 46 | 47 | it('should render ContextualMenuGroups correctly', () => { 48 | const menu = ReactTestRenderer.create( 49 | 50 |
    Menu
    51 | 52 | 53 | Link 1 54 | Link 2 55 | 56 | 57 | Link 3 58 | Link 4 59 | 60 | 61 |
    ); 62 | const json = menu.toJSON(); 63 | expect(json).toMatchSnapshot(); 64 | }); 65 | 66 | it('should open the ContextualMenu when clicked', () => { 67 | const menu = shallow( 68 | 69 | 70 | 71 | Link 1 72 | Link 2 73 | Link 3 74 | 75 | ); 76 | 77 | expect(menu.find('[aria-expanded=true]')).toHaveLength(0); 78 | expect(menu.find('[aria-hidden=true]')).toHaveLength(1); 79 | 80 | menu.find('button').hostNodes().simulate('click'); 81 | expect(menu.find('[aria-expanded=true]')).toHaveLength(1); 82 | expect(menu.find('[aria-hidden=true]')).toHaveLength(0); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/lib/components/ContextualMenu/ContextualMenuDropdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | 5 | import ContextualMenuGroup from './ContextualMenuGroup'; 6 | 7 | const ContextualMenuDropdown = (props) => { 8 | const { children, className, ...otherProps } = props; 9 | 10 | const classNames = getClassName({ 11 | [className]: className, 12 | 'p-contextual-menu__dropdown': true, 13 | }) || undefined; 14 | 15 | const dropdownItems = React.Children.map(children, (child) => { 16 | if (child.type !== ContextualMenuGroup) { 17 | const childClasses = getClassName({ 18 | [child.props.className]: child.props.className, 19 | 'p-contextual-menu__link': true, 20 | }); 21 | return React.cloneElement(child, { 22 | className: childClasses, 23 | }); 24 | } 25 | return child; 26 | }); 27 | 28 | return ( 29 | 30 | { dropdownItems } 31 | 32 | ); 33 | }; 34 | 35 | ContextualMenuDropdown.defaultProps = { 36 | children: null, 37 | className: null, 38 | }; 39 | 40 | ContextualMenuDropdown.propTypes = { 41 | children: PropTypes.node, 42 | className: PropTypes.string, 43 | }; 44 | 45 | ContextualMenuDropdown.displayName = 'ContextualMenuDropdown'; 46 | 47 | export default ContextualMenuDropdown; 48 | -------------------------------------------------------------------------------- /src/lib/components/ContextualMenu/ContextualMenuGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | 5 | const ContextualMenuGroup = (props) => { 6 | const { children, className, ...otherProps } = props; 7 | 8 | const classNames = getClassName({ 9 | [className]: className, 10 | 'p-contextual-menu__group': true, 11 | }) || undefined; 12 | 13 | const items = React.Children.map(children, (child) => { 14 | const childClasses = getClassName({ 15 | [child.props.className]: child.props.className, 16 | 'p-contextual-menu__link': true, 17 | }); 18 | return React.cloneElement(child, { 19 | className: childClasses, 20 | }); 21 | }); 22 | 23 | return ( 24 | 25 | {items} 26 | 27 | ); 28 | }; 29 | 30 | ContextualMenuGroup.defaultProps = { 31 | children: null, 32 | className: null, 33 | }; 34 | 35 | ContextualMenuGroup.propTypes = { 36 | children: PropTypes.node, 37 | className: PropTypes.string, 38 | }; 39 | 40 | ContextualMenuGroup.displayName = 'ContextualMenuGroup'; 41 | 42 | export default ContextualMenuGroup; 43 | -------------------------------------------------------------------------------- /src/lib/components/DividerList/DividerList.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const DividerList = (props) => { 5 | const dividerItems = React.Children.map(props.children, 6 | child => React.cloneElement(child, { 7 | items: React.Children.count(props.children), 8 | }), 9 | ); 10 | 11 | return ( 12 |
    13 | {dividerItems} 14 |
    15 | ); 16 | }; 17 | 18 | DividerList.propTypes = { 19 | children: PropTypes.node.isRequired, 20 | }; 21 | 22 | DividerList.displayName = 'DividerList'; 23 | 24 | export default DividerList; 25 | -------------------------------------------------------------------------------- /src/lib/components/DividerList/DividerList.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import DividerList from './DividerList'; 4 | import DividerListItem from './DividerListItem'; 5 | 6 | describe('DividerList component', () => { 7 | it('should render two children correctly', () => { 8 | const dividerList = ReactTestRenderer.create( 9 | 10 | 11 | One 12 | 13 | 14 | Two 15 | 16 | , 17 | ); 18 | const json = dividerList.toJSON(); 19 | expect(json).toMatchSnapshot(); 20 | }); 21 | 22 | it('should render three children correctly', () => { 23 | const dividerList = ReactTestRenderer.create( 24 | 25 | 26 | One 27 | 28 | 29 | Two 30 | 31 | 32 | Three 33 | 34 | , 35 | ); 36 | const json = dividerList.toJSON(); 37 | expect(json).toMatchSnapshot(); 38 | }); 39 | 40 | it('should render four children correctly', () => { 41 | const dividerList = ReactTestRenderer.create( 42 | 43 | 44 | One 45 | 46 | 47 | Two 48 | 49 | 50 | Three 51 | 52 | 53 | Four 54 | 55 | , 56 | ); 57 | const json = dividerList.toJSON(); 58 | expect(json).toMatchSnapshot(); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /src/lib/components/DividerList/DividerListItem.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const DividerListItem = props => ( 5 |
    6 | {props.children} 7 |
    8 | ); 9 | 10 | DividerListItem.defaultProps = { 11 | items: 3, 12 | }; 13 | 14 | DividerListItem.propTypes = { 15 | children: PropTypes.node.isRequired, 16 | items: PropTypes.oneOf([2, 3, 4, 6]), 17 | }; 18 | 19 | DividerListItem.displayName = 'DividerListItem'; 20 | 21 | export default DividerListItem; 22 | -------------------------------------------------------------------------------- /src/lib/components/DividerList/__snapshots__/DividerList.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`DividerList component should render four children correctly 1`] = ` 4 |
    7 |
    10 | One 11 |
    12 |
    15 | Two 16 |
    17 |
    20 | Three 21 |
    22 |
    25 | Four 26 |
    27 |
    28 | `; 29 | 30 | exports[`DividerList component should render three children correctly 1`] = ` 31 |
    34 |
    37 | One 38 |
    39 |
    42 | Two 43 |
    44 |
    47 | Three 48 |
    49 |
    50 | `; 51 | 52 | exports[`DividerList component should render two children correctly 1`] = ` 53 |
    56 |
    59 | One 60 |
    61 |
    64 | Two 65 |
    66 |
    67 | `; 68 | -------------------------------------------------------------------------------- /src/lib/components/Footer/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Footer = props => ( 5 |
    6 | { props.children } 7 |
    8 | ); 9 | 10 | Footer.propTypes = { 11 | children: PropTypes.node.isRequired, 12 | }; 13 | 14 | Footer.displayName = 'Footer'; 15 | 16 | export default Footer; 17 | -------------------------------------------------------------------------------- /src/lib/components/Footer/Footer.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { withInfo } from '@storybook/addon-info'; 4 | 5 | import Footer from './Footer'; 6 | import FooterNav from './FooterNav'; 7 | import FooterNavContainer from './FooterNavContainer'; 8 | 9 | storiesOf('Footer', module) 10 | .add('Default', 11 | withInfo('You can use this simple footer pattern to display simple HTML elements: lists, headings, columns, icons, buttons, etc. Centering is optional.')(() => ( 12 |
    13 |

    14 | Footer by Me. The source code is licensed MIT. 15 | The website content is licensed CC BY NC SA 4.0. 16 |

    17 |
    ), 18 | ), 19 | ) 20 | .add('With Links', 21 | withInfo('You can use this simple footer pattern to display copyright notices and a selection of links, using the FooterNavContainer and FooterNav sub-components.')(() => ( 22 |
    23 |

    © 2017 Company name and logo are registered trademarks of Company Ltd.

    24 | 25 | 26 | 27 | 28 | 29 | 30 |
    ), 31 | ), 32 | ); 33 | -------------------------------------------------------------------------------- /src/lib/components/Footer/Footer.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import Footer from './Footer'; 4 | import FooterNavContainer from './FooterNavContainer'; 5 | import FooterNav from './FooterNav'; 6 | 7 | describe('Footer component', () => { 8 | it('should render basic HTML elements correctly', () => { 9 | const footer = ReactTestRenderer.create( 10 |
    11 |

    Heading 1

    12 |

    © 2017 Company name and logo are registered trademarks of Company Ltd.

    13 |
      14 |
    • List Item 1
    • 15 |
    • List Item 2
    • 16 |
    17 |
    , 18 | ); 19 | const json = footer.toJSON(); 20 | expect(json).toMatchSnapshot(); 21 | }); 22 | 23 | it('should render FooterNavContainer and FooterNav components correctly', () => { 24 | const footer = ReactTestRenderer.create( 25 |
    26 |

    © 2017 Company name and logo are registered trademarks of Company Ltd.

    27 | 28 | 29 | 30 | 31 | 32 | 33 |
    , 34 | ); 35 | const json = footer.toJSON(); 36 | expect(json).toMatchSnapshot(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /src/lib/components/Footer/FooterNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const FooterNav = props => ( 5 |
  • 6 | { props.title } 7 |
  • 8 | ); 9 | 10 | FooterNav.propTypes = { 11 | title: PropTypes.string.isRequired, 12 | link: PropTypes.string.isRequired, 13 | }; 14 | 15 | FooterNav.displayName = 'FooterNav'; 16 | 17 | export default FooterNav; 18 | -------------------------------------------------------------------------------- /src/lib/components/Footer/FooterNavContainer.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const FooterNavContainer = props => ( 5 | 10 | ); 11 | 12 | FooterNavContainer.propTypes = { 13 | children: PropTypes.node.isRequired, 14 | }; 15 | 16 | FooterNavContainer.displayName = 'FooterNavContainer'; 17 | 18 | export default FooterNavContainer; 19 | -------------------------------------------------------------------------------- /src/lib/components/Footer/__snapshots__/Footer.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Footer component should render FooterNavContainer and FooterNav components correctly 1`] = ` 4 | 59 | `; 60 | 61 | exports[`Footer component should render basic HTML elements correctly 1`] = ` 62 |
    65 |

    66 | Heading 1 67 |

    68 |

    69 | © 2017 Company name and logo are registered trademarks of Company Ltd. 70 |

    71 |
      72 |
    • 73 | List Item 1 74 |
    • 75 |
    • 76 | List Item 2 77 |
    • 78 |
    79 |
    80 | `; 81 | -------------------------------------------------------------------------------- /src/lib/components/Form/FieldSet.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | 5 | const FieldSet = (props) => { 6 | const { children, className, ...otherProps } = props; 7 | 8 | const classNames = getClassName({ 9 | [className]: className, 10 | }) || undefined; 11 | 12 | return ( 13 |
    14 | { children } 15 |
    16 | ); 17 | }; 18 | 19 | FieldSet.defaultProps = { 20 | className: undefined, 21 | }; 22 | 23 | FieldSet.propTypes = { 24 | children: PropTypes.node.isRequired, 25 | className: PropTypes.string, 26 | }; 27 | 28 | FieldSet.displayName = 'FieldSet'; 29 | 30 | export default FieldSet; 31 | -------------------------------------------------------------------------------- /src/lib/components/Form/Form.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | 5 | const Form = (props) => { 6 | const { 7 | children, className, inline, stacked, ...otherProps 8 | } = props; 9 | 10 | const classNames = getClassName({ 11 | [className]: className, 12 | 'p-form': true, 13 | 'p-form--inline': inline && !stacked, 14 | 'p-form--stacked': stacked && !inline, 15 | }) || undefined; 16 | 17 | return ( 18 |
    19 | {children} 20 |
    21 | ); 22 | }; 23 | 24 | Form.defaultProps = { 25 | children: null, 26 | className: undefined, 27 | inline: false, 28 | stacked: false, 29 | }; 30 | 31 | Form.propTypes = { 32 | children: PropTypes.node, 33 | className: PropTypes.string, 34 | inline: PropTypes.bool, 35 | stacked: PropTypes.bool, 36 | }; 37 | 38 | Form.displayName = 'Form'; 39 | 40 | export default Form; 41 | -------------------------------------------------------------------------------- /src/lib/components/Form/Form.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import { shallow } from 'enzyme'; 4 | import Form from './Form'; 5 | import FormControl from './FormControl'; 6 | import Input from '../Input/Input'; 7 | import Label from '../Input/Label'; 8 | 9 | describe('
    ', () => { 10 | it('should render with "form" tag', () => { 11 | const form = shallow(Form
    ); 12 | 13 | expect(form.type()).toBe('form'); 14 | }); 15 | 16 | it('should render children', () => { 17 | const form = shallow(
    Form
    ); 18 | 19 | expect(form.text()).toBe('Form'); 20 | }); 21 | 22 | it('should contain the inline form class if inline prop exists', () => { 23 | const inlineClass = 'p-form--inline'; 24 | const form = shallow(
    Form
    ); 25 | 26 | expect(form.hasClass(inlineClass)).toBe(true); 27 | }); 28 | 29 | it('should contain the stacked form class if inline prop exists', () => { 30 | const inlineClass = 'p-form--stacked'; 31 | const form = shallow(
    Form
    ); 32 | 33 | expect(form.hasClass(inlineClass)).toBe(true); 34 | }); 35 | 36 | it('should render additional classes', () => { 37 | const form = shallow(
    Form
    ); 38 | 39 | expect(form.hasClass('class')).toBe(true); 40 | }); 41 | 42 | it('should match snapshot of empty Form', () => { 43 | const form = ReactTestRenderer.create( 44 |
    ); 45 | const json = form.toJSON(); 46 | expect(json).toMatchSnapshot(); 47 | }); 48 | 49 | it('should match snapshot of Form with Label and Input', () => { 50 | const form = ReactTestRenderer.create( 51 | 52 | 53 | 54 |
    ); 55 | const json = form.toJSON(); 56 | expect(json).toMatchSnapshot(); 57 | }); 58 | 59 | it('should match snapshot of Form with FormControl', () => { 60 | const form = ReactTestRenderer.create( 61 |
    62 | 63 | 64 | 65 | 66 |
    ); 67 | const json = form.toJSON(); 68 | expect(json).toMatchSnapshot(); 69 | }); 70 | 71 | it('should match snapshot of Form with FormControl and help text', () => { 72 | const form = ReactTestRenderer.create( 73 |
    74 | 75 | 76 | 77 | 78 |
    ); 79 | const json = form.toJSON(); 80 | expect(json).toMatchSnapshot(); 81 | }); 82 | 83 | it('should match snapshot of Form with FormControl and validation', () => { 84 | const form = ReactTestRenderer.create( 85 |
    86 | 93 | 94 | 95 | 96 |
    ); 97 | const json = form.toJSON(); 98 | expect(json).toMatchSnapshot(); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /src/lib/components/Form/FormControl.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | import FormHelpText from './FormHelpText'; 5 | import Input from '../Input/Input'; 6 | 7 | const FormControl = (props) => { 8 | const { 9 | children, className, help, validation, 10 | } = props; 11 | 12 | const classNames = getClassName({ 13 | [className]: className, 14 | 'p-form__control': true, 15 | 'p-form-validation': validation.status, 16 | [`is-${validation.status}`]: validation && validation.status, 17 | }) || undefined; 18 | 19 | const formControlItems = React.Children.map(children, (child) => { 20 | if (validation.status && child.type === Input) { 21 | return React.cloneElement(child, { hasValidation: true }); 22 | } 23 | return child; 24 | }); 25 | 26 | return ( 27 |
    28 | { formControlItems } 29 | { validation.status && ( 30 |

    31 | { validation.tag && {validation.tag} } 32 | { validation.message } 33 |

    ) 34 | } 35 | { help && { help }} 36 |
    37 | ); 38 | }; 39 | 40 | FormControl.defaultProps = { 41 | children: null, 42 | className: undefined, 43 | help: null, 44 | validation: { 45 | tag: null, 46 | status: null, 47 | message: null, 48 | }, 49 | }; 50 | 51 | FormControl.propTypes = { 52 | children: PropTypes.node, 53 | className: PropTypes.string, 54 | help: PropTypes.node, 55 | validation: PropTypes.shape({ 56 | tag: PropTypes.string, 57 | status: PropTypes.oneOf([null, 'caution', 'success', 'error']), 58 | message: PropTypes.node, 59 | }), 60 | }; 61 | 62 | FormControl.displayName = 'FormControl'; 63 | 64 | export default FormControl; 65 | -------------------------------------------------------------------------------- /src/lib/components/Form/FormGroup.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | 5 | const FormGroup = (props) => { 6 | const { children, className } = props; 7 | 8 | const classNames = getClassName({ 9 | [className]: className, 10 | 'p-form__group': true, 11 | }) || undefined; 12 | 13 | return ( 14 |
    15 | { children } 16 |
    17 | ); 18 | }; 19 | 20 | FormGroup.defaultProps = { 21 | children: undefined, 22 | className: '', 23 | }; 24 | 25 | FormGroup.propTypes = { 26 | children: PropTypes.node, 27 | className: PropTypes.string, 28 | }; 29 | 30 | FormGroup.displayName = 'FormGroup'; 31 | 32 | export default FormGroup; 33 | -------------------------------------------------------------------------------- /src/lib/components/Form/FormHelpText.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | 5 | const FormHelpText = (props) => { 6 | const { children, className } = props; 7 | 8 | const classNames = getClassName({ 9 | [className]: className, 10 | 'p-form-help-text': true, 11 | }) || undefined; 12 | 13 | return ( 14 |

    { children }

    15 | ); 16 | }; 17 | 18 | FormHelpText.defaultProps = { 19 | children: null, 20 | className: undefined, 21 | }; 22 | 23 | FormHelpText.propTypes = { 24 | children: PropTypes.node, 25 | className: PropTypes.string, 26 | }; 27 | 28 | FormHelpText.displayName = 'FormHelpText'; 29 | 30 | export default FormHelpText; 31 | -------------------------------------------------------------------------------- /src/lib/components/Form/__snapshots__/Form.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`
    should match snapshot of Form with FormControl 1`] = ` 4 | 7 |
    10 | 16 | 21 |
    22 |
    23 | `; 24 | 25 | exports[`
    should match snapshot of Form with FormControl and help text 1`] = ` 26 | 29 |
    32 | 38 | 43 |

    46 | help text 47 |

    48 |
    49 |
    50 | `; 51 | 52 | exports[`
    should match snapshot of Form with FormControl and validation 1`] = ` 53 | 56 |
    59 | 65 | 70 |

    73 | 74 | Success: 75 | 76 | Validation message 77 |

    78 |
    79 |
    80 | `; 81 | 82 | exports[`
    should match snapshot of Form with Label and Input 1`] = ` 83 | 86 | 92 | 97 |
    98 | `; 99 | 100 | exports[`
    should match snapshot of empty Form 1`] = ` 101 | 104 | `; 105 | -------------------------------------------------------------------------------- /src/lib/components/HeadingIcon/HeadingIcon.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const HeadingIcon = props => ( 5 |
    6 |
    7 | {props.alt} 8 |

    {props.title}

    9 |
    10 | {props.children} 11 |
    12 | ); 13 | 14 | HeadingIcon.defaultProps = { 15 | alt: '', 16 | children: '', 17 | }; 18 | 19 | HeadingIcon.propTypes = { 20 | title: PropTypes.string.isRequired, 21 | src: PropTypes.string.isRequired, 22 | alt: PropTypes.string, 23 | children: PropTypes.node, 24 | }; 25 | 26 | HeadingIcon.displayName = 'HeadingIcon'; 27 | 28 | export default HeadingIcon; 29 | -------------------------------------------------------------------------------- /src/lib/components/HeadingIcon/HeadingIcon.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { text } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import HeadingIcon from './HeadingIcon'; 7 | 8 | storiesOf('Heading Icon', module) 9 | .add('Default', 10 | withInfo('The HeadingIcon component can be used to add an icon to a standard header.')(() => ( 11 | ), 16 | ), 17 | ) 18 | 19 | .add('With Children', 20 | withInfo('The HeadingIcon component can be used to add an icon to a standard header.')(() => ( 21 | 26 |

    27 | {text('Text', 'Sit amet, consectetur adipisicing elit. Enim excepturi, repudiandae blanditiis odio perferendis voluptatibus dolorum, dicta illum quae ipsa voluptatum, sunt quasi? Nulla reiciendis magnam nostrum aliquam, beatae doloribus.')} 28 |

    29 |
    ), 30 | ), 31 | ); 32 | -------------------------------------------------------------------------------- /src/lib/components/HeadingIcon/HeadingIcon.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import HeadingIcon from './HeadingIcon'; 4 | 5 | describe('HeadingIcon component', () => { 6 | it('should render an icon and title without children correctly', () => { 7 | const headingIcon = ReactTestRenderer.create( 8 | , 13 | ); 14 | const json = headingIcon.toJSON(); 15 | expect(json).toMatchSnapshot(); 16 | }); 17 | 18 | it('should render children correctly', () => { 19 | const headingIcon = ReactTestRenderer.create( 20 | 25 |

    26 | sit amet, consectetur adipisicing elit. Enim excepturi, repudiandae blanditiis odio 27 | perferendis voluptatibus dolorum, dicta illum quae ipsa voluptatum, sunt quasi? Nulla 28 | reiciendis magnam nostrum aliquam, beatae doloribus. 29 |

    30 |
    , 31 | ); 32 | const json = headingIcon.toJSON(); 33 | expect(json).toMatchSnapshot(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /src/lib/components/HeadingIcon/__snapshots__/HeadingIcon.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`HeadingIcon component should render an icon and title without children correctly 1`] = ` 4 |
    7 |
    10 | Lorem ipsum dolor 15 |

    18 | Lorem ipsum dolor 19 |

    20 |
    21 | 22 |
    23 | `; 24 | 25 | exports[`HeadingIcon component should render children correctly 1`] = ` 26 |
    29 |
    32 | Lorem ipsum dolor 37 |

    40 | Lorem ipsum dolor 41 |

    42 |
    43 |

    44 | sit amet, consectetur adipisicing elit. Enim excepturi, repudiandae blanditiis odio perferendis voluptatibus dolorum, dicta illum quae ipsa voluptatum, sunt quasi? Nulla reiciendis magnam nostrum aliquam, beatae doloribus. 45 |

    46 |
    47 | `; 48 | -------------------------------------------------------------------------------- /src/lib/components/Image/Image.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const Image = (props) => { 5 | let classArray = []; 6 | 7 | if (props.bordered) { 8 | classArray = [...classArray, 'p-image--bordered']; 9 | } 10 | if (props.shadowed) { 11 | classArray = [...classArray, 'p-image--shadowed']; 12 | } 13 | 14 | const classString = classArray.join(' '); 15 | 16 | return ( 17 | {props.alt} 23 | ); 24 | }; 25 | 26 | Image.defaultProps = { 27 | alt: '', 28 | bordered: false, 29 | shadowed: false, 30 | style: null, 31 | }; 32 | 33 | Image.propTypes = { 34 | src: PropTypes.string.isRequired, 35 | alt: PropTypes.string, 36 | bordered: PropTypes.bool, 37 | shadowed: PropTypes.bool, 38 | style: PropTypes.object, 39 | }; 40 | 41 | Image.displayName = 'Image'; 42 | 43 | export default Image; 44 | -------------------------------------------------------------------------------- /src/lib/components/Image/Image.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { text, boolean } from '@storybook/addon-knobs'; 4 | import { withInfo } from '@storybook/addon-info'; 5 | 6 | import Image from './Image'; 7 | 8 | storiesOf('Image', module) 9 | .add('Default', 10 | withInfo('Default Image component.')(() => ( 11 | {text('alt',), 17 | ), 18 | ) 19 | .add('Bordered', 20 | withInfo('Bordered Image component.')(() => ( 21 | {text('alt',), 27 | ), 28 | ) 29 | .add('Shadowed', 30 | withInfo('Image component with drop-shadow.')(() => ( 31 | {text('alt',), 37 | ), 38 | ); 39 | -------------------------------------------------------------------------------- /src/lib/components/Image/Image.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import Image from './Image'; 4 | 5 | describe('Image component', () => { 6 | it('should render a default Image correctly', () => { 7 | const image = ReactTestRenderer.create( 8 | ); 9 | const json = image.toJSON(); 10 | expect(json).toMatchSnapshot(); 11 | }); 12 | 13 | it('should render bordered prop correctly', () => { 14 | const image = ReactTestRenderer.create( 15 | ); 16 | const json = image.toJSON(); 17 | expect(json).toMatchSnapshot(); 18 | }); 19 | 20 | it('should render shadowed prop correctly', () => { 21 | const image = ReactTestRenderer.create( 22 | ); 23 | const json = image.toJSON(); 24 | expect(json).toMatchSnapshot(); 25 | }); 26 | 27 | it('should render both bordered and shadowed props correctly', () => { 28 | const image = ReactTestRenderer.create( 29 | ); 30 | const json = image.toJSON(); 31 | expect(json).toMatchSnapshot(); 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /src/lib/components/Image/__snapshots__/Image.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Image component should render a default Image correctly 1`] = ` 4 | 10 | `; 11 | 12 | exports[`Image component should render bordered prop correctly 1`] = ` 13 | 19 | `; 20 | 21 | exports[`Image component should render both bordered and shadowed props correctly 1`] = ` 22 | 28 | `; 29 | 30 | exports[`Image component should render shadowed prop correctly 1`] = ` 31 | 37 | `; 38 | -------------------------------------------------------------------------------- /src/lib/components/InlineImages/InlineImages.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | 4 | const InlineImages = (props) => { 5 | const images = React.Children.map(props.children, child => ( 6 |
  • 7 | {child} 8 |
  • ), 9 | ); 10 | 11 | return ( 12 |
      13 | {images} 14 |
    15 | ); 16 | }; 17 | 18 | InlineImages.propTypes = { 19 | children: PropTypes.oneOfType([ 20 | PropTypes.element, 21 | PropTypes.arrayOf(PropTypes.element), 22 | ]).isRequired, 23 | }; 24 | 25 | InlineImages.displayName = 'InlineImages'; 26 | 27 | export default InlineImages; 28 | -------------------------------------------------------------------------------- /src/lib/components/InlineImages/InlineImages.stories.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { storiesOf } from '@storybook/react'; 3 | import { withInfo } from '@storybook/addon-info'; 4 | 5 | import InlineImages from './InlineImages'; 6 | import Image from '../Image/Image'; 7 | 8 | storiesOf('Inline Images', module) 9 | .add('Default', 10 | withInfo("The InlineImages component can be used to showcase a group of related images, such as a group of customer or partner logos. Vanilla's Image components and tags are accepted children.")(() => ( 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ), 19 | ), 20 | ); 21 | -------------------------------------------------------------------------------- /src/lib/components/InlineImages/InlineImages.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import InlineImages from './InlineImages'; 4 | import Image from '../Image/Image'; 5 | 6 | describe('InlineImages component', () => { 7 | it('should render the InlineImages component correctly', () => { 8 | const inlineImages = ReactTestRenderer.create( 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | const json = inlineImages.toJSON(); 18 | expect(json).toMatchSnapshot(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /src/lib/components/InlineImages/__snapshots__/InlineImages.test.js.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`InlineImages component should render the InlineImages component correctly 1`] = ` 4 |
      7 |
    • 10 | 16 |
    • 17 |
    • 20 | 26 |
    • 27 |
    • 30 | 36 |
    • 37 |
    • 40 | 46 |
    • 47 |
    • 50 | 56 |
    • 57 |
    • 60 | 66 |
    • 67 |
    68 | `; 69 | -------------------------------------------------------------------------------- /src/lib/components/Input/Input.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import PropTypes from 'prop-types'; 3 | import getClassName from '../../utils/getClassName'; 4 | 5 | function inputTag(type) { 6 | if (type === 'select') { 7 | return 'select'; 8 | } else if (type === 'textarea') { 9 | return 'textarea'; 10 | } 11 | return 'input'; 12 | } 13 | 14 | const Input = (props) => { 15 | const { 16 | className, hasValidation, type, ...otherProps 17 | } = props; 18 | 19 | const classNames = getClassName({ 20 | [className]: className, 21 | 'p-form-validation__input': hasValidation, 22 | }) || undefined; 23 | 24 | const Tag = inputTag(type); 25 | 26 | return ( 27 | 32 | ); 33 | }; 34 | 35 | Input.defaultProps = { 36 | className: undefined, 37 | type: 'text', 38 | hasValidation: false, 39 | }; 40 | 41 | Input.propTypes = { 42 | className: PropTypes.string, 43 | id: PropTypes.string.isRequired, 44 | type: PropTypes.oneOf([ 45 | 'text', 'password', 'file', 'checkbox', 'radio', 'select', 'textarea', 'date', 46 | 'hidden', 'month', 'time', 'week', 'color', 'number', 'email', 'url', 'tel', 47 | ]), 48 | hasValidation: PropTypes.bool, 49 | }; 50 | 51 | Input.displayName = 'Input'; 52 | 53 | export default Input; 54 | -------------------------------------------------------------------------------- /src/lib/components/Input/Input.test.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactTestRenderer from 'react-test-renderer'; 3 | import { shallow } from 'enzyme'; 4 | import Input from './Input'; 5 | import Label from './Label'; 6 | 7 | describe('', () => { 8 | it('should match the default snapshot', () => { 9 | const input = ReactTestRenderer.create( 10 | ); 11 | const json = input.toJSON(); 12 | expect(json).toMatchSnapshot(); 13 | }); 14 | 15 | it('should render an input tag by default', () => { 16 | const input = shallow( 17 | , 18 | ); 19 | 20 | expect(input.type()).toBe('input'); 21 | }); 22 | 23 | it('should render a select tag if type is "select"', () => { 24 | const input = shallow( 25 | , 26 | ); 27 | 28 | expect(input.type()).toBe('select'); 29 | }); 30 | 31 | it('should render a textarea tag if type is "textarea"', () => { 32 | const input = shallow( 33 | , 34 | ); 35 | 36 | expect(input.type()).toBe('textarea'); 37 | }); 38 | 39 | it('should render an input tag if type is not a special case', () => { 40 | const input = shallow( 41 | , 42 | ); 43 | 44 | expect(input.type()).toBe('input'); 45 | }); 46 | 47 | it('should have appropriate validation class if hasValidation prop is true', () => { 48 | const validationClass = 'p-form-validation__input'; 49 | const input = shallow( 50 | , 51 | ); 52 | 53 | expect(input.hasClass(validationClass)).toBe(true); 54 | }); 55 | 56 | it('should accept custom classnames', () => { 57 | const input = shallow( 58 | , 59 | ); 60 | 61 | expect(input.hasClass('custom-class')).toBe(true); 62 | }); 63 | }); 64 | 65 | describe('