@workday/canvas-kit-react
Version:
The parent module that contains all Workday Canvas Kit React components
197 lines (196 loc) • 7.96 kB
JavaScript
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))));
},
});