diff --git a/src-icons/svg/arrow-right.svg b/src-icons/svg/arrow-right.svg new file mode 100644 index 00000000..28e785df --- /dev/null +++ b/src-icons/svg/arrow-right.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/Button/README.stories.mdx b/src/Button/README.stories.mdx index 17a71a93..635ef19a 100644 --- a/src/Button/README.stories.mdx +++ b/src/Button/README.stories.mdx @@ -70,6 +70,9 @@ Basic button types and sizes: + @@ -95,6 +98,37 @@ Disabled: + + + + +Loading: + + + + + + + + + + @@ -137,6 +171,32 @@ Change the props in the table below to adjust the Button to your needs. +Test the button loading state. + + + + {() => { + const [isLoading, setIsLoading] = React.useState(false); + const buttonClick = () => { + setIsLoading(true); + setTimeout(() => { + setIsLoading(false); + }, "3000") + }; + return ( + + ); + }} + + + ## Props diff --git a/src/Button/index.js b/src/Button/index.js index f6c7c875..a4e31983 100644 --- a/src/Button/index.js +++ b/src/Button/index.js @@ -1,6 +1,6 @@ import React, {forwardRef} from 'react'; import PropTypes from 'prop-types'; -import styled, {css} from 'styled-components'; +import styled, {css, keyframes} from 'styled-components'; import {alpha, mix, getSolidBackgroundShade, isDark} from '../utils/colors'; import {pxToRem} from '../utils/units'; @@ -42,6 +42,7 @@ const shouldForwardProp = getPropFilter([ 'round', 'square', 'fullWidth', + 'isLoading', 'color', 'size', 'align', @@ -169,7 +170,7 @@ const Wrapper = styled(ButtonCore).withConfig({ } }} - cursor: not-allowed; + cursor: ${p => (p.isLoading ? 'wait' : 'not-allowed')}; ${p => (p.color === 'transparent' || p.color === 'shaded') && @@ -269,6 +270,26 @@ const ButtonText = styled.span.withConfig({ ${p => (p.textOverflow === 'ellipsis' ? ellipsis : overflowWrap)} `; +const spin = keyframes` + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +`; + +const SpinningIcon = styled(Icon)` + animation: ${spin} 0.6s infinite linear both; +`; + +const SpinnerDot = () => ( + +); + +const IconButton = ({isLoading, name}) => + isLoading ? : ; + const Button = forwardRef((props, ref) => { const { iconOnly, @@ -281,6 +302,9 @@ const Button = forwardRef((props, ref) => { fullWidth, title, textOverflow, + isLoading, + isDisabled, + loadingLabel, ...otherProps } = props; @@ -296,24 +320,40 @@ const Button = forwardRef((props, ref) => { align={align} fullWidth={fullWidth || textOverflow === 'ellipsis'} {...otherProps} + aria-live={isLoading !== undefined ? 'polite' : undefined} + isDisabled={isDisabled || isLoading} + isLoading={isLoading} > {icon && iconRight !== true && ( - + )} {children && (iconOnly ? ( - {children} - ) : ( - + {children} - + {isLoading && loadingLabel} + + ) : ( + <> + + {children} + + {isLoading && ( + {loadingLabel} + )} + {!icon && isLoading && } + ))} - {iconRight && ( - )} @@ -327,6 +367,7 @@ Button.displayName = 'Button'; Button.defaultProps = { color: 'default', textOverflow: 'wrap', + loadingLabel: 'Loading…', }; Button.propTypes = { @@ -345,6 +386,14 @@ Button.propTypes = { * can still be read out by screen readers. */ isDisabled: PropTypes.bool, + /** + * Renders the button in its loading state. + */ + isLoading: PropTypes.bool, + /** + * Renders the hidden loading label for the button in its loading state. + */ + loadingLabel: PropTypes.string, /** * Choose an icon to display next to the button's label */ diff --git a/src/Icon/iconMap.js b/src/Icon/iconMap.js index 078254c8..31f13d26 100644 --- a/src/Icon/iconMap.js +++ b/src/Icon/iconMap.js @@ -1,5 +1,7 @@ const iconMap = { arrow: require('../icons/Arrow').default, + 'arrow-left': require('../icons/Arrow').default, + 'arrow-right': require('../icons/ArrowRight').default, asset: require('../icons/AssetDefault').default, 'asset-activity': require('../icons/AssetActivity').default, 'asset-audio': require('../icons/AssetAudio').default, diff --git a/src/docs/3-icons.stories.mdx b/src/docs/3-icons.stories.mdx index c4d7bb98..ce634651 100644 --- a/src/docs/3-icons.stories.mdx +++ b/src/docs/3-icons.stories.mdx @@ -13,6 +13,7 @@ import Icon from '../Icon'; + diff --git a/src/icons/ArrowRight.tsx b/src/icons/ArrowRight.tsx new file mode 100644 index 00000000..bd73b4e7 --- /dev/null +++ b/src/icons/ArrowRight.tsx @@ -0,0 +1,39 @@ +import React, {forwardRef} from 'react'; + +import Svg from './BaseSvg'; +import { SVGRProps } from './util'; + +const ArrowRightIcon = forwardRef((props, ref) => { + const {size, color, ...otherProps} = props; + + // Unless the icon has an explicit ARIA label, we'll hide it visually + const ariaHidden = + !(otherProps['aria-label'] || otherProps['aria-labelledby']) || + undefined; + + return ( + + + + ); +}); + +ArrowRightIcon.displayName = 'ArrowRightIcon'; + +ArrowRightIcon.defaultProps = { + size: 18, + color: 'currentcolor', +}; + +export default ArrowRightIcon; diff --git a/src/icons/index.ts b/src/icons/index.ts index 8b4fb791..14125e39 100644 --- a/src/icons/index.ts +++ b/src/icons/index.ts @@ -1,83 +1,84 @@ -export {default as Arrow} from './Arrow'; -export {default as Asset} from './AssetDefault'; -export {default as AssetActivity} from './AssetActivity'; -export {default as AssetAudio} from './AssetAudio'; -export {default as AssetDefault} from './AssetDefault'; -export {default as AssetDiagram} from './AssetDiagram'; -export {default as AssetFluidbook} from './AssetFluidbook'; -export {default as AssetFolder} from './AssetFolder'; -export {default as AssetImage} from './AssetImage'; -export {default as AssetInfographic} from './AssetInfographic'; -export {default as AssetLink} from './AssetLink'; -export {default as AssetPdf} from './AssetPdf'; -export {default as AssetPodcast} from './AssetPodcast'; -export {default as AssetPresentation} from './AssetPresentation'; -export {default as AssetQuiz} from './AssetQuiz'; -export {default as AssetSpreadsheet} from './AssetSpreadsheet'; -export {default as AssetText} from './AssetText'; -export {default as AssetVideo} from './AssetVideo'; -export {default as AssetWebinar} from './AssetWebinar'; -export {default as Box} from './Box'; -export {default as Calendar} from './Calendar'; -export {default as Captions} from './Captions'; -export {default as Chat} from './Chat'; -export {default as Chevron} from './Chevron'; -export {default as Cycle} from './Cycle'; -export {default as Dislike} from './Dislike'; -export {default as Disk} from './Disk'; -export {default as Dot} from './Dot'; -export {default as Download} from './Download'; -export {default as Dropbox} from './Dropbox'; -export {default as Edit} from './Edit'; -export {default as Ellipsis} from './Ellipsis'; -export {default as External} from './External'; -export {default as EyeClosed} from './EyeClosed'; -export {default as EyeOpen} from './EyeOpen'; -export {default as Favourite} from './Favourite'; -export {default as Flag} from './Flag'; -export {default as Forbidden} from './Forbidden'; -export {default as Fullscreen} from './Fullscreen'; -export {default as FullscreenExit} from './FullscreenExit'; -export {default as Globe} from './Globe'; -export {default as Hierarchy} from './Hierarchy'; -export {default as Home} from './Home'; -export {default as Info} from './Info'; -export {default as Like} from './Like'; -export {default as Link} from './Link'; -export {default as Logout} from './Logout'; -export {default as Megaphone} from './Megaphone'; -export {default as Menu} from './Menu'; -export {default as Minus} from './Minus'; -export {default as NavDown} from './NavDown'; -export {default as NavLeft} from './NavLeft'; -export {default as NavRight} from './NavRight'; -export {default as NavUp} from './NavUp'; -export {default as Offline} from './Offline'; -export {default as Ok} from './Ok'; -export {default as Pause} from './Pause'; -export {default as Pin} from './Pin'; -export {default as Play} from './Play'; -export {default as Playlist} from './Playlist'; -export {default as PlaylistLink} from './PlaylistLink'; -export {default as Plus} from './Plus'; -export {default as Section} from './Section'; -export {default as Search} from './Search'; -export {default as Send} from './Send'; -export {default as Settings} from './Settings'; -export {default as Share} from './Share'; -export {default as SidebarClose} from './SidebarClose'; -export {default as SidebarOpen} from './SidebarOpen'; -export {default as Spinnerdot} from './Spinnerdot'; -export {default as Star} from './Star'; -export {default as Tag} from './Tag'; -export {default as Team} from './Team'; -export {default as Time} from './Time'; -export {default as Tools} from './Tools'; -export {default as Topic} from './Topic'; -export {default as Trash} from './Trash'; -export {default as Trending} from './Trending'; -export {default as Undo} from './Undo'; -export {default as Unpin} from './Unpin'; -export {default as Upload} from './Upload'; -export {default as User} from './User'; -export {default as X} from './X'; +export { default as Arrow } from './Arrow'; +export { default as ArrowRight } from './ArrowRight'; +export { default as Asset } from './AssetDefault'; +export { default as AssetActivity } from './AssetActivity'; +export { default as AssetAudio } from './AssetAudio'; +export { default as AssetDefault } from './AssetDefault'; +export { default as AssetDiagram } from './AssetDiagram'; +export { default as AssetFluidbook } from './AssetFluidbook'; +export { default as AssetFolder } from './AssetFolder'; +export { default as AssetImage } from './AssetImage'; +export { default as AssetInfographic } from './AssetInfographic'; +export { default as AssetLink } from './AssetLink'; +export { default as AssetPdf } from './AssetPdf'; +export { default as AssetPodcast } from './AssetPodcast'; +export { default as AssetPresentation } from './AssetPresentation'; +export { default as AssetQuiz } from './AssetQuiz'; +export { default as AssetSpreadsheet } from './AssetSpreadsheet'; +export { default as AssetText } from './AssetText'; +export { default as AssetVideo } from './AssetVideo'; +export { default as AssetWebinar } from './AssetWebinar'; +export { default as Box } from './Box'; +export { default as Calendar } from './Calendar'; +export { default as Captions } from './Captions'; +export { default as Chat } from './Chat'; +export { default as Chevron } from './Chevron'; +export { default as Cycle } from './Cycle'; +export { default as Dislike } from './Dislike'; +export { default as Disk } from './Disk'; +export { default as Dot } from './Dot'; +export { default as Download } from './Download'; +export { default as Dropbox } from './Dropbox'; +export { default as Edit } from './Edit'; +export { default as Ellipsis } from './Ellipsis'; +export { default as External } from './External'; +export { default as EyeClosed } from './EyeClosed'; +export { default as EyeOpen } from './EyeOpen'; +export { default as Favourite } from './Favourite'; +export { default as Flag } from './Flag'; +export { default as Forbidden } from './Forbidden'; +export { default as Fullscreen } from './Fullscreen'; +export { default as FullscreenExit } from './FullscreenExit'; +export { default as Globe } from './Globe'; +export { default as Hierarchy } from './Hierarchy'; +export { default as Home } from './Home'; +export { default as Info } from './Info'; +export { default as Like } from './Like'; +export { default as Link } from './Link'; +export { default as Logout } from './Logout'; +export { default as Megaphone } from './Megaphone'; +export { default as Menu } from './Menu'; +export { default as Minus } from './Minus'; +export { default as NavDown } from './NavDown'; +export { default as NavLeft } from './NavLeft'; +export { default as NavRight } from './NavRight'; +export { default as NavUp } from './NavUp'; +export { default as Offline } from './Offline'; +export { default as Ok } from './Ok'; +export { default as Pause } from './Pause'; +export { default as Pin } from './Pin'; +export { default as Play } from './Play'; +export { default as Playlist } from './Playlist'; +export { default as PlaylistLink } from './PlaylistLink'; +export { default as Plus } from './Plus'; +export { default as Section } from './Section'; +export { default as Search } from './Search'; +export { default as Send } from './Send'; +export { default as Settings } from './Settings'; +export { default as Share } from './Share'; +export { default as SidebarClose } from './SidebarClose'; +export { default as SidebarOpen } from './SidebarOpen'; +export { default as Spinnerdot } from './Spinnerdot'; +export { default as Star } from './Star'; +export { default as Tag } from './Tag'; +export { default as Team } from './Team'; +export { default as Time } from './Time'; +export { default as Tools } from './Tools'; +export { default as Topic } from './Topic'; +export { default as Trash } from './Trash'; +export { default as Trending } from './Trending'; +export { default as Undo } from './Undo'; +export { default as Unpin } from './Unpin'; +export { default as Upload } from './Upload'; +export { default as User } from './User'; +export { default as X } from './X';