UNPKG

@workday/canvas-kit-react

Version:

The parent module that contains all Workday Canvas Kit React components

197 lines (196 loc) • 7.96 kB
import * as React from 'react'; import { createComponent, focusRing, mouseFocusBehavior, styled, useUniqueId, } from '@workday/canvas-kit-react/common'; import { borderRadius, colors, inputColors, space } from '@workday/canvas-kit-react/tokens'; import { LabelText } from '@workday/canvas-kit-react/text'; import { px2rem } from '@workday/canvas-kit-styling'; const radioBorderRadius = 9; const RadioContainer = styled('div')({ display: 'flex', alignItems: 'center', minHeight: space.m, position: 'relative', }); /** * Using a wrapper prevents the browser default behavior of trigging * :hover on the radio when you hover on it's corresponding label. * This stops the ripple from showing when you hover on the label. */ const RadioInputWrapper = styled('div')({ display: 'flex', height: `calc(${space.s} + 2px)`, width: `calc(${space.s} + 2px)`, }); const RadioRipple = styled('span')({ borderRadius: borderRadius.circle, boxShadow: `0 0 0 0 ${colors.soap200}`, height: `calc(${space.s} + 2px)`, transition: 'box-shadow 150ms ease-out', width: `calc(${space.s} + 2px)`, position: 'absolute', pointerEvents: 'none', // This is a decorative element we don't want it to block clicks to input }, ({ variant }) => ({ opacity: variant === 'inverse' ? '0.4' : '1', })); const RadioInput = styled('input')({ borderRadius: `calc(${space.xxs} + 1px)`, width: space.m, height: space.m, margin: 0, marginTop: '-3px', marginLeft: '-3px', position: 'absolute', opacity: 0, '&:focus, &:active': { outline: 'none', }, }, ({ checked, disabled, variant, theme: { canvas: { palette: { primary: themePrimary, common: { focusOutline: themeFocusOutline }, }, }, }, }) => ({ cursor: disabled ? undefined : 'pointer', /** * These selectors are targetting various sibling elements (~) here because * their styles need to be connected to changes around the input's state * (e.g. hover, focus, etc.). * * We are choosing not to use component selectors from Emotion in this case. * The Babel transforms have been problematic in the past. */ // `span:first-of-type` refers to `RadioRipple`, the light grey // element that animates around the component on hover '&:hover ~ span:first-of-type': { boxShadow: disabled ? undefined : `0 0 0 calc((${space.l} - (${space.s} + 2px)) / 2) ${colors.soap200}`, }, // `div:first-of-type` refers to the `RadioBackground`, the visual facade of the // input (which is visually hidden) '&:hover ~ div:first-of-type': { backgroundColor: checked ? variant === 'inverse' ? colors.frenchVanilla100 : themePrimary.main : disabled ? inputColors.disabled.background : 'white', borderColor: checked ? variant === 'inverse' ? colors.soap300 : themePrimary.main : disabled ? inputColors.disabled.border : variant === 'inverse' ? colors.soap300 : inputColors.hoverBorder, borderWidth: '1px', }, '&:focus, &focus:hover': { '& ~ div:first-of-type': { borderWidth: '2px', borderColor: variant === 'inverse' ? colors.blackPepper400 : themeFocusOutline, boxShadow: 'none', outline: `${px2rem(2)} solid transparent`, outlineOffset: variant === 'inverse' ? '0' : '2px', ...focusRing({ width: variant === 'inverse' ? 2 : 0, separation: 0, animate: false, innerColor: variant === 'inverse' ? colors.blackPepper400 : undefined, outerColor: variant === 'inverse' ? colors.frenchVanilla100 : undefined, }), }, }, '&:checked:focus ~ div:first-of-type': { ...focusRing({ separation: 2, width: 2, innerColor: variant === 'inverse' ? colors.blackPepper400 : undefined, outerColor: variant === 'inverse' ? colors.frenchVanilla100 : themeFocusOutline, }), borderColor: variant === 'inverse' ? colors.frenchVanilla100 : themePrimary.main, borderWidth: '2px', }, ...mouseFocusBehavior({ '&:focus ~ div:first-of-type': { ...focusRing({ width: 0, outerColor: variant === 'inverse' ? colors.frenchVanilla100 : themeFocusOutline, }), borderWidth: '1px', borderColor: checked ? variant === 'inverse' ? colors.soap300 : themePrimary.main : inputColors.border, }, '&:focus:hover ~ div:first-of-type, &:focus:active ~ div:first-of-type': { borderColor: checked ? variant === 'inverse' ? colors.soap300 : themePrimary.main : variant === 'inverse' ? colors.soap300 : inputColors.hoverBorder, }, }), })); const RadioBackground = styled('div')({ alignItems: 'center', backgroundColor: colors.frenchVanilla100, borderRadius: radioBorderRadius, borderStyle: 'solid', borderWidth: '1px', boxSizing: 'border-box', display: 'flex', height: `calc(${space.s} + 2px)`, justifyContent: 'center', padding: '0px 2px', pointerEvents: 'none', position: 'absolute', transition: 'border 200ms ease, background 200ms', width: `calc(${space.s} + 2px)`, }, ({ checked, disabled, variant, theme: { canvas: { palette: { primary: themePrimary }, }, }, }) => ({ borderColor: checked ? variant === 'inverse' ? colors.soap300 : themePrimary.main : disabled ? colors.licorice100 : variant === 'inverse' ? colors.soap300 : inputColors.border, backgroundColor: checked ? variant === 'inverse' ? colors.frenchVanilla100 : themePrimary.main : disabled ? inputColors.disabled.background : 'white', opacity: disabled && variant === 'inverse' ? '.4' : '1', })); const RadioCheck = styled('div')({ borderRadius: radioBorderRadius, display: 'flex', flexDirection: 'column', height: space.xxs, pointerEvents: 'none', transition: 'transform 200ms ease, opacity 200ms ease', width: space.xxs, }, ({ theme, variant }) => ({ backgroundColor: variant === 'inverse' ? theme.canvas.palette.primary.main : theme.canvas.palette.primary.contrast, }), ({ checked }) => ({ opacity: checked ? 1 : 0, transform: checked ? 'scale(1)' : 'scale(0.5)', })); export const Radio = createComponent('input')({ displayName: 'Radio', Component: ({ checked = false, id, label = '', disabled, name, onChange, value, variant, ...elemProps }, ref, Element) => { const inputId = useUniqueId(id); return (React.createElement(RadioContainer, null, React.createElement(RadioInputWrapper, { disabled: disabled }, React.createElement(RadioInput, { as: Element, checked: checked, disabled: disabled, id: inputId, ref: ref, name: name, onChange: onChange, type: "radio", value: value, "aria-checked": checked, variant: variant, ...elemProps }), React.createElement(RadioRipple, { variant: variant }), React.createElement(RadioBackground, { checked: checked, disabled: disabled, variant: variant }, React.createElement(RadioCheck, { checked: checked, variant: variant }))), label && (React.createElement(LabelText, { paddingLeft: space.xs, htmlFor: inputId, disabled: disabled, variant: variant }, label)))); }, });