Skip to content

Commit

Permalink
feat(Button): No container button variant (#3823)
Browse files Browse the repository at this point in the history
* chore(Button): Remove nbsp from layout

* feat(Button): Add No Container variant

* chore(Stories): Remove redundant text from story

* chore(Button): Use consistent palette for (unused) css property

* refactor(Button): Organising button styling

* chore(Button): Remove hover background from containerless button

* chore(Button): Use box-shadow to prevent incorrect border aliasing

* chore(Styled): Match tertiary text and border on hover

* docs(Button): Remove no container button from default story
  • Loading branch information
markhigham authored Jun 12, 2024
1 parent 01d7f45 commit a221894
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 158 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import styled from 'styled-components'

import { Meta, StoryFn } from '@storybook/react'

import { IconBrightnessLow } from '@royalnavy/icon-library'
import { IconWifi } from '@royalnavy/icon-library'
import { spacing } from '@royalnavy/design-tokens'

import { Button, ButtonProps } from './index'
Expand Down Expand Up @@ -33,18 +33,20 @@ const StyledButtonStrip = styled.div`
max-width: 500px;
display: flex;
justify-content: start;
gap: ${spacing('2')};
gap: ${spacing('4')};
padding: ${spacing('4')} 0;
`

type ButtonStripArgs = ButtonProps & {
title?: string
hideText?: boolean
hideNoContainer?: boolean
}

const ButtonStrip = (args: ButtonStripArgs) => {
const localArgs = { ...args }
const shouldHideButtonText = !!args.hideText
const shouldDisplayNoContainerButton = !args.hideNoContainer
delete localArgs.variant
delete localArgs.isDisabled
const disabledStates = [false, true]
Expand All @@ -59,31 +61,37 @@ const ButtonStrip = (args: ButtonStripArgs) => {
isDisabled={state}
{...localArgs}
>
{shouldHideButtonText ? '' : 'Primary'}
{shouldHideButtonText ? '' : 'Primary button'}
</Button>
&nbsp;
<Button
variant={BUTTON_VARIANT.SECONDARY}
isDisabled={state}
{...localArgs}
>
{shouldHideButtonText ? '' : 'Secondary'}
{shouldHideButtonText ? '' : 'Secondary button'}
</Button>
&nbsp;
<Button
variant={BUTTON_VARIANT.TERTIARY}
isDisabled={state}
{...localArgs}
>
{shouldHideButtonText ? '' : 'Tertiary'}
{shouldHideButtonText ? '' : 'Tertiary button'}
</Button>
&nbsp;
{shouldDisplayNoContainerButton && (
<Button
variant={BUTTON_VARIANT.NO_CONTAINER}
isDisabled={state}
{...localArgs}
>
{shouldHideButtonText ? '' : 'No container button'}
</Button>
)}
<Button
variant={BUTTON_VARIANT.DANGER}
isDisabled={state}
{...localArgs}
>
{shouldHideButtonText ? '' : 'Danger'}
{shouldHideButtonText ? '' : 'Danger button'}
</Button>
</StyledButtonStrip>
))}
Expand All @@ -96,7 +104,7 @@ const ButtonStrip = (args: ButtonStripArgs) => {
export const RegularButtons: StoryFn<typeof Button> = (args) => {
const iconLeftArgs = {
...args,
icon: <IconBrightnessLow />,
icon: <IconWifi />,
iconPosition: BUTTON_ICON_POSITION.LEFT,
}
const iconRightArgs = {
Expand All @@ -115,12 +123,11 @@ export const RegularButtons: StoryFn<typeof Button> = (args) => {

return (
<>
<p>All variants of {args.size} buttons</p>
<ButtonStrip {...args} title="Default" />
<ButtonStrip {...args} title="Default" hideNoContainer />
<ButtonStrip {...iconLeftArgs} title="With icons left" />
<ButtonStrip {...iconRightArgs} title="With icons right" />
<ButtonStrip {...iconOnlyArgs} title="Icon only" hideText />
<ButtonStrip {...loadingArgs} title="Loading" />
<ButtonStrip {...loadingArgs} title="Loading" hideNoContainer />
</>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ const BUTTON_VARIANT = {
SECONDARY: 'secondary',
TERTIARY: 'tertiary',
DANGER: 'danger',
NO_CONTAINER: 'no-container',
} as const

const BUTTON_ICON_POSITION = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './Button'
export * from './constants'

export { getButtonStyles } from './partials/StyledButton'
export { getButtonStyles } from './partials/getButtonStyles'
Original file line number Diff line number Diff line change
@@ -1,147 +1,5 @@
import styled, { css } from 'styled-components'
import { color, fontSize, shadow, spacing } from '@royalnavy/design-tokens'
import { rgba } from 'polished'

import { BUTTON_ICON_POSITION, BUTTON_VARIANT } from '../constants'
import { ButtonIconPositionType, ButtonVariantType } from '../Button'
import { ComponentSizeType, COMPONENT_SIZE } from '../../Forms'

const DROP_SHADOW = `0 2px 6px ${rgba(0, 0, 0, 0.3)}`
const TRANSPARENT_SHADOW = shadow('0')
const DEFAULT_HOVER_BORDER_SHADOW = `0 0 0 2px ${color('action', '900')}`
const DANGER_HOVER_BORDER_SHADOW = `0 0 0 2px ${color('danger', '900')}`

const COLOR_MAP = {
[BUTTON_VARIANT.PRIMARY]: {
color: color('neutral', 'white'),
backgroundColor: color('action', '600'),
borderColor: color('action', '600'),
borderWidth: 0,
hoverBackgroundColor: color('action', '700'),
hoverBoxShadow: DEFAULT_HOVER_BORDER_SHADOW,
activeBackgroundColor: color('action', '900'),
focusBorderColor: color('action', '800'),
},
[BUTTON_VARIANT.SECONDARY]: {
color: color('action', '900'),
backgroundColor: color('action', '100'),
borderColor: color('action', '600'),
borderWidth: '1px',
hoverBackgroundColor: color('action', '200'),
hoverBoxShadow: DEFAULT_HOVER_BORDER_SHADOW,
activeBackgroundColor: color('action', '300'),
focusBorderColor: color('action', '300'),
},
[BUTTON_VARIANT.TERTIARY]: {
color: color('action', '600'),
backgroundColor: color('neutral', 'white'),
borderColor: color('action', '600'),
borderWidth: '1px',
hoverBackgroundColor: color('neutral', '000'),
hoverBoxShadow: DEFAULT_HOVER_BORDER_SHADOW,
activeBackgroundColor: color('neutral', '100'),
focusBorderColor: color('action', '800'),
},
[BUTTON_VARIANT.DANGER]: {
color: color('neutral', 'white'),
backgroundColor: color('danger', '700'),
borderColor: color('danger', '900'),
borderWidth: 0,
hoverBackgroundColor: color('danger', '800'),
hoverBoxShadow: DANGER_HOVER_BORDER_SHADOW,
activeBackgroundColor: color('danger', '900'),
focusBorderColor: color('action', '800'),
},
}

const SIZE_MAP = {
[COMPONENT_SIZE.FORMS]: {
height: '46px',
fontSize: fontSize('m'),
borderRadius: '8px',
},
[COMPONENT_SIZE.SMALL]: {
height: '33px',
fontSize: fontSize('base'),
borderRadius: '8px',
},
}

interface StyledButtonProps {
$variant: ButtonVariantType
$size: ComponentSizeType
$iconPosition?: ButtonIconPositionType
}

export function getButtonStyles({
$size,
$variant,
$iconPosition = BUTTON_ICON_POSITION.RIGHT,
}: StyledButtonProps) {
return css`
height: ${SIZE_MAP[$size].height};
display: inline-flex;
flex-direction: ${$iconPosition === BUTTON_ICON_POSITION.LEFT
? 'row-reverse'
: 'row'};
align-items: center;
justify-content: center;
border-radius: ${SIZE_MAP[$size].borderRadius};
outline: 0;
padding: 0 ${spacing('7')};
font-size: ${SIZE_MAP[$size].fontSize};
font-weight: 400;
text-decoration: none;
cursor: pointer;
user-select: none;
transition: all 75ms cubic-bezier(0, 1.19, 0.82, 0.9);
white-space: nowrap;
&:hover {
text-decoration: none;
}
&:active {
box-shadow: ${TRANSPARENT_SHADOW}, ${TRANSPARENT_SHADOW};
}
${css`
color: ${COLOR_MAP[$variant].color};
background-color: ${COLOR_MAP[$variant].backgroundColor};
border: ${COLOR_MAP[$variant].borderWidth} solid
${COLOR_MAP[$variant].borderColor};
&:focus {
border: ${COLOR_MAP[$variant].borderWidth} solid
${COLOR_MAP[$variant].focusBorderColor};
}
&:focus,
&:hover {
background-color: ${COLOR_MAP[$variant].hoverBackgroundColor};
box-shadow: ${COLOR_MAP[$variant].hoverBoxShadow}, ${DROP_SHADOW};
}
&:active {
background-color: ${COLOR_MAP[$variant].activeBackgroundColor};
}
`}
&:disabled {
&,
&:hover,
&:active,
&:focus {
color: ${color('neutral', '400')};
background-color: ${color('neutral', '000')};
border: ${COLOR_MAP[$variant].borderWidth} solid
${color('neutral', '200')};
box-shadow: none;
cursor: not-allowed;
}
}
`
}
import styled from 'styled-components'
import { getButtonStyles, StyledButtonProps } from './getButtonStyles'

export const StyledButton = styled.button<StyledButtonProps>`
position: relative;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
import { css } from 'styled-components'
import { spacing } from '@royalnavy/design-tokens'

import { BUTTON_ICON_POSITION } from '../constants'
import { ButtonIconPositionType, ButtonVariantType } from '../Button'
import { ComponentSizeType } from '../../Forms'
import { SIZE_MAP, STYLES_MAP } from './styles'

const DEFAULT_HORIZONTAL_PADDING = spacing('7')

export interface StyledButtonProps {
$variant: ButtonVariantType
$size: ComponentSizeType
$iconPosition?: ButtonIconPositionType
}

const commonStyles = css`
box-sizing: border-box;
font-weight: 400;
text-decoration: none;
cursor: pointer;
user-select: none;
transition: all 75ms cubic-bezier(0, 1.19, 0.82, 0.9);
white-space: nowrap;
display: inline-flex;
align-items: center;
justify-content: center;
`

export function getButtonStyles({
$size,
$variant,
$iconPosition = BUTTON_ICON_POSITION.RIGHT,
}: StyledButtonProps) {
const styles = STYLES_MAP[$variant]
const sizes = SIZE_MAP[$size]
return css`
${commonStyles};
font-size: ${sizes.fontSize};
height: ${sizes.height};
flex-direction: ${$iconPosition === BUTTON_ICON_POSITION.LEFT
? 'row-reverse'
: 'row'};
border-radius: ${sizes.borderRadius};
padding: 0 ${styles.horizontalPadding ?? DEFAULT_HORIZONTAL_PADDING};
color: ${styles.default.color};
background-color: ${styles.default.backgroundColor};
border: 0;
outline: 0;
box-shadow: ${styles.default.boxShadow};
text-decoration: ${styles.default.textDecoration};
&:hover {
color: ${styles.hover?.color ?? styles.default.color};
background-color: ${styles.hover?.backgroundColor ??
styles.default.backgroundColor};
box-shadow: ${styles.hover?.boxShadow ?? styles.default.boxShadow};
text-decoration: ${styles.hover?.textDecoration ??
styles.default.textDecoration};
}
&:focus {
color: ${styles.focus?.color ?? styles.default.color};
background-color: ${styles.focus?.backgroundColor ??
styles.default.backgroundColor};
outline: 0;
box-shadow: ${styles.focus?.boxShadow ?? styles.default.boxShadow};
text-decoration: ${styles.focus?.textDecoration ??
styles.default.textDecoration};
}
&:active {
color: ${styles.active?.color ?? styles.default.color};
background-color: ${styles.active?.backgroundColor ??
styles.default.backgroundColor};
box-shadow: ${styles.active?.boxShadow ?? styles.default.boxShadow};
}
&:disabled {
&,
&:hover,
&:active,
&:focus {
color: ${styles.disabled.color};
background-color: ${styles.disabled.backgroundColor};
text-decoration: ${styles.disabled.textDecoration};
box-shadow: none;
cursor: not-allowed;
text-decoration: none;
}
}
`
}
Loading

0 comments on commit a221894

Please sign in to comment.