UNPKG

@spark-web/radio

Version:

--- title: Radio storybookPath: forms-radio--default isExperimentalPackage: true ---

432 lines (423 loc) 15.7 kB
import _objectSpread from '@babel/runtime/helpers/esm/objectSpread2'; import _objectWithoutProperties from '@babel/runtime/helpers/esm/objectWithoutProperties'; import { ControlLabel } from '@spark-web/control-label'; import { Stack } from '@spark-web/stack'; import { createContext, useContext, forwardRef, Fragment } from 'react'; import _slicedToArray from '@babel/runtime/helpers/esm/slicedToArray'; import { css } from '@emotion/react'; import { useFocusRing, useId, composeId } from '@spark-web/a11y'; import { Box } from '@spark-web/box'; import { useTheme } from '@spark-web/theme'; import { isEmpty } from 'lodash'; import { jsxs, jsx } from '@emotion/react/jsx-runtime'; import { DefaultTextPropsProvider, Text } from '@spark-web/text'; import { useFieldIds, FieldMessage } from '@spark-web/field'; var RadioGroupContext = /*#__PURE__*/createContext(undefined); var useRadioGroupContext = function useRadioGroupContext() { return useContext(RadioGroupContext); }; var _excluded$2 = ["size"], _excluded2$1 = ["checked"]; var RadioPrimitive = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) { var _inputProps$value; var _ref$size = _ref.size, size = _ref$size === void 0 ? 'small' : _ref$size, inputProps = _objectWithoutProperties(_ref, _excluded$2); var theme = useTheme(); var responsiveStyles = theme.utils.responsiveStyles({ mobile: { height: theme.typography.text.small.mobile.capHeight }, tablet: { height: theme.typography.text.small.tablet.capHeight } }); var _useRadioStyles = useRadioStyles({ size: size }), _useRadioStyles2 = _slicedToArray(_useRadioStyles, 2), boxProps = _useRadioStyles2[0], radioStyles = _useRadioStyles2[1]; return jsxs(Box, { display: "flex", alignItems: "center", flexShrink: 0, css: css(responsiveStyles), children: [jsx(Box, _objectSpread(_objectSpread(_objectSpread({}, inputProps), boxProps), {}, { "aria-checked": inputProps.checked, "aria-disabled": inputProps.disabled, as: "input", css: css(radioStyles), ref: forwardedRef, type: "radio", value: (_inputProps$value = inputProps.value) === null || _inputProps$value === void 0 ? void 0 : _inputProps$value.toString() })), jsx("span", { "aria-hidden": true, "data-radio-border": "true" })] }); }); RadioPrimitive.displayName = 'RadioPrimitive'; var sizeToScaleKey = { small: 'xxsmall', medium: 'xsmall' }; var outerToInnerSize = { xxsmall: 6, xsmall: 9 }; function useTransitionProperties() { var theme = useTheme(); return { transitionProperty: 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter', transitionTimingFunction: theme.animation.standard.easing, transitionDuration: "".concat(theme.animation.standard.duration, "ms") }; } /** * Returns a tuple where the first item is an object of props to spread onto the * underlying Box component that our inputs are created with, and the second * item is a CSS object to be passed to Emotion's `css` function */ function useRadioStyles(_ref2) { var _focusChecked$border, _focusChecked$border2; var size = _ref2.size; var theme = useTheme(); var focusRingStyles = useFocusRing(); var radioTheme = theme.components.radio; var _ref3 = radioTheme.focus || {}, focusChecked = _ref3.checked, focus = _objectWithoutProperties(_ref3, _excluded2$1); var outerSize = sizeToScaleKey[size]; var innerSize = outerToInnerSize[outerSize]; var transitionProperties = useTransitionProperties(); return [{ border: radioTheme.border["default"], borderRadius: radioTheme.border.radius.full, background: radioTheme.background, display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 1, height: outerSize, width: outerSize, position: 'relative', shadow: 'small' }, _objectSpread(_objectSpread({ appearance: 'none', verticalAlign: 'text-bottom', borderWidth: radioTheme.border.width }, transitionProperties), {}, { // Inner circle of radio '&::before': _objectSpread({ content: '""', position: 'absolute', margin: 'auto', top: 0, right: 0, bottom: 0, left: 0, height: 0, width: 0, overflow: 'hidden' }, transitionProperties), // Focus styles '&:focus': _objectSpread({}, !isEmpty(focus) ? focus : focusRingStyles), // Checked styles '&:checked': { background: radioTheme.color.background.primary, borderColor: radioTheme.border.color.primary, '&:focus': focusChecked ? { borderWidth: focusChecked.border.width, borderColor: (_focusChecked$border = focusChecked.border) === null || _focusChecked$border === void 0 ? void 0 : _focusChecked$border.color } : undefined }, '&:checked::before': { background: radioTheme.color.background.surface, borderRadius: radioTheme.border.radius.pseudo, height: innerSize, width: innerSize }, '&:hover::before': { background: radioTheme.backgroundInteractions.backgroundColorHover }, // Show a border on the radio when the label is hovered. 'label:hover &:not([disabled], &[aria-disabled=true])': { borderColor: radioTheme.border.color.primary }, // Hover styles when checked 'label:hover &:not([disabled], &[aria-disabled=true]):checked': { // TODO: radio gets lighter on hover instead of darker like in the designs, will fix once tokens are revised background: radioTheme.backgroundInteractions.primaryHover, borderColor: radioTheme.backgroundInteractions.borderColor, borderWidth: radioTheme.backgroundInteractions.borderWidthHover, '&:focus': focusChecked ? { borderWidth: focusChecked.border.width, borderColor: (_focusChecked$border2 = focusChecked.border) === null || _focusChecked$border2 === void 0 ? void 0 : _focusChecked$border2.color } : undefined }, // Disabled styles when checked '&[disabled]:checked, &[aria-disabled=true]:checked': { // TODO: using a `border` colour for background here as we don't have a token for it just yet background: radioTheme.border.color.field, border: radioTheme.border.color.accent }, '&[disabled]:checked::before, &[aria-disabled=true]:checked::before': { background: radioTheme.color.background.fieldAccent } })]; } function useRadioGroupState(props) { var _props$name; var name = useId(props.name); return { disabled: props.disabled, name: (_props$name = props.name) !== null && _props$name !== void 0 ? _props$name : name, onChange: function onChange(event) { if (props.disabled) { return; } var inputValue = event.target.value; // event.target.value always returns string, convert to boolean if value is originally boolean type or initially undefined. if (typeof props.value === 'boolean' || typeof props.value === 'undefined') { if (inputValue === 'true') { inputValue = true; } if (inputValue === 'false') { inputValue = false; } } props.onChange(inputValue); }, size: props.size, value: props.value, 'aria-describedby': props['aria-describedby'] }; } function useRadioGroupItem(_ref) { var props = _ref.props, state = _ref.state; if (typeof state === 'undefined') { return undefined; } var radioValue = props.value; if (typeof radioValue === 'undefined') { throw new Error('Each <Radio> within a <RadioGroup> requires a `value` property.'); } return { checked: state.value === radioValue, disabled: state.disabled, name: state.name, onChange: state.onChange, size: state.size, value: radioValue, 'aria-describedby': state['aria-describedby'] }; } var _excluded$1 = ["children", "data", "disabled", "id", "size"]; var Radio = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) { var _ref2; var children = _ref.children, data = _ref.data, disabled = _ref.disabled, id = _ref.id, sizeProp = _ref.size, consumerProps = _objectWithoutProperties(_ref, _excluded$1); var groupState = useRadioGroupContext(); var radioGroupItemProps = useRadioGroupItem({ props: consumerProps, state: groupState }); var inputProps = typeof groupState === 'undefined' ? consumerProps : radioGroupItemProps; var isDisabled = (_ref2 = disabled !== null && disabled !== void 0 ? disabled : groupState === null || groupState === void 0 ? void 0 : groupState.disabled) !== null && _ref2 !== void 0 ? _ref2 : false; var size = sizeProp !== null && sizeProp !== void 0 ? sizeProp : groupState === null || groupState === void 0 ? void 0 : groupState.size; return jsx(Stack, { gap: "small", children: jsx(ControlLabel, { control: jsx(RadioPrimitive, _objectSpread(_objectSpread({}, inputProps), {}, { data: data, ref: forwardedRef, disabled: isDisabled, size: size, id: id })), disabled: isDisabled, htmlFor: id, size: size !== null && size !== void 0 ? size : 'small', children: children }) }); }); Radio.displayName = 'Radio'; var _excluded = ["children", "data", "description", "padding", "disabled"], _excluded2 = ["checked"]; var RadioCard = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) { var _ref2; var children = _ref.children, data = _ref.data, description = _ref.description, _ref$padding = _ref.padding, padding = _ref$padding === void 0 ? 'none' : _ref$padding, disabled = _ref.disabled, consumerProps = _objectWithoutProperties(_ref, _excluded); var groupState = useRadioGroupContext(); var radioGroupItemProps = useRadioGroupItem({ props: consumerProps, state: groupState }); var inputProps = typeof groupState === 'undefined' ? consumerProps : radioGroupItemProps; var isDisabled = (_ref2 = disabled !== null && disabled !== void 0 ? disabled : groupState === null || groupState === void 0 ? void 0 : groupState.disabled) !== null && _ref2 !== void 0 ? _ref2 : false; var size = 'small'; var _useRadioCardStyles = useRadioCardStyles(isDisabled), _useRadioCardStyles2 = _slicedToArray(_useRadioCardStyles, 2), boxProps = _useRadioCardStyles2[0], radioCardStyles = _useRadioCardStyles2[1]; var inputId = useId(); var labelId = composeId(inputId, 'label'); var descriptionId = composeId(inputId, 'description'); return jsx(Stack, _objectSpread(_objectSpread({}, boxProps), {}, { as: "label", htmlFor: inputId, css: css(radioCardStyles), children: jsxs(Box, { alignItems: "start", display: "inline-flex", gap: size, padding: padding, children: [jsx(RadioPrimitive, _objectSpread(_objectSpread({}, inputProps), {}, { "aria-describedby": description ? descriptionId : undefined, "aria-labelledby": labelId, data: data, disabled: isDisabled, id: inputId, ref: forwardedRef, size: size })), jsxs(Stack, { gap: "large", children: [jsx(DefaultTextPropsProvider, { tone: isDisabled ? 'disabled' : 'neutral', weight: description ? 'semibold' : 'regular', children: jsx(Content, { id: labelId, children: children }) }), description && jsx(Text, { id: descriptionId, tone: isDisabled ? 'disabled' : 'muted', children: description })] })] }) })); }); RadioCard.displayName = 'RadioCard'; function Content(_ref3) { var children = _ref3.children, id = _ref3.id; if (typeof children === 'string' || typeof children === 'number') { return jsx(Text, { id: id, children: children }); } return jsx(Fragment, { children: children }); } function useRadioCardStyles(isDisabled) { var _radioTheme$radioCard, _radioTheme$radioCard2; var focusRingStyles = useFocusRing(); var transitionProperties = useTransitionProperties(); var theme = useTheme(); var radioTheme = theme.components.radio; var _ref4 = radioTheme.focus || {}; _ref4.checked; var focus = _objectWithoutProperties(_ref4, _excluded2); return [ // Box props to be spread onto label { background: isDisabled ? 'inputDisabled' : 'surface', cursor: isDisabled ? 'default' : 'pointer', padding: 'large', position: 'relative', userSelect: 'none', height: 'full' }, { // Simulates a border for the radio card 'input[type=radio] + [data-radio-border=true]': _objectSpread({ position: 'absolute', top: 0, right: 0, bottom: 0, left: 0, borderColor: radioTheme.border.color.field, borderStyle: 'solid', borderWidth: radioTheme.radioCardBorder.border.width, borderRadius: radioTheme.radioCardBorder.border.radius, pointerEvents: 'none' }, transitionProperties), // Change border color of card on hover (when not disabled) ':hover input:not([disabled], [aria-disabled=true]) + [data-radio-border=true]': { borderColor: radioTheme.backgroundInteractions.borderColorHover, boxShadow: (_radioTheme$radioCard = radioTheme.radioCardBorder.hover) !== null && _radioTheme$radioCard !== void 0 && _radioTheme$radioCard.boxShadow ? (_radioTheme$radioCard2 = radioTheme.radioCardBorder) === null || _radioTheme$radioCard2 === void 0 || (_radioTheme$radioCard2 = _radioTheme$radioCard2.hover) === null || _radioTheme$radioCard2 === void 0 ? void 0 : _radioTheme$radioCard2.boxShadow : 'none' }, // Remove focus ring on radio (as it is on the whole card) 'input[type=radio]:focus': { boxShadow: 'none' }, // Border style for card when checked 'input[type=radio]:checked + [data-radio-border=true]': { borderColor: isDisabled ? radioTheme.border.color.field : radioTheme.border.color.primary, // TO DO: Fix this -> should be 2 from token borderWidth: radioTheme.backgroundInteractions.borderWidthChecked }, // Focus styles for card 'input[type=radio]:focus + [data-radio-border=true]': _objectSpread({ borderColor: radioTheme.backgroundInteractions.borderColorFocus }, !isEmpty(focus) ? focus : focusRingStyles) }]; } var RadioGroup = function RadioGroup(_ref) { var children = _ref.children, disabled = _ref.disabled, name = _ref.name, onChange = _ref.onChange, size = _ref.size, value = _ref.value, message = _ref.message, idProp = _ref.id, ariaDescribedBy = _ref['aria-describedby'], ariaLabel = _ref['aria-label'], _ref$tone = _ref.tone, tone = _ref$tone === void 0 ? 'neutral' : _ref$tone; var context = useRadioGroupState({ disabled: disabled, name: name, onChange: onChange, size: size, value: value, 'aria-describedby': ariaDescribedBy }); var _useFieldIds = useFieldIds(idProp), radioGroupId = _useFieldIds.inputId, messageId = _useFieldIds.messageId; return jsxs(RadioGroupContext.Provider, { value: _objectSpread(_objectSpread({}, context), {}, { 'aria-describedby': message && messageId }), children: [jsx("div", { id: radioGroupId, role: "radiogroup", style: { display: 'contents' }, "aria-label": ariaLabel, children: children }), message && jsx(FieldMessage, { tone: tone, id: messageId, message: message })] }); }; export { Radio, RadioCard, RadioGroup, RadioPrimitive };