From f359dcec4a68fc842bc2c5f1e8b9f6f8cb80189e Mon Sep 17 00:00:00 2001 From: Cassie Tarakajian Date: Mon, 11 May 2020 16:28:18 -0400 Subject: [PATCH] Separate Icons from Button component --- .gitignore | 2 + .storybook/main.js | 14 +++++ client/common/Button.jsx | 54 ++++++------------ client/common/Button.stories.jsx | 22 ++++++-- client/common/Icon.jsx | 25 --------- client/common/Icons.jsx | 55 +++++++++++++++++++ .../{Icon.stories.jsx => Icons.stories.jsx} | 10 ++-- client/modules/User/components/APIKeyForm.jsx | 7 +-- client/modules/User/components/Collection.jsx | 14 ++--- .../modules/User/components/GithubButton.jsx | 22 -------- .../modules/User/components/GoogleButton.jsx | 22 -------- .../User/components/SocialAuthButton.jsx | 25 ++++++--- 12 files changed, 135 insertions(+), 137 deletions(-) delete mode 100644 client/common/Icon.jsx create mode 100644 client/common/Icons.jsx rename client/common/{Icon.stories.jsx => Icons.stories.jsx} (50%) delete mode 100644 client/modules/User/components/GithubButton.jsx delete mode 100644 client/modules/User/components/GoogleButton.jsx diff --git a/.gitignore b/.gitignore index 91400122..e36b805f 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ cert_chain.crt localhost.crt localhost.key privkey.pem + +storybook-static diff --git a/.storybook/main.js b/.storybook/main.js index 75bbfec7..b7d42a9a 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -1,3 +1,5 @@ +const path = require('path'); + module.exports = { stories: ['../client/**/*.stories.(jsx|mdx)'], addons: [ @@ -10,6 +12,18 @@ module.exports = { webpackFinal: async config => { // do mutation to the config + const rules = config.module.rules; + + // modify storybook's file-loader rule to avoid conflicts with svgr + const fileLoaderRule = rules.find(rule => rule.test.test('.svg')); + fileLoaderRule.exclude = path.resolve(__dirname, '../client'); + + // use svgr for svg files + rules.push({ + test: /\.svg$/, + use: ["@svgr/webpack"], + }) + return config; }, }; diff --git a/client/common/Button.jsx b/client/common/Button.jsx index 58353693..f789b660 100644 --- a/client/common/Button.jsx +++ b/client/common/Button.jsx @@ -4,7 +4,6 @@ import styled from 'styled-components'; import { Link } from 'react-router'; import { remSize, prop } from '../theme'; -import Icon, { ValidIconNameType } from './Icon'; const kinds = { block: 'block', @@ -151,14 +150,8 @@ const StyledIconButton = styled.button` * A Button performs an primary action */ const Button = ({ - children, href, iconAfterName, iconBeforeName, kind, label, to, type, ...props + children, href, kind, 'aria-label': ariaLabel, to, type, ...props }) => { - const IconAfter = Icon[iconAfterName]; - const IconBefore = Icon[iconBeforeName]; - const hasChildren = React.Children.count(children) > 0; - - const content = <>{IconBefore}{hasChildren && {children}}{IconAfter}; - let StyledComponent = StyledButton; if (kind === kinds.inline) { @@ -168,29 +161,26 @@ const Button = ({ } if (href) { - return {content}; + return {children}; } if (to) { - return {content}; + return {children}; } - return {content}; + return {children}; }; Button.defaultProps = { - children: null, - disabled: false, - iconAfterName: null, - iconBeforeName: null, - kind: kinds.block, - href: null, - label: null, - to: null, - type: 'button', + 'children': null, + 'disabled': false, + 'kind': kinds.block, + 'href': null, + 'aria-label': null, + 'to': null, + 'type': 'button', }; -Button.iconNames = Object.keys(Icon); Button.kinds = kinds; Button.propTypes = { @@ -198,39 +188,31 @@ Button.propTypes = { * The visible part of the button, telling the user what * the action is */ - children: PropTypes.element, + 'children': PropTypes.element, /** If the button can be activated or not */ - disabled: PropTypes.bool, - /** - * Name of icon to place before child content - */ - iconAfterName: ValidIconNameType, - /** - * Name of icon to place after child content - */ - iconBeforeName: ValidIconNameType, + 'disabled': PropTypes.bool, /** * The kind of button - determines how it appears visually */ - kind: PropTypes.oneOf(Object.values(kinds)), + 'kind': PropTypes.oneOf(Object.values(kinds)), /** * Specifying an href will use an to link to the URL */ - href: PropTypes.string, + 'href': PropTypes.string, /* * An ARIA Label used for accessibility */ - label: PropTypes.string, + 'aria-label': PropTypes.string, /** * Specifying a to URL will use a react-router Link */ - to: PropTypes.string, + 'to': PropTypes.string, /** * If using a button, then type is defines the type of button */ - type: PropTypes.oneOf(['button', 'submit']), + 'type': PropTypes.oneOf(['button', 'submit']), }; export default Button; diff --git a/client/common/Button.stories.jsx b/client/common/Button.stories.jsx index 4ce30d4c..9b6ab3b3 100644 --- a/client/common/Button.stories.jsx +++ b/client/common/Button.stories.jsx @@ -3,6 +3,9 @@ import { action } from '@storybook/addon-actions'; import { boolean, text } from '@storybook/addon-knobs'; import Button from './Button'; +import Icons from './Icons'; + +const { Github, DropdownArrow, Plus } = Icons; export default { title: 'Common/Button', @@ -36,17 +39,28 @@ export const ReactRouterLink = () => ( ); export const ButtonWithIconBefore = () => ( - + ); export const ButtonWithIconAfter = () => ( - + ); export const InlineButtonWithIconAfter = () => ( - + ); export const InlineIconOnlyButton = () => ( - ); diff --git a/client/common/Icon.jsx b/client/common/Icon.jsx deleted file mode 100644 index f049ac07..00000000 --- a/client/common/Icon.jsx +++ /dev/null @@ -1,25 +0,0 @@ -import PropTypes from 'prop-types'; - -import SortArrowUp from '../images/sort-arrow-up.svg'; -import SortArrowDown from '../images/sort-arrow-down.svg'; -import Github from '../images/github.svg'; -import Google from '../images/google.svg'; -import Plus from '../images/plus-icon.svg'; -import Close from '../images/close.svg'; - -// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html -// could do something like, if there's an aria-label prop, give it role="img" focusable="false" -// otherwise, give it aria-hidden="true" focusable="false" - -const Icons = { - SortArrowUp, - SortArrowDown, - Github, - Google, - Plus, - Close -}; - -export default Icons; - -export const ValidIconNameType = PropTypes.oneOf(Object.keys(Icons)); diff --git a/client/common/Icons.jsx b/client/common/Icons.jsx new file mode 100644 index 00000000..580645a3 --- /dev/null +++ b/client/common/Icons.jsx @@ -0,0 +1,55 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import SortArrowUp from '../images/sort-arrow-up.svg'; +import SortArrowDown from '../images/sort-arrow-down.svg'; +import Github from '../images/github.svg'; +import Google from '../images/google.svg'; +import Plus from '../images/plus-icon.svg'; +import Close from '../images/close.svg'; +import DropdownArrow from '../images/down-filled-triangle.svg'; + +// HOC that adds the right web accessibility props +// https://www.scottohara.me/blog/2019/05/22/contextual-images-svgs-and-a11y.html + +// could also give these a default size, color, etc. based on the theme +// Need to add size to these - like small icon, medium icon, large icon. etc. +function withLabel(Icon) { + const render = (props) => { + const { 'aria-label': ariaLabel } = props; + if (ariaLabel) { + return (); + } + return (); + }; + + render.propTypes = { + 'aria-label': PropTypes.string + }; + + render.defaultProps = { + 'aria-label': null + }; + + return render; +} + +const Icons = { + SortArrowUp: withLabel(SortArrowUp), + SortArrowDown: withLabel(SortArrowDown), + Github: withLabel(Github), + Google: withLabel(Google), + Plus: withLabel(Plus), + Close: withLabel(Close), + DropdownArrow: withLabel(DropdownArrow) +}; + +export default Icons; diff --git a/client/common/Icon.stories.jsx b/client/common/Icons.stories.jsx similarity index 50% rename from client/common/Icon.stories.jsx rename to client/common/Icons.stories.jsx index 43be1580..d178ab99 100644 --- a/client/common/Icon.stories.jsx +++ b/client/common/Icons.stories.jsx @@ -1,17 +1,17 @@ import React from 'react'; import { select } from '@storybook/addon-knobs'; -import Icon from './Icon'; +import Icons from './Icons'; export default { - title: 'Common/Icon', - component: Icon + title: 'Common/Icons', + component: Icons }; export const AllIcons = () => { - const names = Object.keys(Icon); + const names = Object.keys(Icons); - const SelectedIcon = Icon[select('name', names, names[0])]; + const SelectedIcon = Icons[select('name', names, names[0])]; return ( ); diff --git a/client/modules/User/components/APIKeyForm.jsx b/client/modules/User/components/APIKeyForm.jsx index 405adc81..49bc95cd 100644 --- a/client/modules/User/components/APIKeyForm.jsx +++ b/client/modules/User/components/APIKeyForm.jsx @@ -2,6 +2,7 @@ import PropTypes from 'prop-types'; import React from 'react'; import Button from '../../../common/Button'; +import Icons from '../../../common/Icons'; import CopyableInput from '../../IDE/components/CopyableInput'; import APIKeyList from './APIKeyList'; @@ -80,14 +81,12 @@ class APIKeyForm extends React.Component { value={this.state.keyLabel} /> diff --git a/client/modules/User/components/Collection.jsx b/client/modules/User/components/Collection.jsx index 86be37a2..60e54e0b 100644 --- a/client/modules/User/components/Collection.jsx +++ b/client/modules/User/components/Collection.jsx @@ -8,6 +8,7 @@ import { bindActionCreators } from 'redux'; import classNames from 'classnames'; import Button from '../../../common/Button'; +import Icons from '../../../common/Icons'; import * as ProjectActions from '../../IDE/actions/project'; import * as ProjectsActions from '../../IDE/actions/projects'; import * as CollectionsActions from '../../IDE/actions/collections'; @@ -22,7 +23,6 @@ import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketc import CopyableInput from '../../IDE/components/CopyableInput'; import { SketchSearchbar } from '../../IDE/components/Searchbar'; -import DropdownArrowIcon from '../../../images/down-arrow.svg'; import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; import RemoveIcon from '../../../images/close.svg'; @@ -53,17 +53,11 @@ const ShareURL = ({ value }) => { return (
- {/* TODO make sure this has the right aria-label and SVG attributes */} - {/* */} + Share + + { showURL &&
diff --git a/client/modules/User/components/GithubButton.jsx b/client/modules/User/components/GithubButton.jsx deleted file mode 100644 index 32edfe8a..00000000 --- a/client/modules/User/components/GithubButton.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import GithubIcon from '../../../images/github.svg'; - -function GithubButton(props) { - return ( - - - {props.buttonText} - - ); -} - -GithubButton.propTypes = { - buttonText: PropTypes.string.isRequired -}; - -export default GithubButton; diff --git a/client/modules/User/components/GoogleButton.jsx b/client/modules/User/components/GoogleButton.jsx deleted file mode 100644 index c508aa9b..00000000 --- a/client/modules/User/components/GoogleButton.jsx +++ /dev/null @@ -1,22 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; - -import GoogleIcon from '../../../images/google.svg'; - -function GoogleButton(props) { - return ( - - - {props.buttonText} - - ); -} - -GoogleButton.propTypes = { - buttonText: PropTypes.string.isRequired -}; - -export default GoogleButton; diff --git a/client/modules/User/components/SocialAuthButton.jsx b/client/modules/User/components/SocialAuthButton.jsx index c70490ae..95f8e2b2 100644 --- a/client/modules/User/components/SocialAuthButton.jsx +++ b/client/modules/User/components/SocialAuthButton.jsx @@ -4,21 +4,27 @@ import styled from 'styled-components'; import { remSize } from '../../../theme'; +import Icons from '../../../common/Icons'; import Button from '../../../common/Button'; const authUrls = { - Github: '/auth-github', - Google: '/auth/google/' + github: '/auth/github', + google: '/auth/google' }; const labels = { - Github: 'Login with GitHub', - Google: 'Login with Google' + github: 'Login with GitHub', + google: 'Login with Google' +}; + +const icons = { + github: Icons.Github, + google: Icons.Google }; const services = { - Github: 'github', - Google: 'google' + github: 'github', + google: 'google' }; const StyledButton = styled(Button)` @@ -26,12 +32,13 @@ const StyledButton = styled(Button)` `; function SocialAuthButton({ service }) { + const ServiceIcon = icons[service]; return ( - {labels[service]} + + {labels[service]} ); } @@ -39,7 +46,7 @@ function SocialAuthButton({ service }) { SocialAuthButton.services = services; SocialAuthButton.propTypes = { - service: PropTypes.oneOf(['Github', 'Google']).isRequired + service: PropTypes.oneOf(['github', 'google']).isRequired }; export default SocialAuthButton;