@spark-web/button
Version:
--- title: Button storybookPath: forms-buttons-button--default isExperimentalPackage: true ---
556 lines (546 loc) • 20.9 kB
JavaScript
import _objectSpread from '@babel/runtime/helpers/esm/objectSpread2';
import _objectWithoutProperties from '@babel/runtime/helpers/esm/objectWithoutProperties';
import { Box } from '@spark-web/box';
import { useComposedRefs } from '@spark-web/utils';
import { forwardRef, useRef, useCallback, Children, isValidElement, cloneElement } from 'react';
import { jsx, jsxs } from '@emotion/react/jsx-runtime';
import _slicedToArray from '@babel/runtime/helpers/esm/slicedToArray';
import { css } from '@emotion/react';
import { Spinner } from '@spark-web/spinner';
import { Text } from '@spark-web/text';
import { useFocusRing } from '@spark-web/a11y';
import { useTheme } from '@spark-web/theme';
import { useLinkComponent } from '@spark-web/link';
import { forwardRefWithAs } from '@spark-web/utils/ts';
var _excluded$2 = ["onClick", "disabled", "type"];
var BaseButton = /*#__PURE__*/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 = useRef(null);
var composedRef = 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 = useCallback(function (event) {
var _internalRef$current;
(_internalRef$current = internalRef.current) === null || _internalRef$current === void 0 || _internalRef$current.focus();
var preventableClickHandler = getPreventableClickHandler(onClickProp, disabled);
preventableClickHandler(event);
}, [disabled, onClickProp]);
return jsx(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 || 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',
textTone: 'dark'
}, highDisabledStyles),
secondary: _objectSpread({
background: 'secondary',
backgroundHover: 'secondaryHover',
backgroundActive: 'secondaryActive'
}, highDisabledStyles),
neutral: _objectSpread({
background: 'neutral',
border: 'neutral',
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,
primaryDark: _objectSpread({
background: 'primaryDark',
backgroundHover: 'primaryHover',
backgroundActive: 'primaryActive'
}, highDisabledStyles),
dark: _objectSpread({
background: 'dark',
backgroundHover: 'darkHover',
backgroundActive: 'darkActive'
}, highDisabledStyles)
},
low: {
primary: _objectSpread({
background: 'surface',
backgroundFill: 'primarySoft',
border: 'primary',
borderWidth: 'large',
textTone: 'dark',
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),
primaryDark: _objectSpread({
background: 'surface',
border: 'primaryActive',
borderWidth: 'large',
textTone: 'primaryActive',
backgroundHover: 'none',
borderHover: 'primaryHover',
textToneHover: 'primaryHover',
backgroundActive: 'none',
borderActive: 'primaryActive',
textToneActive: 'primaryActive'
}, lowDisabledAltStyles),
dark: _objectSpread({
background: 'surface',
backgroundHover: 'none',
backgroundActive: 'none',
textTone: 'dark',
borderWidth: 'large',
border: 'dark',
borderHover: 'darkHover',
borderActive: 'darkActive'
}, lowDisabledAltStyles)
},
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),
primaryDark: _objectSpread({
background: 'surface',
textTone: 'primaryActive',
backgroundHover: 'primaryLowHover',
backgroundActive: 'primaryLowActive'
}, noneDisabledStyles),
dark: _objectSpread({
background: 'surface',
textTone: 'dark',
backgroundHover: 'darkLowHover',
backgroundActive: 'darkLowActive'
}, 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,
weight = _ref.weight;
var variant = variants[prominence][tone];
return Children.map(children, function (child) {
if (typeof child === 'string' || typeof child === 'number') {
return jsx(HiddenWhenLoading, {
isLoading: isLoading,
children: jsx(Text, {
as: "span",
baseline: false,
overflowStrategy: "nowrap",
weight: weight !== null && weight !== void 0 ? weight : 'semibold',
size: mapTokens.fontSize[size],
tone: variant === null || variant === void 0 ? void 0 : variant.textTone,
children: child
})
});
}
if (/*#__PURE__*/isValidElement(child)) {
return jsx(HiddenWhenLoading, {
isLoading: isLoading,
children: /*#__PURE__*/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 jsx(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 _variant$backgroundFi, _theme$components$but, _theme$components, _theme$components2;
var iconOnly = _ref.iconOnly,
prominence = _ref.prominence,
_ref$rounded = _ref.rounded,
rounded = _ref$rounded === void 0 ? false : _ref$rounded,
size = _ref.size,
tone = _ref.tone,
_ref$filled = _ref.filled,
filled = _ref$filled === void 0 ? false : _ref$filled;
var theme = useTheme();
var focusRingStyles = useFocusRing({
tone: tone,
always: true
});
var disabledFocusRingStyles = 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.animation.standard.easing,
transitionDuration: "".concat(theme.animation.standard.duration, "ms")
};
return [{
alignItems: 'center',
background: filled ? (_variant$backgroundFi = variant === null || variant === void 0 ? void 0 : variant.backgroundFill) !== null && _variant$backgroundFi !== void 0 ? _variant$backgroundFi : variant === null || variant === void 0 ? void 0 : variant.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: rounded ? 'full' : 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.border.color[variant.borderHover] : undefined,
backgroundColor: variant !== null && variant !== void 0 && variant.backgroundHover ? theme.backgroundInteractions[variant.backgroundHover] : undefined,
// Style button text when hovering
'> *': _objectSpread(_objectSpread({}, transitionColors), {}, {
color: variant !== null && variant !== void 0 && variant.textToneHover ? theme.color.foreground[variant.textToneHover] : undefined,
stroke: variant !== null && variant !== void 0 && variant.textToneHover ? theme.color.foreground[variant.textToneHover] : undefined
})
},
':active': {
borderColor: variant !== null && variant !== void 0 && variant.borderActive ? theme.border.color[variant.borderActive] : undefined,
backgroundColor: variant !== null && variant !== void 0 && variant.backgroundActive ? theme.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.color.foreground[variant.textToneActive] : undefined,
stroke: variant !== null && variant !== void 0 && variant.textToneActive ? theme.color.foreground[variant.textToneActive] : undefined
})
},
':focus': (_theme$components$but = (_theme$components = theme.components) === null || _theme$components === void 0 || (_theme$components = _theme$components.buttons) === null || _theme$components === void 0 ? void 0 : _theme$components.focus) !== null && _theme$components$but !== void 0 ? _theme$components$but : focusRingStyles
},
'&[aria-disabled=true]': {
backgroundColor: variant !== null && variant !== void 0 && variant.backgroundDisabled ? theme.color.background[variant === null || variant === void 0 ? void 0 : variant.backgroundDisabled] : undefined,
borderColor: variant !== null && variant !== void 0 && variant.borderDisabled ? theme.border.color[variant.borderDisabled] : undefined,
cursor: 'default',
'*': {
color: variant !== null && variant !== void 0 && variant.textToneDisabled ? theme.color.foreground[variant.textToneDisabled] : undefined,
stroke: variant !== null && variant !== void 0 && variant.textToneDisabled ? theme.color.foreground[variant.textToneDisabled] : undefined
},
':focus': disabledFocusRingStyles
}
}), {
fontWeight: (_theme$components2 = theme.components) === null || _theme$components2 === void 0 || (_theme$components2 = _theme$components2.buttons) === null || _theme$components2 === void 0 ? void 0 : _theme$components2.fontWeight
}];
}
var _excluded$1 = ["aria-controls", "aria-describedby", "aria-expanded", "aria-label", "data", "disabled", "id", "loading", "onClick", "css", "rounded", "prominence", "size", "tone", "type", "filled"];
/**
* Buttons are used to initialize an action, their label should express what
* action will occur when the user interacts with it.
*/
var Button = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) {
var ariaControls = _ref['aria-controls'],
ariaDescribedBy = _ref['aria-describedby'],
ariaExpanded = _ref['aria-expanded'],
ariaLabel = _ref['aria-label'],
data = _ref.data,
disabled = _ref.disabled,
id = _ref.id,
_ref$loading = _ref.loading,
loading = _ref$loading === void 0 ? false : _ref$loading,
onClick = _ref.onClick,
customCss = _ref.css,
_ref$rounded = _ref.rounded,
rounded = _ref$rounded === void 0 ? false : _ref$rounded,
_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,
_ref$filled = _ref.filled,
filled = _ref$filled === void 0 ? false : _ref$filled,
props = _objectWithoutProperties(_ref, _excluded$1);
var iconOnly = Boolean(props.label);
var _useButtonStyles = useButtonStyles({
iconOnly: iconOnly,
rounded: rounded,
size: size,
tone: tone,
prominence: prominence,
filled: filled
}),
_useButtonStyles2 = _slicedToArray(_useButtonStyles, 3),
boxProps = _useButtonStyles2[0],
buttonStyles = _useButtonStyles2[1],
fontWeight = _useButtonStyles2[2].fontWeight;
var variant = variants[prominence][tone];
return jsxs(BaseButton, _objectSpread(_objectSpread({}, boxProps), {}, {
"aria-controls": ariaControls,
"aria-describedby": ariaDescribedBy,
"aria-expanded": ariaExpanded,
"aria-label": ariaLabel !== null && ariaLabel !== void 0 ? ariaLabel : props.label,
css: css(buttonStyles, css(customCss)),
data: data,
disabled: loading || disabled,
id: id,
onClick: onClick,
ref: forwardedRef,
type: type,
children: [resolveButtonChildren(_objectSpread(_objectSpread({}, props), {}, {
isLoading: loading,
prominence: prominence,
size: size,
tone: tone,
weight: fontWeight
})), loading && jsx(Loading, {
tone: variant === null || variant === void 0 ? void 0 : variant.textTone
})]
}));
});
Button.displayName = 'Button';
function Loading(_ref2) {
var tone = _ref2.tone;
return jsx(Box, {
as: "span",
position: "absolute",
top: 0,
bottom: 0,
left: 0,
right: 0,
display: "flex",
alignItems: "center",
justifyContent: "center",
children: jsx(Spinner, {
size: "xsmall",
tone: tone
})
});
}
var _excluded = ["data", "href", "target", "id", "css", "prominence", "rounded", "size", "tone"];
/** The appearance of a `Button`, with the semantics of a link. */
var ButtonLink = forwardRefWithAs(function (_ref, forwardedRef) {
var data = _ref.data,
href = _ref.href,
target = _ref.target,
id = _ref.id,
customCss = _ref.css,
_ref$prominence = _ref.prominence,
prominence = _ref$prominence === void 0 ? 'high' : _ref$prominence,
_ref$rounded = _ref.rounded,
rounded = _ref$rounded === void 0 ? false : _ref$rounded,
_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 = useLinkComponent(forwardedRef);
var iconOnly = Boolean(consumerProps.label);
var _useButtonStyles = useButtonStyles({
iconOnly: iconOnly,
prominence: prominence,
rounded: rounded,
size: size,
tone: tone
}),
_useButtonStyles2 = _slicedToArray(_useButtonStyles, 3),
boxProps = _useButtonStyles2[0],
buttonStyles = _useButtonStyles2[1],
fontWeight = _useButtonStyles2[2].fontWeight;
return jsx(Box, _objectSpread(_objectSpread(_objectSpread({}, boxProps), {}, {
"aria-label": consumerProps.label,
as: LinkComponent,
asElement: "a",
css: css(buttonStyles, customCss),
data: data,
href: href,
id: id,
target: target,
ref: forwardedRef
}, consumerProps), {}, {
children: resolveButtonChildren(_objectSpread(_objectSpread({}, consumerProps), {}, {
isLoading: false,
prominence: prominence,
size: size,
tone: tone,
weight: fontWeight
}))
}));
});
export { BaseButton, Button, ButtonLink, useButtonStyles };