Separate Icons from Button component

This commit is contained in:
Cassie Tarakajian 2020-05-11 16:28:18 -04:00
parent 6d749615cc
commit f359dcec4a
12 changed files with 135 additions and 137 deletions

2
.gitignore vendored
View file

@ -17,3 +17,5 @@ cert_chain.crt
localhost.crt localhost.crt
localhost.key localhost.key
privkey.pem privkey.pem
storybook-static

View file

@ -1,3 +1,5 @@
const path = require('path');
module.exports = { module.exports = {
stories: ['../client/**/*.stories.(jsx|mdx)'], stories: ['../client/**/*.stories.(jsx|mdx)'],
addons: [ addons: [
@ -10,6 +12,18 @@ module.exports = {
webpackFinal: async config => { webpackFinal: async config => {
// do mutation to the 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; return config;
}, },
}; };

View file

@ -4,7 +4,6 @@ import styled from 'styled-components';
import { Link } from 'react-router'; import { Link } from 'react-router';
import { remSize, prop } from '../theme'; import { remSize, prop } from '../theme';
import Icon, { ValidIconNameType } from './Icon';
const kinds = { const kinds = {
block: 'block', block: 'block',
@ -151,14 +150,8 @@ const StyledIconButton = styled.button`
* A Button performs an primary action * A Button performs an primary action
*/ */
const Button = ({ 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 && <span>{children}</span>}{IconAfter}</>;
let StyledComponent = StyledButton; let StyledComponent = StyledButton;
if (kind === kinds.inline) { if (kind === kinds.inline) {
@ -168,29 +161,26 @@ const Button = ({
} }
if (href) { if (href) {
return <StyledComponent kind={kind} as="a" aria-label={label} href={href} {...props}>{content}</StyledComponent>; return <StyledComponent kind={kind} as="a" aria-label={ariaLabel} href={href} {...props}>{children}</StyledComponent>;
} }
if (to) { if (to) {
return <StyledComponent kind={kind} as={Link} aria-label={label} to={to} {...props}>{content}</StyledComponent>; return <StyledComponent kind={kind} as={Link} aria-label={ariaLabel} to={to} {...props}>{children}</StyledComponent>;
} }
return <StyledComponent kind={kind} aria-label={label} type={type} {...props}>{content}</StyledComponent>; return <StyledComponent kind={kind} aria-label={ariaLabel} type={type} {...props}>{children}</StyledComponent>;
}; };
Button.defaultProps = { Button.defaultProps = {
children: null, 'children': null,
disabled: false, 'disabled': false,
iconAfterName: null, 'kind': kinds.block,
iconBeforeName: null, 'href': null,
kind: kinds.block, 'aria-label': null,
href: null, 'to': null,
label: null, 'type': 'button',
to: null,
type: 'button',
}; };
Button.iconNames = Object.keys(Icon);
Button.kinds = kinds; Button.kinds = kinds;
Button.propTypes = { Button.propTypes = {
@ -198,39 +188,31 @@ Button.propTypes = {
* The visible part of the button, telling the user what * The visible part of the button, telling the user what
* the action is * the action is
*/ */
children: PropTypes.element, 'children': PropTypes.element,
/** /**
If the button can be activated or not If the button can be activated or not
*/ */
disabled: PropTypes.bool, 'disabled': PropTypes.bool,
/**
* Name of icon to place before child content
*/
iconAfterName: ValidIconNameType,
/**
* Name of icon to place after child content
*/
iconBeforeName: ValidIconNameType,
/** /**
* The kind of button - determines how it appears visually * 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 <a> to link to the URL * Specifying an href will use an <a> to link to the URL
*/ */
href: PropTypes.string, 'href': PropTypes.string,
/* /*
* An ARIA Label used for accessibility * An ARIA Label used for accessibility
*/ */
label: PropTypes.string, 'aria-label': PropTypes.string,
/** /**
* Specifying a to URL will use a react-router Link * 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 * 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; export default Button;

View file

@ -3,6 +3,9 @@ import { action } from '@storybook/addon-actions';
import { boolean, text } from '@storybook/addon-knobs'; import { boolean, text } from '@storybook/addon-knobs';
import Button from './Button'; import Button from './Button';
import Icons from './Icons';
const { Github, DropdownArrow, Plus } = Icons;
export default { export default {
title: 'Common/Button', title: 'Common/Button',
@ -36,17 +39,28 @@ export const ReactRouterLink = () => (
); );
export const ButtonWithIconBefore = () => ( export const ButtonWithIconBefore = () => (
<Button iconBeforeName={Button.iconNames.Github}>Create</Button> <Button>
<Github aria-label="Github logo" />
<span>Create</span>
</Button>
); );
export const ButtonWithIconAfter = () => ( export const ButtonWithIconAfter = () => (
<Button iconAfterName={Button.iconNames.Github}>Create</Button> <Button>
<span>Create</span>
<Github aria-label="Github logo" />
</Button>
); );
export const InlineButtonWithIconAfter = () => ( export const InlineButtonWithIconAfter = () => (
<Button kind={Button.kinds.inline} iconAfterName={Button.iconNames.SortArrowDown}>File name</Button> <Button kind={Button.kinds.inline}>
<span>File name</span>
<DropdownArrow />
</Button>
); );
export const InlineIconOnlyButton = () => ( export const InlineIconOnlyButton = () => (
<Button kind={Button.kinds.inline} iconAfterName={Button.iconNames.Plus} label="Add to collection" /> <Button kind={Button.kinds.inline} aria-label="Add to collection">
<Plus />
</Button>
); );

View file

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

55
client/common/Icons.jsx Normal file
View file

@ -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 (<Icon
{...props}
aria-label={ariaLabel}
role="img"
focusable="false"
/>);
}
return (<Icon
{...props}
aria-hidden
focusable="false"
/>);
};
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;

View file

@ -1,17 +1,17 @@
import React from 'react'; import React from 'react';
import { select } from '@storybook/addon-knobs'; import { select } from '@storybook/addon-knobs';
import Icon from './Icon'; import Icons from './Icons';
export default { export default {
title: 'Common/Icon', title: 'Common/Icons',
component: Icon component: Icons
}; };
export const AllIcons = () => { 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 ( return (
<SelectedIcon /> <SelectedIcon />
); );

View file

@ -2,6 +2,7 @@ import PropTypes from 'prop-types';
import React from 'react'; import React from 'react';
import Button from '../../../common/Button'; import Button from '../../../common/Button';
import Icons from '../../../common/Icons';
import CopyableInput from '../../IDE/components/CopyableInput'; import CopyableInput from '../../IDE/components/CopyableInput';
import APIKeyList from './APIKeyList'; import APIKeyList from './APIKeyList';
@ -80,14 +81,12 @@ class APIKeyForm extends React.Component {
value={this.state.keyLabel} value={this.state.keyLabel}
/> />
<Button <Button
iconBeforeName={Button.iconNames.Plus}
disabled={this.state.keyLabel === ''} disabled={this.state.keyLabel === ''}
type="submit" type="submit"
label="Create new key" label="Create new key"
> >
{/* TODO make sure this aria label is right for the button */} <Icons.Plus />
{/* <PlusIcon className="api-key-form__create-icon" focusable="false" aria-hidden="true" /> */} <span>Create</span>
Create
</Button> </Button>
</form> </form>

View file

@ -8,6 +8,7 @@ import { bindActionCreators } from 'redux';
import classNames from 'classnames'; import classNames from 'classnames';
import Button from '../../../common/Button'; import Button from '../../../common/Button';
import Icons from '../../../common/Icons';
import * as ProjectActions from '../../IDE/actions/project'; import * as ProjectActions from '../../IDE/actions/project';
import * as ProjectsActions from '../../IDE/actions/projects'; import * as ProjectsActions from '../../IDE/actions/projects';
import * as CollectionsActions from '../../IDE/actions/collections'; import * as CollectionsActions from '../../IDE/actions/collections';
@ -22,7 +23,6 @@ import AddToCollectionSketchList from '../../IDE/components/AddToCollectionSketc
import CopyableInput from '../../IDE/components/CopyableInput'; import CopyableInput from '../../IDE/components/CopyableInput';
import { SketchSearchbar } from '../../IDE/components/Searchbar'; import { SketchSearchbar } from '../../IDE/components/Searchbar';
import DropdownArrowIcon from '../../../images/down-arrow.svg';
import ArrowUpIcon from '../../../images/sort-arrow-up.svg'; import ArrowUpIcon from '../../../images/sort-arrow-up.svg';
import ArrowDownIcon from '../../../images/sort-arrow-down.svg'; import ArrowDownIcon from '../../../images/sort-arrow-down.svg';
import RemoveIcon from '../../../images/close.svg'; import RemoveIcon from '../../../images/close.svg';
@ -53,17 +53,11 @@ const ShareURL = ({ value }) => {
return ( return (
<div className="collection-share" ref={node}> <div className="collection-share" ref={node}>
<Button <Button
iconAfterName={Button.iconNames.SortArrowDown}
onClick={() => setShowURL(!showURL)} onClick={() => setShowURL(!showURL)}
>Share
</Button>
{/* TODO make sure this has the right aria-label and SVG attributes */}
{/* <button>
aria-label="Show collection share URL"
> >
<span>Share</span> Share
<DropdownArrowIcon className="collection-share__arrow" focusable="false" aria-hidden="true" /> <Icons.DropdownArrow />
</button> */} </Button>
{ showURL && { showURL &&
<div className="collection__share-dropdown"> <div className="collection__share-dropdown">
<CopyableInput value={value} label="Link to Collection" /> <CopyableInput value={value} label="Link to Collection" />

View file

@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import GithubIcon from '../../../images/github.svg';
function GithubButton(props) {
return (
<a
className="github-button"
href="/auth/github"
>
<GithubIcon className="github-icon" role="img" aria-label="GitHub Logo" focusable="false" />
<span>{props.buttonText}</span>
</a>
);
}
GithubButton.propTypes = {
buttonText: PropTypes.string.isRequired
};
export default GithubButton;

View file

@ -1,22 +0,0 @@
import PropTypes from 'prop-types';
import React from 'react';
import GoogleIcon from '../../../images/google.svg';
function GoogleButton(props) {
return (
<a
className="google-button"
href="/auth/google/"
>
<GoogleIcon className="google-icon" role="img" aria-label="Google Logo" focusable="false" />
<span>{props.buttonText}</span>
</a>
);
}
GoogleButton.propTypes = {
buttonText: PropTypes.string.isRequired
};
export default GoogleButton;

View file

@ -4,21 +4,27 @@ import styled from 'styled-components';
import { remSize } from '../../../theme'; import { remSize } from '../../../theme';
import Icons from '../../../common/Icons';
import Button from '../../../common/Button'; import Button from '../../../common/Button';
const authUrls = { const authUrls = {
Github: '/auth-github', github: '/auth/github',
Google: '/auth/google/' google: '/auth/google'
}; };
const labels = { const labels = {
Github: 'Login with GitHub', github: 'Login with GitHub',
Google: 'Login with Google' google: 'Login with Google'
};
const icons = {
github: Icons.Github,
google: Icons.Google
}; };
const services = { const services = {
Github: 'github', github: 'github',
Google: 'google' google: 'google'
}; };
const StyledButton = styled(Button)` const StyledButton = styled(Button)`
@ -26,12 +32,13 @@ const StyledButton = styled(Button)`
`; `;
function SocialAuthButton({ service }) { function SocialAuthButton({ service }) {
const ServiceIcon = icons[service];
return ( return (
<StyledButton <StyledButton
iconBeforeName={Button.iconNames[service]}
href={authUrls[service]} href={authUrls[service]}
> >
{labels[service]} <ServiceIcon aria-label={`${service} logo`} />
<span>{labels[service]}</span>
</StyledButton> </StyledButton>
); );
} }
@ -39,7 +46,7 @@ function SocialAuthButton({ service }) {
SocialAuthButton.services = services; SocialAuthButton.services = services;
SocialAuthButton.propTypes = { SocialAuthButton.propTypes = {
service: PropTypes.oneOf(['Github', 'Google']).isRequired service: PropTypes.oneOf(['github', 'google']).isRequired
}; };
export default SocialAuthButton; export default SocialAuthButton;