UNPKG

@spark-web/radio

Version:

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

407 lines (377 loc) 13.2 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 { useContext, createContext, forwardRef, Fragment } from 'react'; import _slicedToArray from '@babel/runtime/helpers/esm/slicedToArray'; import { css } from '@emotion/css'; import { useFocusRing, useId, composeId } from '@spark-web/a11y'; import { Box } from '@spark-web/box'; import { useTheme } from '@spark-web/theme'; import { jsxs, jsx } from '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"]; var RadioPrimitive = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) { 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 /*#__PURE__*/jsxs(Box, { display: "flex", alignItems: "center", flexShrink: 0, className: css(responsiveStyles), children: [/*#__PURE__*/jsx(Box, _objectSpread(_objectSpread(_objectSpread({}, inputProps), boxProps), {}, { "aria-checked": inputProps.checked, "aria-disabled": inputProps.disabled, as: "input", className: css(radioStyles), ref: forwardedRef, type: "radio" })), /*#__PURE__*/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 size = _ref2.size; var theme = useTheme(); var focusRingStyles = useFocusRing(); var outerSize = sizeToScaleKey[size]; var innerSize = outerToInnerSize[outerSize]; var transitionProperties = useTransitionProperties(); return [{ border: 'field', borderRadius: 'full', background: 'surface', display: 'inline-flex', alignItems: 'center', justifyContent: 'center', flex: 1, height: outerSize, width: outerSize, position: 'relative', shadow: 'small' }, _objectSpread(_objectSpread({ appearance: 'none', verticalAlign: 'text-bottom' }, 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), // Show a border on the radio when the label is hovered. 'label:hover &:not([disabled], &[aria-disabled=true])': { borderColor: theme.border.color.primaryHover }, // Focus styles '&:focus': focusRingStyles, // Checked styles '&:checked': { background: theme.color.background.primary, borderColor: theme.border.color.primaryHover }, '&:checked::before': { background: theme.color.background.surface, borderRadius: theme.border.radius.full, height: innerSize, width: innerSize }, // 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: theme.backgroundInteractions.primaryHover, border: theme.border.color.fieldAccent }, // 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: theme.border.color.field, border: theme.border.color.accent }, '&[disabled]:checked::before, &[aria-disabled=true]:checked::before': { background: theme.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; 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 /*#__PURE__*/jsx(Stack, { gap: "small", children: /*#__PURE__*/jsx(ControlLabel, { control: /*#__PURE__*/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", "disabled"]; var RadioCard = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) { var _ref2; var children = _ref.children, data = _ref.data, description = _ref.description, 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 /*#__PURE__*/jsx(Stack, _objectSpread(_objectSpread({}, boxProps), {}, { as: "label", htmlFor: inputId, className: css(radioCardStyles), children: /*#__PURE__*/jsxs(Box, { alignItems: "start", display: "inline-flex", gap: size, children: [/*#__PURE__*/jsx(RadioPrimitive, _objectSpread(_objectSpread({}, inputProps), {}, { "aria-describedby": description ? descriptionId : undefined, "aria-labelledby": labelId, data: data, disabled: isDisabled, id: inputId, ref: forwardedRef, size: size })), /*#__PURE__*/jsxs(Stack, { gap: "large", children: [/*#__PURE__*/jsx(DefaultTextPropsProvider, { tone: isDisabled ? 'disabled' : 'neutral', weight: description ? 'semibold' : 'regular', children: /*#__PURE__*/jsx(Content, { id: labelId, children: children }) }), description && /*#__PURE__*/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 /*#__PURE__*/jsx(Text, { id: id, children: children }); } return /*#__PURE__*/jsx(Fragment, { children: children }); } function useRadioCardStyles(isDisabled) { var theme = useTheme(); var focusRingStyles = useFocusRing(); var transitionProperties = useTransitionProperties(); return [// Box props to be spread onto label { background: isDisabled ? 'inputDisabled' : 'surface', cursor: isDisabled ? 'default' : 'pointer', padding: 'large', position: 'relative', userSelect: 'none' }, { // 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: theme.border.color.field, borderStyle: 'solid', borderWidth: theme.border.width.standard, borderRadius: theme.border.radius.small, 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: theme.border.color.primaryHover }, // Focus styles for card 'input[type=radio]:focus + [data-radio-border=true]': _objectSpread({ borderColor: theme.border.color.primary }, focusRingStyles), // 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 ? theme.border.color.field : theme.border.color.primary, borderWidth: theme.border.width.large } }]; } 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'], _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 /*#__PURE__*/jsxs(RadioGroupContext.Provider, { value: _objectSpread(_objectSpread({}, context), {}, { 'aria-describedby': message && messageId }), children: [/*#__PURE__*/jsx("div", { id: radioGroupId, role: "radiogroup", style: { display: 'contents' }, children: children }), message && /*#__PURE__*/jsx(FieldMessage, { tone: tone, id: messageId, message: message })] }); }; export { Radio, RadioCard, RadioGroup, RadioPrimitive };