diff --git a/src/blocks/button/Button.tsx b/src/blocks/button/Button.tsx index 623c096a99..ceef6f8d60 100644 --- a/src/blocks/button/Button.tsx +++ b/src/blocks/button/Button.tsx @@ -4,6 +4,7 @@ import styled, { FlattenSimpleInterpolation } from 'styled-components'; import type { TransformedHTMLAttributes } from '../Blocks.types'; import type { ButtonSize, ButtonVariant } from './Button.types'; import { getButtonSizeStyles, getButtonVariantStyles } from './Button.utils'; +import { Spinner } from '../spinner'; export type ButtonProps = { /* Child react nodes rendered by Box */ @@ -26,6 +27,8 @@ export type ButtonProps = { variant?: ButtonVariant; /* Button takes the full width if enabled */ block?: boolean; + /* Button loading state */ + loading?: boolean; } & TransformedHTMLAttributes; const StyledButton = styled.button` @@ -44,9 +47,10 @@ const StyledButton = styled.button` align-items: center; justify-content: center; } - /* Button variant CSS styles */ - ${({ variant }) => getButtonVariantStyles(variant || 'primary')} + ${({ variant, loading }) => getButtonVariantStyles(variant || 'primary', loading!)} + + ${({ loading }) => loading && 'opacity: var(--opacity-80);'} /* Button and font size CSS styles */ ${({ iconOnly, size }) => getButtonSizeStyles({ iconOnly: !!iconOnly, size: size || 'medium' })} @@ -61,6 +65,9 @@ const StyledButton = styled.button` ${(props) => props.css || ''} `; +const SpinnerContainer = styled.div` + padding: 5px; +`; const Button = forwardRef( ( { @@ -69,6 +76,7 @@ const Button = forwardRef( size = 'medium', leadingIcon, trailingIcon, + loading = false, iconOnly, circular = false, children, @@ -77,20 +85,26 @@ const Button = forwardRef( ref ) => ( + {loading && ( + + + + )} {leadingIcon && {leadingIcon}} {!iconOnly && children} {trailingIcon && {trailingIcon}} - {iconOnly && !children && {iconOnly}} + {iconOnly && !loading && !children && {iconOnly}} ) ); diff --git a/src/blocks/button/Button.utils.ts b/src/blocks/button/Button.utils.ts index 80d5ba91b6..8dd243fb1b 100644 --- a/src/blocks/button/Button.utils.ts +++ b/src/blocks/button/Button.utils.ts @@ -1,72 +1,89 @@ import { FlattenSimpleInterpolation, css } from 'styled-components'; import { ButtonSize, ButtonVariant } from './Button.types'; -export const getButtonVariantStyles = (variant: ButtonVariant) => { +export const getButtonVariantStyles = (variant: ButtonVariant, loading: boolean) => { switch (variant) { case 'primary': { return ` - background-color: var(--components-button-primary-background-default); + background-color: var(--${ + loading ? 'components-button-primary-background-loading' : 'components-button-primary-background-default' + }); color: var(--components-button-primary-text-default); - - &:hover { - background-color: var(--components-button-primary-background-hover) - } - - &:active { - background-color: var(--components-button-primary-background-pressed); - } + ${ + !loading && + ` + &:hover { + background-color: var(--components-button-primary-background-hover) + } + &:active { + background-color: var(--components-button-primary-background-pressed); + } + ` + }; &:focus-visible { background-color: var(--components-button-primary-background-focus); border: var(--border-sm) solid var(--components-button-primary-stroke-focus); outline: none; } + ${ + !loading && + `&:disabled { + background-color: var(--components-button-primary-background-disabled); + color: var(--components-button-primary-text-disabled); + }` + }; - &:disabled { - background-color: var(--components-button-primary-background-disabled); - color: var(--components-button-primary-text-disabled); - } `; } case 'secondary': { return ` background-color: var(--components-button-secondary-background-default); color: var(--components-button-secondary-text-default); - - &:hover { - background-color: var(--components-button-secondary-background-hover); - } - - &:active { - background-color: var(--components-button-secondary-background-pressed); - } + ${ + !loading && + ` + &:hover { + background-color: var(--components-button-secondary-background-hover); + } + + &:active { + background-color: var(--components-button-secondary-background-pressed); + }` + }; &:focus-visible { background-color: var(--components-button-secondary-background-focus); border: var(--border-sm) solid var(--components-button-secondary-stroke-focus); outline: none; } - - &:disabled { - background-color: var(--components-button-secondary-background-disabled); - color: var(--components-button-secondary-text-disabled) - } + ${ + !loading && + `&:disabled { + background-color: var(--components-button-secondary-background-disabled); + color: var(--components-button-secondary-text-disabled); + };` + }; + `; } case 'tertiary': { return ` background-color: var(--components-button-tertiary-background-default); color: var(--components-button-tertiary-text-default); - - &:hover { - color: var(--components-button-tertiary-text-default); - background-color: var(--components-button-tertiary-background-hover); - } - - &:active { - background-color: var(--components-button-tertiary-background-pressed); - color: var(--components-button-secondary-text-default); - } + ${ + !loading && + ` + &:hover { + color: var(--components-button-tertiary-text-default); + background-color: var(--components-button-tertiary-background-hover); + } + + &:active { + background-color: var(--components-button-tertiary-background-pressed); + color: var(--components-button-secondary-text-default); + }` + }; &:focus-visible { border: var(--border-sm) solid var(--components-button-tertiary-stroke-focus); @@ -74,61 +91,73 @@ export const getButtonVariantStyles = (variant: ButtonVariant) => { color: var(--components-button-tertiary-text-default); outline: none; } - - &:disabled { - background-color: var(--components-button-tertiary-background-disabled); - color: var(--components-button-tertiary-text-disabled); - } + ${ + !loading && + `&:disabled { + background-color: var(--components-button-tertiary-background-disabled); + color: var(--components-button-tertiary-text-disabled); + }` + }; `; } case 'danger': { return ` background-color: var(--components-button-danger-background-default); color: var(--components-button-danger-text-default); - - &:hover { - background-color: var(--components-button-danger-background-hover); - } - - &:active { - background-color: var(--components-button-danger-background-pressed); - } + ${ + !loading && + ` + &:hover { + background-color: var(--components-button-danger-background-hover); + } + + &:active { + background-color: var(--components-button-danger-background-pressed); + }` + }; &:focus-visible { background-color: var(--components-button-danger-background-focus); border: var(--border-sm) solid var(--components-button-danger-stroke-focus); outline: none; } - - &:disabled { - background-color: var(--components-button-danger-background-disabled); - color: var(--components-button-danger-text-disabled); - } + ${ + !loading && + `&:disabled { + background-color: var(--components-button-danger-background-disabled); + color: var(--components-button-danger-text-disabled); + }` + }; `; } case 'dangerSecondary': { return ` background-color: var(--components-button-danger-secondary-background-default); color: var(--components-button-danger-secondary-text-default); - - &:hover { - background-color: var(--components-button-danger-secondary-background-hover); - } - - &:active { - background-color: var(--components-button-danger-secondary-background-pressed); - } + ${ + !loading && + ` + &:hover { + background-color: var(--components-button-danger-secondary-background-hover); + } + + &:active { + background-color: var(--components-button-danger-secondary-background-pressed); + }` + }; &:focus-visible { background-color: var(--components-button-danger-secondary-background-focus); border: var(--border-sm) solid var(--components-button-danger-secondary-stroke-focus); outline: none; } - - &:disabled { - background-color: var(--components-button-danger-secondary-background-disabled); - color:var(--components-button-danger-secondary-text-disabled); - } + ${ + !loading && + `&:disabled { + background-color: var(--components-button-danger-secondary-background-disabled); + color:var(--components-button-danger-secondary-text-disabled); + }` + }; `; } case 'outline': { @@ -137,27 +166,33 @@ export const getButtonVariantStyles = (variant: ButtonVariant) => { border: var(--border-sm) solid var(--components-button-outline-stroke-default); color: var(--components-button-outline-text-default); outline: none; - - &:hover { - border: var(--border-sm) solid var(--components-button-outline-stroke-hover); - background-color: var(--components-button-outline-background-hover); - } - - &:active { - border: var(--border-sm) solid var(--components-button-outline-stroke-pressed); - background-color: var(--components-button-outline-background-pressed); - } + ${ + !loading && + ` + &:hover { + border: var(--border-sm) solid var(--components-button-outline-stroke-hover); + background-color: var(--components-button-outline-background-hover); + } + + &:active { + border: var(--border-sm) solid var(--components-button-outline-stroke-pressed); + background-color: var(--components-button-outline-background-pressed); + }` + }; &:focus-visible { border: var(--border-sm) solid var(--components-button-outline-stroke-focus); background-color: var(--components-button-outline-background-focus); } - - &:disabled { - border: none; - background-color: var(--components-button-tertiary-background-disabled); - color: var(--components-button-outline-text-disabled); - } + + ${ + !loading && + `&:disabled { + border: none; + background-color: var(--components-button-tertiary-background-disabled); + color: var(--components-button-outline-text-disabled); + }` + }; `; } } @@ -202,6 +237,10 @@ export const getButtonSizeStyles = ({ width: 16px; height: 16px; } + [role='spinner'] { + width: 10.66px; + height: 10.66px; + } .icon-text > span { height: 16px; @@ -246,6 +285,10 @@ export const getButtonSizeStyles = ({ width: 24px; height: 24px; } + [role='spinner'] { + width: 16px; + height: 16px; + } .icon-text > span { height: 16px; @@ -291,6 +334,10 @@ export const getButtonSizeStyles = ({ width: 24px; height: 24px; } + [role='spinner'] { + width: 16px; + height: 16px; + } .icon-text > span { height: 24px; @@ -335,7 +382,10 @@ export const getButtonSizeStyles = ({ width: 32px; height: 32px; } - + [role='spinner'] { + width: 21.333px; + height: 21.333px; + } .icon-text > span { height: 24px; width: 24px; diff --git a/src/blocks/icons/components/Ellipse.tsx b/src/blocks/icons/components/Ellipse.tsx new file mode 100644 index 0000000000..9d96c0fa92 --- /dev/null +++ b/src/blocks/icons/components/Ellipse.tsx @@ -0,0 +1,27 @@ +import { FC } from 'react'; +import { IconWrapper } from '../IconWrapper'; +import { IconProps } from '../Icons.types'; + +const Ellipse: FC = (allProps) => { + const { svgProps: props, ...restProps } = allProps; + return ( + + + + } + {...restProps} + /> + ); +}; + +export default Ellipse; diff --git a/src/blocks/icons/index.ts b/src/blocks/icons/index.ts index 92a4875b36..e32de6ab91 100644 --- a/src/blocks/icons/index.ts +++ b/src/blocks/icons/index.ts @@ -41,6 +41,8 @@ export { default as Dashboard } from './components/Dashboard'; export { default as EditProfile } from './components/EditProfile'; +export { default as Ellipse } from './components/Ellipse'; + export { default as Envelope } from './components/Envelope'; export { default as ErrorFilled } from './components/ErrorFilled'; diff --git a/src/blocks/spinner/Spinner.tsx b/src/blocks/spinner/Spinner.tsx index a3eb71cc7b..9d93ec370d 100644 --- a/src/blocks/spinner/Spinner.tsx +++ b/src/blocks/spinner/Spinner.tsx @@ -1,7 +1,8 @@ import React from 'react'; import styled, { FlattenSimpleInterpolation, keyframes } from 'styled-components'; import { SpinnerSize, SpinnerVariant } from './Spinner.types'; -import { getSpinnerSize, getSpinnerStrokeWidth } from './Spinner.utils'; +import { getSpinnerSize } from './Spinner.utils'; +import { Ellipse } from '../icons'; export type SpinnerProps = { /* Additional prop from styled components to apply custom css to Spinner */ @@ -13,54 +14,40 @@ export type SpinnerProps = { }; const spin = keyframes` - 0% { - transform: rotate(0deg); - } - 100% { - transform: rotate(360deg); - } + from { + transform:rotate(0deg); + } + to { + transform:rotate(360deg); + } `; -const Container = styled.div<{ css?: FlattenSimpleInterpolation; size: SpinnerSize; variant: SpinnerVariant }>` - position: relative; - animation: ${spin} 1s linear infinite; - border-radius: 50%; - ${({ size, variant }) => ` - border-width: var(--${getSpinnerStrokeWidth(size)}) ; - border-style: solid; - border-color: var(--components-spinner-icon-${variant}) - transparent transparent transparent; - width: ${getSpinnerSize(size)}; - height: ${getSpinnerSize(size)}; - :before,:after { - content: ''; - width: var(--${getSpinnerStrokeWidth(size)}); - height: var(--${getSpinnerStrokeWidth(size)}); - border-radius: 50%; - background: var(--components-spinner-icon-${variant}); - position: absolute; - } - :before { - top: 8.65%; - left: 10%; - } - :after { - top: 8.65%; - right: 10%; - } - `} +const Container = styled.div<{ css?: FlattenSimpleInterpolation; size: SpinnerSize }>` + display: flex; + align-items: center; + justify-content: center; + animation-name: ${spin}; + animation-duration: 1s; + animation-iteration-count: infinite; + animation-timing-function: linear; + ${({ size }) => ` + width: ${getSpinnerSize(size)}px; + height: ${getSpinnerSize(size)}px; + `} /* Custom CSS applied via styled component css prop */ ${(props) => props.css || ''}; `; -const Spinner: React.FC = ({ size = 'small', css, variant = 'default' }) => { +const Spinner: React.FC = ({ size = 'small', css }) => { return ( + role="spinner" + > + + ); }; diff --git a/src/blocks/spinner/Spinner.utils.ts b/src/blocks/spinner/Spinner.utils.ts index a66c69c5e5..ebac408ae2 100644 --- a/src/blocks/spinner/Spinner.utils.ts +++ b/src/blocks/spinner/Spinner.utils.ts @@ -3,13 +3,13 @@ import { SpinnerSize, SpinnerVariant } from './Spinner.types'; export const getSpinnerSize = (size: SpinnerSize) => { switch (size) { case 'small': - return '16px'; + return 16; case 'medium': - return '24px'; + return 24; case 'large': - return '32px'; + return 32; default: - return '48px'; + return 48; } }; export const getSpinnerStrokeWidth = (size: SpinnerSize) => { diff --git a/src/blocks/theme/semantics/semantics.button.ts b/src/blocks/theme/semantics/semantics.button.ts index fd625cc34f..3ac4dda4e7 100644 --- a/src/blocks/theme/semantics/semantics.button.ts +++ b/src/blocks/theme/semantics/semantics.button.ts @@ -10,6 +10,7 @@ export const primaryButtonSemantics = { 'background-hover': { light: colorBrands['primary-400'], dark: colorBrands['primary-400'] }, 'background-pressed': { light: colorBrands['primary-800'], dark: colorBrands['primary-600'] }, 'background-focus': { light: colorBrands['primary-500'], dark: colorBrands['primary-400'] }, + 'background-loading': { light: colorBrands['primary-400'], dark: colorBrands['primary-400'] }, 'background-disabled': { light: surfaceSemantics['state-disabled'].light, dark: surfaceSemantics['state-disabled'].dark, @@ -29,6 +30,7 @@ export const secondaryButtonSemantics = { 'background-hover': { light: colorBrands['neutral-200'], dark: colorBrands['neutral-700'] }, 'background-pressed': { light: colorBrands['neutral-300'], dark: colorBrands['neutral-1000'] }, 'background-focus': { light: colorBrands['neutral-100'], dark: colorBrands['neutral-800'] }, + 'background-loading': { light: colorBrands['neutral-100'], dark: colorBrands['neutral-800'] }, 'background-disabled': { light: surfaceSemantics['state-disabled'].light, dark: surfaceSemantics['state-disabled'].dark, @@ -49,6 +51,7 @@ export const tertiaryButtonSemantics = { 'background-hover': { light: colorBrands['neutral-900'], dark: colorBrands['neutral-300'] }, 'background-pressed': { light: colorBrands['neutral-100'], dark: colorPrimitives['gray-1000'] }, 'background-focus': { light: colorBrands['neutral-1000'], dark: colorBrands['neutral-700'] }, + 'background-loading': { light: colorBrands['neutral-900'], dark: colorBrands['neutral-700'] }, 'background-disabled': { light: surfaceSemantics['state-disabled'].light, dark: surfaceSemantics['state-disabled'].dark, @@ -81,6 +84,7 @@ export const outlineButtonSemantics = { 'stroke-default': { light: strokeSemantics['tertiary'].light, dark: strokeSemantics['tertiary'].dark }, 'stroke-focus': { light: colorBrands['primary-300'], dark: colorBrands['primary-400'] }, + 'stroke-loading': { light: colorBrands['neutral-200'], dark: colorBrands['primary-400'] }, 'stroke-hover': { light: strokeSemantics['brand-subtle'].light, dark: strokeSemantics['secondary'].dark }, 'stroke-pressed': { light: colorBrands['neutral-600'], dark: colorBrands['neutral-300'] }, }; @@ -90,6 +94,7 @@ export const dangerButtonSemantics = { 'background-hover': { light: colorBrands['danger-500'], dark: colorBrands['danger-400'] }, 'background-pressed': { light: colorBrands['danger-800'], dark: colorBrands['danger-700'] }, 'background-focus': { light: colorBrands['danger-500'], dark: colorBrands['danger-400'] }, + 'background-loading': { light: colorBrands['danger-500'], dark: colorBrands['danger-400'] }, 'background-disabled': { light: surfaceSemantics['state-disabled'].light, dark: surfaceSemantics['state-disabled'].dark, @@ -109,6 +114,7 @@ export const dangerSecondaryButtonSemantics = { 'background-hover': { light: colorBrands['danger-100'], dark: colorBrands['danger-700'] }, 'background-pressed': { light: colorBrands['danger-500'], dark: colorBrands['danger-1000'] }, 'background-focus': { light: colorBrands['danger-100'], dark: colorBrands['danger-700'] }, + 'background-loading': { light: colorBrands['danger-100'], dark: colorBrands['danger-700'] }, 'background-disabled': { light: surfaceSemantics['state-disabled'].light, dark: surfaceSemantics['state-disabled'].dark,