diff --git a/.changeset/hot-cheetahs-press.md b/.changeset/hot-cheetahs-press.md new file mode 100644 index 0000000000..a41c6d6a62 --- /dev/null +++ b/.changeset/hot-cheetahs-press.md @@ -0,0 +1,5 @@ +--- +'@sumup/circuit-ui': major +--- + +`Selector` now has multiple `size` options. `SelectorGroup` is changed to horizontal layout and to be inline element by default with an option to stretch to full width. \ No newline at end of file diff --git a/packages/circuit-ui/components/Selector/Selector.docs.mdx b/packages/circuit-ui/components/Selector/Selector.docs.mdx index 542d01df9e..d82be6d887 100644 --- a/packages/circuit-ui/components/Selector/Selector.docs.mdx +++ b/packages/circuit-ui/components/Selector/Selector.docs.mdx @@ -31,6 +31,16 @@ same screen that they are currently in. For example, choosing a payment method o +### Sizes + +Selector component supports 3 different sizes: + +- **mega**, the default size +- **kilo**, used when there are sizing constraints +- **flexible**, used for more complex content within selector which may define its own margins. **flexible** just adds a minimal equal margin on all sides. + + + ## SelectorGroup diff --git a/packages/circuit-ui/components/Selector/Selector.stories.tsx b/packages/circuit-ui/components/Selector/Selector.stories.tsx index 5021b28adf..785a63dc4f 100644 --- a/packages/circuit-ui/components/Selector/Selector.stories.tsx +++ b/packages/circuit-ui/components/Selector/Selector.stories.tsx @@ -15,7 +15,9 @@ import React from 'react'; +import { Stack } from '../../../../.storybook/components'; import SelectorGroup from '../SelectorGroup'; +import Body from '../Body'; import docs from './Selector.docs.mdx'; import { Selector, SelectorProps } from './Selector'; @@ -62,3 +64,21 @@ Disabled.args = { name: 'disabled', disabled: true, }; +export const Sizes = (args: SelectorProps) => ( + + + Kilo + + + Mega + + + + Flexible + + Hello World! + + +); + +Disabled.args = { ...baseArgs, disabled: true }; diff --git a/packages/circuit-ui/components/Selector/Selector.tsx b/packages/circuit-ui/components/Selector/Selector.tsx index 168ce598c6..6f23b52436 100644 --- a/packages/circuit-ui/components/Selector/Selector.tsx +++ b/packages/circuit-ui/components/Selector/Selector.tsx @@ -18,18 +18,18 @@ import React, { Fragment, Ref, HTMLProps } from 'react'; import { css, jsx } from '@emotion/core'; import { Dispatch as TrackingProps } from '@sumup/collector'; +import { Theme } from '@sumup/design-tokens'; import styled, { StyleProps } from '../../styles/styled'; -import { - focusOutline, - hideVisually, - disableVisually, -} from '../../styles/style-mixins'; +import { hideVisually, disableVisually } from '../../styles/style-mixins'; import { uniqueId } from '../../util/id'; import useClickHandler from '../../hooks/use-click-handler'; import deprecate from '../../util/deprecate'; -export interface SelectorProps extends HTMLProps { +export type SelectorSize = 'kilo' | 'mega' | 'flexible'; + +export interface SelectorProps + extends Omit, 'size'> { /** * Value string for input. */ @@ -42,6 +42,10 @@ export interface SelectorProps extends HTMLProps { * The name of the selector. */ name?: string; + /** + * Choose from 3 sizes. Default: 'mega'. + */ + size?: SelectorSize; /** * Whether the selector is selected or not. */ @@ -68,50 +72,58 @@ export interface SelectorProps extends HTMLProps { tracking?: TrackingProps; } -type LabelElProps = Pick; - -const baseStyles = ({ theme }: StyleProps) => css` - label: selector__label; - display: block; - cursor: pointer; - padding: ${theme.spacings.mega} ${theme.spacings.giga}; - border-radius: ${theme.borderRadius.byte}; - background-color: ${theme.colors.white}; - text-align: center; - position: relative; - margin-bottom: ${theme.spacings.mega}; - - &::before { - display: block; - content: ''; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - width: 100%; - height: 100%; - border-radius: ${theme.borderRadius.byte}; - border: ${theme.borderWidth.kilo} solid ${theme.colors.n300}; - transition: border 0.1s ease-in-out; - } +type LabelElProps = Pick< + SelectorProps, + 'disabled' | 'noMargin' | 'size' | 'checked' +>; - &:hover { - background-color: ${theme.colors.n100}; +interface OutlineStyles { + default: string; + hover: string; + active: string; +} - &::before { - border-color: ${theme.colors.n500}; - } - } +const getBorderStyles = (theme: Theme, checked = false): OutlineStyles => { + const defaultBorder = `0 0 0 ${theme.borderWidth.kilo} ${theme.colors.n300}`; + const hoverBorder = `0 0 0 ${theme.borderWidth.kilo} ${theme.colors.n500}`; + const activeBorder = `0 0 0 ${theme.borderWidth.kilo} ${theme.colors.n700}`; + const checkedBorder = `0 0 0 ${theme.borderWidth.mega} ${theme.colors.p500}`; - &:active { - background-color: ${theme.colors.n200}; + return { + default: checked ? checkedBorder : defaultBorder, + hover: checked ? checkedBorder : hoverBorder, + active: checked ? checkedBorder : activeBorder, + }; +}; + +const baseStyles = ({ theme, checked }: StyleProps & LabelElProps) => { + const borderStyles = getBorderStyles(theme, checked); - &::before { - border-color: ${theme.colors.n700}; + return css` + label: selector__label; + display: inline-block; + cursor: pointer; + padding: ${theme.spacings.mega} ${theme.spacings.giga}; + background-color: ${checked ? theme.colors.p100 : theme.colors.white}; + text-align: center; + position: relative; + margin-bottom: ${theme.spacings.mega}; + border: none; + border-radius: ${theme.borderRadius.byte}; + transition: box-shadow 0.1s ease-in-out; + box-shadow: ${borderStyles.default}; + + &:hover { + background-color: ${theme.colors.n100}; + box-shadow: ${borderStyles.hover}; } - } -`; + + &:active { + background-color: ${theme.colors.n200}; + box-shadow: ${borderStyles.active}; + } + `; +}; const disabledStyles = ({ disabled }: LabelElProps) => disabled && @@ -137,30 +149,59 @@ const noMarginStyles = ({ noMargin }: LabelElProps) => { `; }; +const sizeStyles = ({ theme, size = 'mega' }: LabelElProps & StyleProps) => { + const sizeMap = { + kilo: { + padding: `${theme.spacings.bit} ${theme.spacings.mega}`, + }, + mega: { + // +1px is to match the height of other form components + // like Input or Select that also have +1px for vertical padding + padding: `calc(${theme.spacings.byte} + 1px) ${theme.spacings.giga}`, + }, + flexible: { + padding: `${theme.spacings.mega} ${theme.spacings.mega}`, + }, + }; + + return css({ + label: `selector__label--${size}`, + ...sizeMap[size], + }); +}; + const SelectorLabel = styled('label')( baseStyles, + sizeStyles, disabledStyles, noMarginStyles, ); -const inputStyles = ({ theme }: StyleProps) => css` - label: selector__input; - ${hideVisually()}; +const inputStyles = ({ theme, checked }: StyleProps & LabelElProps) => { + const borderStyles = getBorderStyles(theme, checked); + const focusOutline = `0 0 0 ${checked ? '5px' : '4px'} ${theme.colors.p300}`; - &:focus + label::before { - ${focusOutline({ theme })}; - } + return css` + label: selector__input; + ${hideVisually()}; - &:checked + label { - background-color: ${theme.colors.p100}; + &:focus + label { + box-shadow: ${borderStyles.default}, ${focusOutline}; + } - &::before { - border: ${theme.borderWidth.mega} solid ${theme.colors.p500}; + &:focus + label:hover { + box-shadow: ${borderStyles.hover}, ${focusOutline}; } - } -`; -const SelectorInput = styled('input')(inputStyles); + &:focus + label:active { + box-shadow: ${borderStyles.active}, ${focusOutline}; + } + `; +}; + +const SelectorInput = styled('input')< + HTMLProps & LabelElProps +>(inputStyles); /** * A selector allows users to choose between several mutually-exclusive choices @@ -181,6 +222,7 @@ export const Selector = React.forwardRef( className, style, noMargin, + size, ...props }: SelectorProps, ref: SelectorProps['ref'], @@ -213,6 +255,8 @@ export const Selector = React.forwardRef( , .circuit-0 { - display: block; + display: inline-block; cursor: pointer; padding: 16px 24px; - border-radius: 8px; - background-color: #FFF; + background-color: #F0F6FF; text-align: center; position: relative; margin-bottom: 16px; -} - -.circuit-0::before { - display: block; - content: ''; - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - width: 100%; - height: 100%; + border: none; border-radius: 8px; - border: 1px solid #CCC; - -webkit-transition: border 0.1s ease-in-out; - transition: border 0.1s ease-in-out; + -webkit-transition: box-shadow 0.1s ease-in-out; + transition: box-shadow 0.1s ease-in-out; + box-shadow: 0 0 0 2px #3063E9; + padding: calc(8px + 1px) 24px; } .circuit-0:hover { background-color: #F5F5F5; -} - -.circuit-0:hover::before { - border-color: #999; + box-shadow: 0 0 0 2px #3063E9; } .circuit-0:active { background-color: #E6E6E6; -} - -.circuit-0:active::before { - border-color: #666; + box-shadow: 0 0 0 2px #3063E9; }