UNPKG

@spark-web/button

Version:

--- title: Button storybookPath: forms-buttons-button--default isExperimentalPackage: true ---

493 lines (481 loc) 18 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var _objectSpread = require('@babel/runtime/helpers/objectSpread2'); var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties'); var box = require('@spark-web/box'); var utils = require('@spark-web/utils'); var react = require('react'); var jsxRuntime = require('react/jsx-runtime'); var _slicedToArray = require('@babel/runtime/helpers/slicedToArray'); var css = require('@emotion/css'); var a11y = require('@spark-web/a11y'); var spinner = require('@spark-web/spinner'); var text = require('@spark-web/text'); var theme = require('@spark-web/theme'); var link = require('@spark-web/link'); var ts = require('@spark-web/utils/ts'); var _excluded$2 = ["onClick", "disabled", "type"]; var BaseButton = /*#__PURE__*/react.forwardRef(function (_ref, forwardedRef) { var onClickProp = _ref.onClick, _ref$disabled = _ref.disabled, disabled = _ref$disabled === void 0 ? false : _ref$disabled, _ref$type = _ref.type, type = _ref$type === void 0 ? 'button' : _ref$type, consumerProps = _objectWithoutProperties(_ref, _excluded$2); var internalRef = react.useRef(null); var composedRef = utils.useComposedRefs(internalRef, forwardedRef); /** * In Safari buttons are not focused automatically by the browser once * pressed, the default behaviour is to focus the nearest focusable ancestor. * To fix this we need to manually focus the button element after the user * presses the element. */ var onClick = react.useCallback(function (event) { var _internalRef$current; (_internalRef$current = internalRef.current) === null || _internalRef$current === void 0 ? void 0 : _internalRef$current.focus(); var preventableClickHandler = getPreventableClickHandler(onClickProp, disabled); preventableClickHandler(event); }, [disabled, onClickProp]); return /*#__PURE__*/jsxRuntime.jsx(box.Box, _objectSpread(_objectSpread({}, consumerProps), {}, { as: "button", ref: composedRef // Hide aria-disabled attribute when button is not disabled , "aria-disabled": disabled || undefined, onClick: onClick, type: type })); }); BaseButton.displayName = 'BaseButton'; /** * handle "disabled" behaviour w/o disabling buttons * @see https://axesslab.com/disabled-buttons-suck/ */ function getPreventableClickHandler(onClick, disabled) { return function handleClick(event) { if (disabled) { event.preventDefault(); } else { onClick === null || onClick === void 0 ? void 0 : onClick(event); } }; } var highDisabledStyles = { backgroundDisabled: 'disabled', borderDisabled: 'fieldDisabled', textToneDisabled: 'neutralInverted' }; var highDisabledAltStyles = { backgroundDisabled: 'neutral', borderDisabled: 'standard', textToneDisabled: 'placeholder' }; var lowDisabledStyles = { backgroundDisabled: 'inputDisabled', textToneDisabled: 'disabled' }; var lowDisabledAltStyles = { backgroundDisabled: 'inputDisabled', borderDisabled: 'fieldDisabled', textToneDisabled: 'disabled' }; var noneDisabledStyles = { backgroundDisabled: 'neutral', textToneDisabled: 'disabled' }; var variants = { high: { primary: _objectSpread({ background: 'primary', backgroundHover: 'primaryHover', backgroundActive: 'primaryActive' }, highDisabledStyles), secondary: _objectSpread({ background: 'secondary', backgroundHover: 'secondaryHover', backgroundActive: 'secondaryActive' }, highDisabledStyles), neutral: _objectSpread({ background: 'neutral', border: 'field', backgroundHover: 'neutralHover', backgroundActive: 'neutralActive' }, highDisabledAltStyles), positive: _objectSpread({ background: 'positive', backgroundHover: 'positiveHover', backgroundActive: 'positiveActive' }, highDisabledStyles), critical: _objectSpread({ background: 'critical', backgroundHover: 'criticalHover', backgroundActive: 'criticalActive' }, highDisabledStyles), caution: undefined, info: undefined }, low: { primary: _objectSpread({ background: 'surface', border: 'primary', borderWidth: 'large', textTone: 'primary', backgroundHover: 'none', borderHover: 'primaryHover', textToneHover: 'primaryHover', backgroundActive: 'none', borderActive: 'primaryActive', textToneActive: 'primaryActive' }, lowDisabledAltStyles), secondary: _objectSpread({ background: 'surface', border: 'secondary', borderWidth: 'large', textTone: 'secondary', backgroundHover: 'none', borderHover: 'secondaryHover', textToneHover: 'secondaryHover', backgroundActive: 'none', borderActive: 'secondaryActive', textToneActive: 'secondaryActive' }, lowDisabledAltStyles), neutral: _objectSpread({ background: 'neutralLow', backgroundHover: 'neutralLowHover', backgroundActive: 'neutralLowActive' }, lowDisabledStyles), positive: _objectSpread({ background: 'positiveLow', backgroundHover: 'positiveLowHover', backgroundActive: 'positiveLowActive' }, lowDisabledStyles), caution: _objectSpread({ background: 'cautionLow', backgroundHover: 'cautionLowHover', backgroundActive: 'cautionLowActive' }, lowDisabledStyles), critical: _objectSpread({ background: 'criticalLow', backgroundHover: 'criticalLowHover', backgroundActive: 'criticalLowActive' }, lowDisabledStyles), info: _objectSpread({ background: 'infoLow', backgroundHover: 'infoLowHover', backgroundActive: 'infoLowActive' }, lowDisabledStyles) }, none: { primary: _objectSpread({ background: 'surface', textTone: 'primaryActive', backgroundHover: 'primaryLowHover', backgroundActive: 'primaryLowActive' }, noneDisabledStyles), secondary: _objectSpread({ background: 'surface', textTone: 'secondaryActive', backgroundHover: 'secondaryLowHover', backgroundActive: 'secondaryLowActive' }, noneDisabledStyles), neutral: _objectSpread({ background: 'surface', textTone: 'neutral', backgroundHover: 'neutralLowHover', backgroundActive: 'neutralLowActive' }, noneDisabledStyles), positive: _objectSpread({ background: 'surface', textTone: 'positive', backgroundHover: 'positiveLowHover', backgroundActive: 'positiveLowActive' }, noneDisabledStyles), caution: _objectSpread({ background: 'surface', textTone: 'caution', backgroundHover: 'cautionLowHover', backgroundActive: 'cautionLowActive' }, noneDisabledStyles), critical: _objectSpread({ background: 'surface', textTone: 'critical', backgroundHover: 'criticalLowHover', backgroundActive: 'criticalLowActive' }, noneDisabledStyles), info: _objectSpread({ background: 'surface', textTone: 'info', backgroundHover: 'infoLowHover', backgroundActive: 'infoLowActive' }, noneDisabledStyles) } }; var mapTokens = { fontSize: { medium: 'small', large: 'standard' }, size: { medium: 'medium', large: 'large' }, spacing: { medium: 'medium', large: 'xlarge' } }; var resolveButtonChildren = function resolveButtonChildren(_ref) { var children = _ref.children, isLoading = _ref.isLoading, prominence = _ref.prominence, size = _ref.size, tone = _ref.tone; var variant = variants[prominence][tone]; return react.Children.map(children, function (child) { if (typeof child === 'string' || typeof child === 'number') { return /*#__PURE__*/jsxRuntime.jsx(HiddenWhenLoading, { isLoading: isLoading, children: /*#__PURE__*/jsxRuntime.jsx(text.Text, { as: "span", baseline: false, overflowStrategy: "nowrap", weight: "semibold", size: mapTokens.fontSize[size], tone: variant === null || variant === void 0 ? void 0 : variant.textTone, children: child }) }); } if ( /*#__PURE__*/react.isValidElement(child)) { return /*#__PURE__*/jsxRuntime.jsx(HiddenWhenLoading, { isLoading: isLoading, children: /*#__PURE__*/react.cloneElement(child, { // Dismiss buttons need to be `xxsmall` // For everything else, we force them to be `xsmall` size: child.props.size === 'xxsmall' ? child.props.size : 'xsmall', // If the button is low prominence with a decorative tone we want to force // the tone to be the same as the button // We also don't want users to override the tone of the icon inside of the button tone: variant === null || variant === void 0 ? void 0 : variant.textTone }) }); } return null; }); }; function HiddenWhenLoading(_ref2) { var children = _ref2.children, isLoading = _ref2.isLoading; return /*#__PURE__*/jsxRuntime.jsx(box.Box, { as: "span", display: "inline-flex", alignItems: "center", justifyContent: "center", opacity: isLoading ? 0 : undefined, children: children }); } //////////////////////////////////////////////////////////////////////////////// /** * useButtonStyles * * Custom hook for styling buttons and certain links. * Returns a tuple where the first item is an object of props to spread onto the * underlying `Box` component, and the second item is a CSS object that can be * passed to Emotion's `css` function. */ function useButtonStyles(_ref) { var iconOnly = _ref.iconOnly, prominence = _ref.prominence, size = _ref.size, tone = _ref.tone; var theme$1 = theme.useTheme(); var focusRingStyles = a11y.useFocusRing({ tone: tone }); var disabledFocusRingStyles = a11y.useFocusRing({ tone: 'disabled' }); var variant = variants[prominence][tone]; var isLarge = size === 'large'; var transitionColors = { transitionProperty: 'color, background-color, border-color, text-decoration-color, fill, stroke, opacity, box-shadow, transform, filter, backdrop-filter', transitionTimingFunction: theme$1.animation.standard.easing, transitionDuration: "".concat(theme$1.animation.standard.duration, "ms") }; return [{ alignItems: 'center', background: variant === null || variant === void 0 ? void 0 : variant.background, border: variant === null || variant === void 0 ? void 0 : variant.border, borderWidth: variant === null || variant === void 0 ? void 0 : variant.borderWidth, borderRadius: isLarge ? 'medium' : 'small', cursor: 'pointer', display: 'inline-flex', gap: 'small', height: mapTokens.size[size], justifyContent: 'center', paddingX: iconOnly ? undefined : mapTokens.spacing[size], position: 'relative', width: iconOnly ? mapTokens.size[size] : undefined }, _objectSpread(_objectSpread({}, transitionColors), {}, { // Styles for buttons that aren't disabled. // Using the :not() pseudo-class so we don't have to undo the styles when // the button is disabled. '&:not([aria-disabled=true])': { ':hover': { borderColor: variant !== null && variant !== void 0 && variant.borderHover ? theme$1.border.color[variant.borderHover] : undefined, backgroundColor: variant !== null && variant !== void 0 && variant.backgroundHover ? theme$1.backgroundInteractions[variant.backgroundHover] : undefined, // Style button text when hovering '> *': _objectSpread(_objectSpread({}, transitionColors), {}, { color: variant !== null && variant !== void 0 && variant.textToneHover ? theme$1.color.foreground[variant.textToneHover] : undefined, stroke: variant !== null && variant !== void 0 && variant.textToneHover ? theme$1.color.foreground[variant.textToneHover] : undefined }) }, ':active': { borderColor: variant !== null && variant !== void 0 && variant.borderActive ? theme$1.border.color[variant.borderActive] : undefined, backgroundColor: variant !== null && variant !== void 0 && variant.backgroundActive ? theme$1.backgroundInteractions[variant === null || variant === void 0 ? void 0 : variant.backgroundActive] : undefined, transform: 'scale(0.98)', // Style button text when it's active '> *': _objectSpread(_objectSpread({}, transitionColors), {}, { color: variant !== null && variant !== void 0 && variant.textToneActive ? theme$1.color.foreground[variant.textToneActive] : undefined, stroke: variant !== null && variant !== void 0 && variant.textToneActive ? theme$1.color.foreground[variant.textToneActive] : undefined }) }, ':focus': focusRingStyles }, '&[aria-disabled=true]': { backgroundColor: variant !== null && variant !== void 0 && variant.backgroundDisabled ? theme$1.color.background[variant === null || variant === void 0 ? void 0 : variant.backgroundDisabled] : undefined, borderColor: variant !== null && variant !== void 0 && variant.borderDisabled ? theme$1.border.color[variant.borderDisabled] : undefined, cursor: 'default', '*': { color: variant !== null && variant !== void 0 && variant.textToneDisabled ? theme$1.color.foreground[variant.textToneDisabled] : undefined, stroke: variant !== null && variant !== void 0 && variant.textToneDisabled ? theme$1.color.foreground[variant.textToneDisabled] : undefined }, ':focus': disabledFocusRingStyles } })]; } var _excluded$1 = ["aria-controls", "aria-describedby", "aria-expanded", "data", "disabled", "id", "loading", "onClick", "prominence", "size", "tone", "type"]; /** * Buttons are used to initialize an action, their label should express what * action will occur when the user interacts with it. */ var Button = /*#__PURE__*/react.forwardRef(function (_ref, forwardedRef) { var ariaControls = _ref['aria-controls'], ariaDescribedBy = _ref['aria-describedby'], ariaExpanded = _ref['aria-expanded'], data = _ref.data, disabled = _ref.disabled, id = _ref.id, _ref$loading = _ref.loading, loading = _ref$loading === void 0 ? false : _ref$loading, onClick = _ref.onClick, _ref$prominence = _ref.prominence, prominence = _ref$prominence === void 0 ? 'high' : _ref$prominence, _ref$size = _ref.size, size = _ref$size === void 0 ? 'medium' : _ref$size, _ref$tone = _ref.tone, tone = _ref$tone === void 0 ? 'primary' : _ref$tone, type = _ref.type, props = _objectWithoutProperties(_ref, _excluded$1); var iconOnly = Boolean(props.label); var _useButtonStyles = useButtonStyles({ iconOnly: iconOnly, size: size, tone: tone, prominence: prominence }), _useButtonStyles2 = _slicedToArray(_useButtonStyles, 2), boxProps = _useButtonStyles2[0], buttonStyles = _useButtonStyles2[1]; var isDisabled = disabled || loading; var isLoading = loading && !disabled; var variant = variants[prominence][tone]; return /*#__PURE__*/jsxRuntime.jsxs(BaseButton, _objectSpread(_objectSpread({}, boxProps), {}, { "aria-controls": ariaControls, "aria-describedby": ariaDescribedBy, "aria-expanded": ariaExpanded, "aria-label": props.label, className: css.css(buttonStyles), data: data, disabled: isDisabled, id: id, onClick: onClick, ref: forwardedRef, type: type, children: [resolveButtonChildren(_objectSpread(_objectSpread({}, props), {}, { isLoading: isLoading, prominence: prominence, size: size, tone: tone })), isLoading && /*#__PURE__*/jsxRuntime.jsx(Loading, { tone: variant === null || variant === void 0 ? void 0 : variant.textTone })] })); }); Button.displayName = 'Button'; function Loading(_ref2) { var tone = _ref2.tone; return /*#__PURE__*/jsxRuntime.jsxs(box.Box, { as: "span", position: "absolute", top: 0, bottom: 0, left: 0, right: 0, display: "flex", alignItems: "center", justifyContent: "center", children: [/*#__PURE__*/jsxRuntime.jsx(a11y.VisuallyHidden, { children: "button loading indicator" }), /*#__PURE__*/jsxRuntime.jsx(spinner.Spinner, { size: "xsmall", tone: tone })] }); } var _excluded = ["data", "href", "id", "prominence", "size", "tone"]; /** The appearance of a `Button`, with the semantics of a link. */ var ButtonLink = ts.forwardRefWithAs(function (_ref, forwardedRef) { var data = _ref.data, href = _ref.href, id = _ref.id, _ref$prominence = _ref.prominence, prominence = _ref$prominence === void 0 ? 'high' : _ref$prominence, _ref$size = _ref.size, size = _ref$size === void 0 ? 'medium' : _ref$size, _ref$tone = _ref.tone, tone = _ref$tone === void 0 ? 'primary' : _ref$tone, consumerProps = _objectWithoutProperties(_ref, _excluded); var LinkComponent = link.useLinkComponent(forwardedRef); var iconOnly = Boolean(consumerProps.label); var _useButtonStyles = useButtonStyles({ iconOnly: iconOnly, prominence: prominence, size: size, tone: tone }), _useButtonStyles2 = _slicedToArray(_useButtonStyles, 2), boxProps = _useButtonStyles2[0], buttonStyles = _useButtonStyles2[1]; return /*#__PURE__*/jsxRuntime.jsx(box.Box, _objectSpread(_objectSpread({}, boxProps), {}, { "aria-label": consumerProps.label, as: LinkComponent, asElement: "a", className: css.css(buttonStyles), data: data, href: href, id: id, ref: forwardedRef, children: resolveButtonChildren(_objectSpread(_objectSpread({}, consumerProps), {}, { isLoading: false, prominence: prominence, size: size, tone: tone })) })); }); exports.BaseButton = BaseButton; exports.Button = Button; exports.ButtonLink = ButtonLink; exports.useButtonStyles = useButtonStyles;