@atlaskit/button
Version:
A button triggers an event or action. They let users know what will happen next.
205 lines (196 loc) • 7.6 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
/* eslint-disable @atlaskit/design-system/consistent-css-prop-usage */
/**
* @jsxRuntime classic
* @jsx jsx
*/
import React, { useCallback, useContext, useEffect, useRef } from 'react';
// eslint-disable-next-line @atlaskit/ui-styling-standard/use-compiled -- Ignored via go/DSP-18766
import { css, jsx } from '@emotion/react';
import { usePlatformLeafEventHandler } from '@atlaskit/analytics-next';
import noop from '@atlaskit/ds-lib/noop';
import useAutoFocus from '@atlaskit/ds-lib/use-auto-focus';
import FocusRing from '@atlaskit/focus-ring';
// eslint-disable-next-line no-duplicate-imports
import InteractionContext from '@atlaskit/interaction-context';
import { N500 } from '@atlaskit/theme/colors';
import blockEvents from './block-events';
import { getContentStyle, getFadingCss, getIconStyle, overlayCss } from './css';
import { getIfVisuallyHiddenChildren } from './get-if-visually-hidden-children';
// Disabled buttons will still publish events for nested elements in webkit.
// We are disabling pointer events on child elements so that
// the button will always be the target of events
// Note: firefox does not have this behaviour for child elements
const noPointerEventsOnChildrenCss = {
'> *': {
pointerEvents: 'none'
}
};
/**
* These CSS variables consumed by the new icons, to allow them to have appropriate
* padding inside Button while also maintaining spacing for the existing icons.
*
* These styles can be removed once the new icons are fully rolled out, feature flag
* platform-visual-refresh-icons is cleaned up,
* and we bump Button to set padding based on the new icons.
*/
const iconBeforeSpacingFixStyle = css({
'--ds--button--new-icon-padding-end': "var(--ds-space-025, 2px)",
'--ds--button--new-icon-padding-start': "var(--ds-space-050, 4px)",
marginInlineStart: "var(--ds-space-negative-025, -2px)"
});
const iconAfterSpacingFixStyle = css({
'--ds--button--new-icon-padding-end': "var(--ds-space-050, 4px)",
'--ds--button--new-icon-padding-start': "var(--ds-space-025, 2px)",
marginInlineEnd: "var(--ds-space-negative-025, -2px)"
});
const getSpacingFix = (children, spacingStyles) => {
if (!children || getIfVisuallyHiddenChildren(children)) {
return null;
}
return spacingStyles;
};
const getChildren = (children, childrenStyles) => {
if (getIfVisuallyHiddenChildren(children)) {
return children;
}
return children ? jsx("span", {
css: childrenStyles
}, children) : null;
};
const ButtonBase = /*#__PURE__*/React.forwardRef(function ButtonBase(props, ref) {
const {
// I don't think analytics should be in button, but for now it is
analyticsContext,
appearance = 'default',
autoFocus = false,
buttonCss,
children,
className,
href,
// use the provided component prop,
// else default to anchor if there is a href, and button if there is no href
component: Component = href ? 'a' : 'button',
iconAfter,
iconBefore,
interactionName,
isDisabled = false,
isSelected = false,
onBlur,
onClick: providedOnClick = noop,
onFocus,
onMouseDown: providedOnMouseDown = noop,
overlay,
// Pulling out so it doesn't spread on rendered component
shouldFitContainer,
spacing = 'default',
tabIndex = 0,
type = !href ? 'button' : undefined,
testId,
...rest
} = props;
const ourRef = useRef();
const setRef = useCallback(node => {
ourRef.current = node;
if (ref == null) {
return;
}
if (typeof ref === 'function') {
ref(node);
return;
}
// We can write to ref's `current` property, but Typescript does not like it.
// @ts-ignore
ref.current = node;
}, [ourRef, ref]);
// Cross browser auto focusing is pretty broken, so we are doing it ourselves
useAutoFocus(ourRef, autoFocus);
const interactionContext = useContext(InteractionContext);
const handleClick = useCallback((e, analyticsEvent) => {
interactionContext && interactionContext.tracePress(interactionName, e.timeStamp);
providedOnClick(e, analyticsEvent);
}, [providedOnClick, interactionContext, interactionName]);
const onClick = usePlatformLeafEventHandler({
fn: handleClick,
action: 'clicked',
componentName: 'button',
packageName: "@atlaskit/button",
packageVersion: "0.0.0-development",
analyticsData: analyticsContext
});
// Button currently calls preventDefault, which is not standard button behaviour
const onMouseDown = useCallback(event => {
event.preventDefault();
providedOnMouseDown(event);
}, [providedOnMouseDown]);
// Lose focus when becoming disabled (standard button behaviour)
useEffect(() => {
const el = ourRef.current;
if (isDisabled && el && el === document.activeElement) {
el.blur();
}
}, [isDisabled]);
// we are 'disabling' input with a button when there is an overlay
const hasOverlay = Boolean(overlay);
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values, @atlaskit/ui-styling-standard/no-unsafe-values -- Ignored via go/DSP-18766
const fadeStyles = css(getFadingCss({
hasOverlay
}));
const isInteractive = !isDisabled && !hasOverlay;
/**
* HACK: Spinner needs to have different colours in the "new" tokens design compared to the old design.
* For now, while we support both, these styles reach into Spinner when a theme is set, applies the right color.
* Ticket to remove: https://product-fabric.atlassian.net/browse/DSP-2067.
*/
var spinnerHackCss = {};
if (isSelected || isDisabled || appearance === 'warning') {
spinnerHackCss = {
'[data-theme] & circle': {
stroke: `${isSelected || isDisabled ? `var(--ds-icon-subtle, ${N500})` : `var(--ds-icon-warning-inverse, ${N500})`} !important`
}
};
}
return jsx(FocusRing, null, jsx(Component, _extends({}, rest, {
ref: setRef
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-classname-prop -- Ignored via go/DSP-18766
,
className: className,
css: [buttonCss, isInteractive ? null : noPointerEventsOnChildrenCss]
// using undefined so that the property doesn't exist when false
,
"data-has-overlay": hasOverlay ? true : undefined,
"data-testid": testId,
disabled: isDisabled,
href: isInteractive ? href : undefined,
onBlur: onBlur,
onClick: onClick,
onFocus: onFocus,
onMouseDown: onMouseDown
// Adding a tab index so element is always focusable, even when not a <button> or <a>
// Disabling focus via keyboard navigation when disabled
// as this is standard button behaviour
,
tabIndex: isDisabled ? -1 : tabIndex,
type: type
}, blockEvents({
isInteractive
})), iconBefore ? jsx("span", {
css: [fadeStyles,
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
getIconStyle({
spacing
}), getSpacingFix(children, iconBeforeSpacingFixStyle)]
}, iconBefore) : null, getChildren(children, [fadeStyles, getContentStyle({
spacing
})]), iconAfter ? jsx("span", {
css: [fadeStyles,
// eslint-disable-next-line @atlaskit/ui-styling-standard/no-imported-style-values -- Ignored via go/DSP-18766
getIconStyle({
spacing
}), getSpacingFix(children, iconAfterSpacingFixStyle)]
}, iconAfter) : null, overlay ? jsx("span", {
css: [overlayCss, spinnerHackCss]
}, overlay) : null));
});
// eslint-disable-next-line @repo/internal/react/require-jsdoc
export default ButtonBase;