Separate Icons from Button component
This commit is contained in:
parent
6d749615cc
commit
f359dcec4a
12 changed files with 135 additions and 137 deletions
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -17,3 +17,5 @@ cert_chain.crt
|
||||||
localhost.crt
|
localhost.crt
|
||||||
localhost.key
|
localhost.key
|
||||||
privkey.pem
|
privkey.pem
|
||||||
|
|
||||||
|
storybook-static
|
||||||
|
|
|
@ -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;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
|
|
|
@ -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
55
client/common/Icons.jsx
Normal 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;
|
|
@ -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 />
|
||||||
);
|
);
|
|
@ -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>
|
||||||
|
|
||||||
|
|
|
@ -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" />
|
||||||
|
|
|
@ -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;
|
|
|
@ -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;
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue