@spark-web/button 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -196,25 +196,17 @@ with the exception of `href` vs `onClick` props.
196
196
  </Text>
197
197
  ```
198
198
 
199
- ## Props
200
-
201
- | Prop | Type | Default | Description |
202
- | ----------------- | ---------------------------------------------------------------------------------------- | --------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
203
- | aria-controls? | string | | Identifies the element (or elements) whose contents or presence are controlled by the current element. Only applicable for `Button`. |
204
- | aria-describedby? | string | | Identifies the element (or elements) that describes the object. Only applicable for `Button`. |
205
- | aria-expanded? | string | | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. Only applicable for `Button`. |
206
- | children | string \| React.ReactElement\<IconProps> | | Children element to be rendered inside the button. |
207
- | data? | [DataAttributeMap][data-attribute-map] | | Allows setting of data attributes on the button. |
208
- | disabled? | boolean | | When true, prevents `onClick` from firing. Only applicable for `Button`. |
209
- | href | string | | Specifies the url the button should redirect to upon being clicked. Only applicable for `ButtonLink`. |
210
- | id? | string | | Unique identifier for the button. |
211
- | label? | string | | Implicit label for buttons only required for icon-only buttons for accessibility reasons. |
212
- | loading? | boolean | | When true, the button will display a loading spinner. |
213
- | onClick? | Function | | Function to be fired following a click event of the button. Only applicable for `Button`. |
214
- | prominence? | 'high' \| 'low' | 'high' | Sets the visual prominence of the button. |
215
- | size? | 'medium' \| 'large' | 'medium' | Sets the size of the button. |
216
- | tone? | 'primary' \| 'secondary' \| 'neutral' \| 'positive' \| 'caution' \| 'critical' \| 'info' | 'primary' | Sets the tone of the button. |
217
- | type? | 'button' \| 'submit' \| 'reset' | 'button' | Sets the button type. Only applicable for `Button`. |
218
-
219
- [data-attribute-map]:
220
- https://github.com/brighte-labs/spark-web/blob/e7f6f4285b4cfd876312cc89fbdd094039aa239a/packages/utils/src/internal/buildDataAttributes.ts#L1
199
+ ## BaseButton
200
+
201
+ Unstyled button primitive that:
202
+
203
+ - Forwards the button ref
204
+ - Provides a default type of `button` (so it doesn't accidently submit forms if
205
+ left off)
206
+ - Prevents `onClick` from firing when disabled without disabling the button
207
+ - Forces focus of the underlying button when clicked (to address a bug in
208
+ Safari)
209
+
210
+ ## Button Props
211
+
212
+ <PropsTable displayName="Button" />
@@ -0,0 +1,10 @@
1
+ import type { BoxProps } from '@spark-web/box';
2
+ import type { MouseEvent as ReactMouseEvent } from 'react';
3
+ import type { NativeButtonProps } from './types';
4
+ export declare type BaseButtonProps = NativeButtonProps & Partial<BoxProps>;
5
+ export declare const BaseButton: import("react").ForwardRefExoticComponent<NativeButtonProps & Partial<BoxProps> & import("react").RefAttributes<HTMLButtonElement>>;
6
+ /**
7
+ * handle "disabled" behaviour w/o disabling buttons
8
+ * @see https://axesslab.com/disabled-buttons-suck/
9
+ */
10
+ export declare function getPreventableClickHandler(onClick: BaseButtonProps['onClick'], disabled: boolean): (event: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
@@ -1,4 +1,4 @@
1
- import type { MouseEvent as ReactMouseEvent } from 'react';
1
+ /// <reference types="react" />
2
2
  import type { CommonButtonProps, NativeButtonProps } from './types';
3
3
  export declare type ButtonProps = CommonButtonProps & {
4
4
  /**
@@ -26,9 +26,3 @@ export declare type ButtonProps = CommonButtonProps & {
26
26
  * action will occur when the user interacts with it.
27
27
  */
28
28
  export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
29
- /**
30
- * Prevent click events when the component is "disabled".
31
- * Note: we don't want to actually disable a button element for several reasons.
32
- * One being because that would prohibit the use of tooltips.
33
- */
34
- export declare function getPreventableClickHandler(onClick: NativeButtonProps['onClick'], disabled: boolean): (event: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
@@ -5,5 +5,5 @@ export declare type ButtonLinkProps = LinkComponentProps & CommonButtonProps;
5
5
  /** The appearance of a `Button`, with the semantics of a link. */
6
6
  export declare const ButtonLink: <Comp extends import("react").ElementType<any> = "a">(props: {
7
7
  as?: Comp | undefined;
8
- ref?: import("react").Ref<Comp extends "symbol" | "clipPath" | "filter" | "mask" | "marker" | "text" | "circle" | "svg" | "animate" | "animateMotion" | "animateTransform" | "defs" | "desc" | "ellipse" | "feBlend" | "feColorMatrix" | "feComponentTransfer" | "feComposite" | "feConvolveMatrix" | "feDiffuseLighting" | "feDisplacementMap" | "feDistantLight" | "feDropShadow" | "feFlood" | "feFuncA" | "feFuncB" | "feFuncG" | "feFuncR" | "feGaussianBlur" | "feImage" | "feMerge" | "feMergeNode" | "feMorphology" | "feOffset" | "fePointLight" | "feSpecularLighting" | "feSpotLight" | "feTile" | "feTurbulence" | "foreignObject" | "g" | "image" | "line" | "linearGradient" | "metadata" | "mpath" | "path" | "pattern" | "polygon" | "polyline" | "radialGradient" | "rect" | "stop" | "switch" | "textPath" | "tspan" | "use" | "view" | keyof HTMLElementTagNameMap | "set" ? (HTMLElementTagNameMap & Pick<SVGElementTagNameMap, "symbol" | "clipPath" | "filter" | "mask" | "marker" | "text" | "circle" | "svg" | "animate" | "animateMotion" | "animateTransform" | "defs" | "desc" | "ellipse" | "feBlend" | "feColorMatrix" | "feComponentTransfer" | "feComposite" | "feConvolveMatrix" | "feDiffuseLighting" | "feDisplacementMap" | "feDistantLight" | "feDropShadow" | "feFlood" | "feFuncA" | "feFuncB" | "feFuncG" | "feFuncR" | "feGaussianBlur" | "feImage" | "feMerge" | "feMergeNode" | "feMorphology" | "feOffset" | "fePointLight" | "feSpecularLighting" | "feSpotLight" | "feTile" | "feTurbulence" | "foreignObject" | "g" | "image" | "line" | "linearGradient" | "metadata" | "mpath" | "path" | "pattern" | "polygon" | "polyline" | "radialGradient" | "rect" | "stop" | "switch" | "textPath" | "tspan" | "use" | "view" | "set">)[Comp] : Comp extends new (...args: any) => any ? InstanceType<Comp> : undefined> | undefined;
8
+ ref?: import("react").Ref<Comp extends "symbol" | "clipPath" | "filter" | "mask" | "marker" | "svg" | "animate" | "animateMotion" | "animateTransform" | "circle" | "defs" | "desc" | "ellipse" | "feBlend" | "feColorMatrix" | "feComponentTransfer" | "feComposite" | "feConvolveMatrix" | "feDiffuseLighting" | "feDisplacementMap" | "feDistantLight" | "feDropShadow" | "feFlood" | "feFuncA" | "feFuncB" | "feFuncG" | "feFuncR" | "feGaussianBlur" | "feImage" | "feMerge" | "feMergeNode" | "feMorphology" | "feOffset" | "fePointLight" | "feSpecularLighting" | "feSpotLight" | "feTile" | "feTurbulence" | "foreignObject" | "g" | "image" | "line" | "linearGradient" | "metadata" | "mpath" | "path" | "pattern" | "polygon" | "polyline" | "radialGradient" | "rect" | "stop" | "switch" | "text" | "textPath" | "tspan" | "use" | "view" | keyof HTMLElementTagNameMap | "set" ? (HTMLElementTagNameMap & Pick<SVGElementTagNameMap, "symbol" | "clipPath" | "filter" | "mask" | "marker" | "svg" | "animate" | "animateMotion" | "animateTransform" | "circle" | "defs" | "desc" | "ellipse" | "feBlend" | "feColorMatrix" | "feComponentTransfer" | "feComposite" | "feConvolveMatrix" | "feDiffuseLighting" | "feDisplacementMap" | "feDistantLight" | "feDropShadow" | "feFlood" | "feFuncA" | "feFuncB" | "feFuncG" | "feFuncR" | "feGaussianBlur" | "feImage" | "feMerge" | "feMergeNode" | "feMorphology" | "feOffset" | "fePointLight" | "feSpecularLighting" | "feSpotLight" | "feTile" | "feTurbulence" | "foreignObject" | "g" | "image" | "line" | "linearGradient" | "metadata" | "mpath" | "path" | "pattern" | "polygon" | "polyline" | "radialGradient" | "rect" | "stop" | "switch" | "text" | "textPath" | "tspan" | "use" | "view" | "set">)[Comp] : Comp extends new (...args: any) => any ? InstanceType<Comp> : undefined> | undefined;
9
9
  } & Omit<import("react").PropsWithoutRef<import("react").ComponentProps<Comp>>, "as"> & ButtonLinkProps) => import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
@@ -1,4 +1,6 @@
1
+ export { BaseButton } from './BaseButton';
1
2
  export { Button } from './Button';
2
3
  export { ButtonLink } from './ButtonLink';
4
+ export type { BaseButtonProps } from './BaseButton';
3
5
  export type { ButtonProps } from './Button';
4
6
  export type { ButtonLinkProps } from './ButtonLink';
@@ -1,3 +1,4 @@
1
+ /// <reference types="react" />
1
2
  import type { ButtonChildrenProps, ButtonProminence, ButtonSize, ButtonTone } from './types';
2
3
  declare type ResolveButtonChildren = ButtonChildrenProps & {
3
4
  isLoading: boolean;
@@ -4,18 +4,68 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var _objectSpread = require('@babel/runtime/helpers/objectSpread2');
6
6
  var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties');
7
- var a11y = require('@spark-web/a11y');
8
7
  var box = require('@spark-web/box');
9
- var spinner = require('@spark-web/spinner');
10
- var internal = require('@spark-web/utils/internal');
8
+ var utils = require('@spark-web/utils');
11
9
  var react = require('react');
12
- var css = require('@emotion/css');
13
- var text = require('@spark-web/text');
14
10
  var jsxRuntime = require('react/jsx-runtime');
11
+ var a11y = require('@spark-web/a11y');
12
+ var spinner = require('@spark-web/spinner');
13
+ var text = require('@spark-web/text');
14
+ var css = require('@emotion/css');
15
15
  var theme = require('@spark-web/theme');
16
16
  var link = require('@spark-web/link');
17
+ var internal = require('@spark-web/utils/internal');
17
18
  var ts = require('@spark-web/utils/ts');
18
19
 
20
+ var _excluded$2 = ["onClick", "disabled", "type"];
21
+ var BaseButton = /*#__PURE__*/react.forwardRef(function (_ref, forwardedRef) {
22
+ var onClickProp = _ref.onClick,
23
+ _ref$disabled = _ref.disabled,
24
+ disabled = _ref$disabled === void 0 ? false : _ref$disabled,
25
+ _ref$type = _ref.type,
26
+ type = _ref$type === void 0 ? 'button' : _ref$type,
27
+ rest = _objectWithoutProperties(_ref, _excluded$2);
28
+
29
+ var internalRef = react.useRef(null);
30
+ var composedRef = utils.useComposedRefs(internalRef, forwardedRef);
31
+ /**
32
+ * In Safari buttons are not focused automatically by the browser once
33
+ * pressed, the default behaviour is to focus the nearest focusable ancestor.
34
+ * To fix this we need to manually focus the button element after the user
35
+ * presses the element.
36
+ */
37
+
38
+ var onClick = react.useCallback(function (event) {
39
+ var _internalRef$current;
40
+
41
+ (_internalRef$current = internalRef.current) === null || _internalRef$current === void 0 ? void 0 : _internalRef$current.focus();
42
+ var preventableClickHandler = getPreventableClickHandler(onClickProp, disabled);
43
+ preventableClickHandler(event);
44
+ }, [disabled, onClickProp]);
45
+ return /*#__PURE__*/jsxRuntime.jsx(box.Box, _objectSpread(_objectSpread({
46
+ as: "button"
47
+ }, rest), {}, {
48
+ onClick: onClick,
49
+ ref: composedRef,
50
+ type: type
51
+ }));
52
+ });
53
+ BaseButton.displayName = 'BaseButton';
54
+ /**
55
+ * handle "disabled" behaviour w/o disabling buttons
56
+ * @see https://axesslab.com/disabled-buttons-suck/
57
+ */
58
+
59
+ function getPreventableClickHandler(onClick, disabled) {
60
+ return function handleClick(event) {
61
+ if (disabled) {
62
+ event.preventDefault();
63
+ } else {
64
+ onClick === null || onClick === void 0 ? void 0 : onClick(event);
65
+ }
66
+ };
67
+ }
68
+
19
69
  var variants = {
20
70
  high: {
21
71
  primary: {
@@ -203,10 +253,12 @@ var resolveButtonChildren = function resolveButtonChildren(_ref) {
203
253
  function HiddenWhenLoading(_ref2) {
204
254
  var children = _ref2.children,
205
255
  isLoading = _ref2.isLoading;
206
- return /*#__PURE__*/jsxRuntime.jsx("span", {
207
- className: isLoading ? css.css({
208
- opacity: 0
209
- }) : undefined,
256
+ return /*#__PURE__*/jsxRuntime.jsx(box.Box, {
257
+ as: "span",
258
+ display: "inline-flex",
259
+ alignItems: "center",
260
+ justifyContent: "center",
261
+ opacity: isLoading ? 0 : undefined,
210
262
  children: children
211
263
  });
212
264
  }
@@ -274,13 +326,12 @@ var _excluded$1 = ["aria-controls", "aria-describedby", "aria-expanded", "data",
274
326
  * Buttons are used to initialize an action, their label should express what
275
327
  * action will occur when the user interacts with it.
276
328
  */
277
- var Button = /*#__PURE__*/react.forwardRef(function (_ref, ref) {
329
+ var Button = /*#__PURE__*/react.forwardRef(function (_ref, forwardedRef) {
278
330
  var ariaControls = _ref['aria-controls'],
279
331
  ariaDescribedBy = _ref['aria-describedby'],
280
332
  ariaExpanded = _ref['aria-expanded'],
281
333
  data = _ref.data,
282
- _ref$disabled = _ref.disabled,
283
- disabled = _ref$disabled === void 0 ? false : _ref$disabled,
334
+ disabled = _ref.disabled,
284
335
  id = _ref.id,
285
336
  _ref$loading = _ref.loading,
286
337
  loading = _ref$loading === void 0 ? false : _ref$loading,
@@ -291,8 +342,7 @@ var Button = /*#__PURE__*/react.forwardRef(function (_ref, ref) {
291
342
  size = _ref$size === void 0 ? 'medium' : _ref$size,
292
343
  _ref$tone = _ref.tone,
293
344
  tone = _ref$tone === void 0 ? 'primary' : _ref$tone,
294
- _ref$type = _ref.type,
295
- type = _ref$type === void 0 ? 'button' : _ref$type,
345
+ type = _ref.type,
296
346
  props = _objectWithoutProperties(_ref, _excluded$1);
297
347
 
298
348
  var iconOnly = Boolean(props.label);
@@ -305,24 +355,18 @@ var Button = /*#__PURE__*/react.forwardRef(function (_ref, ref) {
305
355
  var isDisabled = disabled || loading;
306
356
  var isLoading = loading && !disabled;
307
357
  var variant = variants[prominence][tone];
308
- /**
309
- * handle "disabled" behaviour w/o disabling buttons
310
- * @see https://axesslab.com/disabled-buttons-suck/
311
- */
312
-
313
- var handleClick = getPreventableClickHandler(onClick, isDisabled);
314
- return /*#__PURE__*/jsxRuntime.jsxs(box.Box, _objectSpread(_objectSpread(_objectSpread({
358
+ return /*#__PURE__*/jsxRuntime.jsxs(BaseButton, _objectSpread(_objectSpread({}, buttonStyleProps), {}, {
315
359
  "aria-controls": ariaControls,
316
360
  "aria-describedby": ariaDescribedBy,
317
361
  "aria-disabled": isDisabled,
318
362
  "aria-expanded": ariaExpanded,
319
363
  "aria-label": props.label,
320
- as: "button",
364
+ data: data,
365
+ disabled: isDisabled,
321
366
  id: id,
322
- onClick: handleClick,
323
- ref: ref,
324
- type: type
325
- }, buttonStyleProps), data ? internal.buildDataAttributes(data) : undefined), {}, {
367
+ onClick: onClick,
368
+ ref: forwardedRef,
369
+ type: type,
326
370
  children: [resolveButtonChildren(_objectSpread(_objectSpread({}, props), {}, {
327
371
  isLoading: isLoading,
328
372
  prominence: prominence,
@@ -334,21 +378,6 @@ var Button = /*#__PURE__*/react.forwardRef(function (_ref, ref) {
334
378
  }));
335
379
  });
336
380
  Button.displayName = 'Button';
337
- /**
338
- * Prevent click events when the component is "disabled".
339
- * Note: we don't want to actually disable a button element for several reasons.
340
- * One being because that would prohibit the use of tooltips.
341
- */
342
-
343
- function getPreventableClickHandler(onClick, disabled) {
344
- return function handleClick(event) {
345
- if (disabled) {
346
- event.preventDefault();
347
- } else {
348
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
349
- }
350
- };
351
- }
352
381
 
353
382
  function Loading(_ref2) {
354
383
  var tone = _ref2.tone;
@@ -411,5 +440,6 @@ var ButtonLink = ts.forwardRefWithAs(function (_ref, ref) {
411
440
  }));
412
441
  });
413
442
 
443
+ exports.BaseButton = BaseButton;
414
444
  exports.Button = Button;
415
445
  exports.ButtonLink = ButtonLink;
@@ -4,18 +4,68 @@ Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
5
  var _objectSpread = require('@babel/runtime/helpers/objectSpread2');
6
6
  var _objectWithoutProperties = require('@babel/runtime/helpers/objectWithoutProperties');
7
- var a11y = require('@spark-web/a11y');
8
7
  var box = require('@spark-web/box');
9
- var spinner = require('@spark-web/spinner');
10
- var internal = require('@spark-web/utils/internal');
8
+ var utils = require('@spark-web/utils');
11
9
  var react = require('react');
12
- var css = require('@emotion/css');
13
- var text = require('@spark-web/text');
14
10
  var jsxRuntime = require('react/jsx-runtime');
11
+ var a11y = require('@spark-web/a11y');
12
+ var spinner = require('@spark-web/spinner');
13
+ var text = require('@spark-web/text');
14
+ var css = require('@emotion/css');
15
15
  var theme = require('@spark-web/theme');
16
16
  var link = require('@spark-web/link');
17
+ var internal = require('@spark-web/utils/internal');
17
18
  var ts = require('@spark-web/utils/ts');
18
19
 
20
+ var _excluded$2 = ["onClick", "disabled", "type"];
21
+ var BaseButton = /*#__PURE__*/react.forwardRef(function (_ref, forwardedRef) {
22
+ var onClickProp = _ref.onClick,
23
+ _ref$disabled = _ref.disabled,
24
+ disabled = _ref$disabled === void 0 ? false : _ref$disabled,
25
+ _ref$type = _ref.type,
26
+ type = _ref$type === void 0 ? 'button' : _ref$type,
27
+ rest = _objectWithoutProperties(_ref, _excluded$2);
28
+
29
+ var internalRef = react.useRef(null);
30
+ var composedRef = utils.useComposedRefs(internalRef, forwardedRef);
31
+ /**
32
+ * In Safari buttons are not focused automatically by the browser once
33
+ * pressed, the default behaviour is to focus the nearest focusable ancestor.
34
+ * To fix this we need to manually focus the button element after the user
35
+ * presses the element.
36
+ */
37
+
38
+ var onClick = react.useCallback(function (event) {
39
+ var _internalRef$current;
40
+
41
+ (_internalRef$current = internalRef.current) === null || _internalRef$current === void 0 ? void 0 : _internalRef$current.focus();
42
+ var preventableClickHandler = getPreventableClickHandler(onClickProp, disabled);
43
+ preventableClickHandler(event);
44
+ }, [disabled, onClickProp]);
45
+ return /*#__PURE__*/jsxRuntime.jsx(box.Box, _objectSpread(_objectSpread({
46
+ as: "button"
47
+ }, rest), {}, {
48
+ onClick: onClick,
49
+ ref: composedRef,
50
+ type: type
51
+ }));
52
+ });
53
+ BaseButton.displayName = 'BaseButton';
54
+ /**
55
+ * handle "disabled" behaviour w/o disabling buttons
56
+ * @see https://axesslab.com/disabled-buttons-suck/
57
+ */
58
+
59
+ function getPreventableClickHandler(onClick, disabled) {
60
+ return function handleClick(event) {
61
+ if (disabled) {
62
+ event.preventDefault();
63
+ } else {
64
+ onClick === null || onClick === void 0 ? void 0 : onClick(event);
65
+ }
66
+ };
67
+ }
68
+
19
69
  var variants = {
20
70
  high: {
21
71
  primary: {
@@ -203,10 +253,12 @@ var resolveButtonChildren = function resolveButtonChildren(_ref) {
203
253
  function HiddenWhenLoading(_ref2) {
204
254
  var children = _ref2.children,
205
255
  isLoading = _ref2.isLoading;
206
- return /*#__PURE__*/jsxRuntime.jsx("span", {
207
- className: isLoading ? css.css({
208
- opacity: 0
209
- }) : undefined,
256
+ return /*#__PURE__*/jsxRuntime.jsx(box.Box, {
257
+ as: "span",
258
+ display: "inline-flex",
259
+ alignItems: "center",
260
+ justifyContent: "center",
261
+ opacity: isLoading ? 0 : undefined,
210
262
  children: children
211
263
  });
212
264
  }
@@ -274,13 +326,12 @@ var _excluded$1 = ["aria-controls", "aria-describedby", "aria-expanded", "data",
274
326
  * Buttons are used to initialize an action, their label should express what
275
327
  * action will occur when the user interacts with it.
276
328
  */
277
- var Button = /*#__PURE__*/react.forwardRef(function (_ref, ref) {
329
+ var Button = /*#__PURE__*/react.forwardRef(function (_ref, forwardedRef) {
278
330
  var ariaControls = _ref['aria-controls'],
279
331
  ariaDescribedBy = _ref['aria-describedby'],
280
332
  ariaExpanded = _ref['aria-expanded'],
281
333
  data = _ref.data,
282
- _ref$disabled = _ref.disabled,
283
- disabled = _ref$disabled === void 0 ? false : _ref$disabled,
334
+ disabled = _ref.disabled,
284
335
  id = _ref.id,
285
336
  _ref$loading = _ref.loading,
286
337
  loading = _ref$loading === void 0 ? false : _ref$loading,
@@ -291,8 +342,7 @@ var Button = /*#__PURE__*/react.forwardRef(function (_ref, ref) {
291
342
  size = _ref$size === void 0 ? 'medium' : _ref$size,
292
343
  _ref$tone = _ref.tone,
293
344
  tone = _ref$tone === void 0 ? 'primary' : _ref$tone,
294
- _ref$type = _ref.type,
295
- type = _ref$type === void 0 ? 'button' : _ref$type,
345
+ type = _ref.type,
296
346
  props = _objectWithoutProperties(_ref, _excluded$1);
297
347
 
298
348
  var iconOnly = Boolean(props.label);
@@ -305,24 +355,18 @@ var Button = /*#__PURE__*/react.forwardRef(function (_ref, ref) {
305
355
  var isDisabled = disabled || loading;
306
356
  var isLoading = loading && !disabled;
307
357
  var variant = variants[prominence][tone];
308
- /**
309
- * handle "disabled" behaviour w/o disabling buttons
310
- * @see https://axesslab.com/disabled-buttons-suck/
311
- */
312
-
313
- var handleClick = getPreventableClickHandler(onClick, isDisabled);
314
- return /*#__PURE__*/jsxRuntime.jsxs(box.Box, _objectSpread(_objectSpread(_objectSpread({
358
+ return /*#__PURE__*/jsxRuntime.jsxs(BaseButton, _objectSpread(_objectSpread({}, buttonStyleProps), {}, {
315
359
  "aria-controls": ariaControls,
316
360
  "aria-describedby": ariaDescribedBy,
317
361
  "aria-disabled": isDisabled,
318
362
  "aria-expanded": ariaExpanded,
319
363
  "aria-label": props.label,
320
- as: "button",
364
+ data: data,
365
+ disabled: isDisabled,
321
366
  id: id,
322
- onClick: handleClick,
323
- ref: ref,
324
- type: type
325
- }, buttonStyleProps), data ? internal.buildDataAttributes(data) : undefined), {}, {
367
+ onClick: onClick,
368
+ ref: forwardedRef,
369
+ type: type,
326
370
  children: [resolveButtonChildren(_objectSpread(_objectSpread({}, props), {}, {
327
371
  isLoading: isLoading,
328
372
  prominence: prominence,
@@ -334,21 +378,6 @@ var Button = /*#__PURE__*/react.forwardRef(function (_ref, ref) {
334
378
  }));
335
379
  });
336
380
  Button.displayName = 'Button';
337
- /**
338
- * Prevent click events when the component is "disabled".
339
- * Note: we don't want to actually disable a button element for several reasons.
340
- * One being because that would prohibit the use of tooltips.
341
- */
342
-
343
- function getPreventableClickHandler(onClick, disabled) {
344
- return function handleClick(event) {
345
- if (disabled) {
346
- event.preventDefault();
347
- } else {
348
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
349
- }
350
- };
351
- }
352
381
 
353
382
  function Loading(_ref2) {
354
383
  var tone = _ref2.tone;
@@ -411,5 +440,6 @@ var ButtonLink = ts.forwardRefWithAs(function (_ref, ref) {
411
440
  }));
412
441
  });
413
442
 
443
+ exports.BaseButton = BaseButton;
414
444
  exports.Button = Button;
415
445
  exports.ButtonLink = ButtonLink;
@@ -1,17 +1,67 @@
1
1
  import _objectSpread from '@babel/runtime/helpers/esm/objectSpread2';
2
2
  import _objectWithoutProperties from '@babel/runtime/helpers/esm/objectWithoutProperties';
3
- import { useFocusRing, VisuallyHidden } from '@spark-web/a11y';
4
3
  import { Box } from '@spark-web/box';
4
+ import { useComposedRefs } from '@spark-web/utils';
5
+ import { forwardRef, useRef, useCallback, Children, isValidElement, cloneElement } from 'react';
6
+ import { jsx, jsxs } from 'react/jsx-runtime';
7
+ import { useFocusRing, VisuallyHidden } from '@spark-web/a11y';
5
8
  import { Spinner } from '@spark-web/spinner';
6
- import { buildDataAttributes } from '@spark-web/utils/internal';
7
- import { Children, isValidElement, cloneElement, forwardRef } from 'react';
8
- import { css } from '@emotion/css';
9
9
  import { Text } from '@spark-web/text';
10
- import { jsx, jsxs } from 'react/jsx-runtime';
10
+ import { css } from '@emotion/css';
11
11
  import { useTheme } from '@spark-web/theme';
12
12
  import { useLinkComponent } from '@spark-web/link';
13
+ import { buildDataAttributes } from '@spark-web/utils/internal';
13
14
  import { forwardRefWithAs } from '@spark-web/utils/ts';
14
15
 
16
+ var _excluded$2 = ["onClick", "disabled", "type"];
17
+ var BaseButton = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) {
18
+ var onClickProp = _ref.onClick,
19
+ _ref$disabled = _ref.disabled,
20
+ disabled = _ref$disabled === void 0 ? false : _ref$disabled,
21
+ _ref$type = _ref.type,
22
+ type = _ref$type === void 0 ? 'button' : _ref$type,
23
+ rest = _objectWithoutProperties(_ref, _excluded$2);
24
+
25
+ var internalRef = useRef(null);
26
+ var composedRef = useComposedRefs(internalRef, forwardedRef);
27
+ /**
28
+ * In Safari buttons are not focused automatically by the browser once
29
+ * pressed, the default behaviour is to focus the nearest focusable ancestor.
30
+ * To fix this we need to manually focus the button element after the user
31
+ * presses the element.
32
+ */
33
+
34
+ var onClick = useCallback(function (event) {
35
+ var _internalRef$current;
36
+
37
+ (_internalRef$current = internalRef.current) === null || _internalRef$current === void 0 ? void 0 : _internalRef$current.focus();
38
+ var preventableClickHandler = getPreventableClickHandler(onClickProp, disabled);
39
+ preventableClickHandler(event);
40
+ }, [disabled, onClickProp]);
41
+ return /*#__PURE__*/jsx(Box, _objectSpread(_objectSpread({
42
+ as: "button"
43
+ }, rest), {}, {
44
+ onClick: onClick,
45
+ ref: composedRef,
46
+ type: type
47
+ }));
48
+ });
49
+ BaseButton.displayName = 'BaseButton';
50
+ /**
51
+ * handle "disabled" behaviour w/o disabling buttons
52
+ * @see https://axesslab.com/disabled-buttons-suck/
53
+ */
54
+
55
+ function getPreventableClickHandler(onClick, disabled) {
56
+ return function handleClick(event) {
57
+ if (disabled) {
58
+ event.preventDefault();
59
+ } else {
60
+ onClick === null || onClick === void 0 ? void 0 : onClick(event);
61
+ }
62
+ };
63
+ }
64
+
15
65
  var variants = {
16
66
  high: {
17
67
  primary: {
@@ -199,10 +249,12 @@ var resolveButtonChildren = function resolveButtonChildren(_ref) {
199
249
  function HiddenWhenLoading(_ref2) {
200
250
  var children = _ref2.children,
201
251
  isLoading = _ref2.isLoading;
202
- return /*#__PURE__*/jsx("span", {
203
- className: isLoading ? css({
204
- opacity: 0
205
- }) : undefined,
252
+ return /*#__PURE__*/jsx(Box, {
253
+ as: "span",
254
+ display: "inline-flex",
255
+ alignItems: "center",
256
+ justifyContent: "center",
257
+ opacity: isLoading ? 0 : undefined,
206
258
  children: children
207
259
  });
208
260
  }
@@ -270,13 +322,12 @@ var _excluded$1 = ["aria-controls", "aria-describedby", "aria-expanded", "data",
270
322
  * Buttons are used to initialize an action, their label should express what
271
323
  * action will occur when the user interacts with it.
272
324
  */
273
- var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
325
+ var Button = /*#__PURE__*/forwardRef(function (_ref, forwardedRef) {
274
326
  var ariaControls = _ref['aria-controls'],
275
327
  ariaDescribedBy = _ref['aria-describedby'],
276
328
  ariaExpanded = _ref['aria-expanded'],
277
329
  data = _ref.data,
278
- _ref$disabled = _ref.disabled,
279
- disabled = _ref$disabled === void 0 ? false : _ref$disabled,
330
+ disabled = _ref.disabled,
280
331
  id = _ref.id,
281
332
  _ref$loading = _ref.loading,
282
333
  loading = _ref$loading === void 0 ? false : _ref$loading,
@@ -287,8 +338,7 @@ var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
287
338
  size = _ref$size === void 0 ? 'medium' : _ref$size,
288
339
  _ref$tone = _ref.tone,
289
340
  tone = _ref$tone === void 0 ? 'primary' : _ref$tone,
290
- _ref$type = _ref.type,
291
- type = _ref$type === void 0 ? 'button' : _ref$type,
341
+ type = _ref.type,
292
342
  props = _objectWithoutProperties(_ref, _excluded$1);
293
343
 
294
344
  var iconOnly = Boolean(props.label);
@@ -301,24 +351,18 @@ var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
301
351
  var isDisabled = disabled || loading;
302
352
  var isLoading = loading && !disabled;
303
353
  var variant = variants[prominence][tone];
304
- /**
305
- * handle "disabled" behaviour w/o disabling buttons
306
- * @see https://axesslab.com/disabled-buttons-suck/
307
- */
308
-
309
- var handleClick = getPreventableClickHandler(onClick, isDisabled);
310
- return /*#__PURE__*/jsxs(Box, _objectSpread(_objectSpread(_objectSpread({
354
+ return /*#__PURE__*/jsxs(BaseButton, _objectSpread(_objectSpread({}, buttonStyleProps), {}, {
311
355
  "aria-controls": ariaControls,
312
356
  "aria-describedby": ariaDescribedBy,
313
357
  "aria-disabled": isDisabled,
314
358
  "aria-expanded": ariaExpanded,
315
359
  "aria-label": props.label,
316
- as: "button",
360
+ data: data,
361
+ disabled: isDisabled,
317
362
  id: id,
318
- onClick: handleClick,
319
- ref: ref,
320
- type: type
321
- }, buttonStyleProps), data ? buildDataAttributes(data) : undefined), {}, {
363
+ onClick: onClick,
364
+ ref: forwardedRef,
365
+ type: type,
322
366
  children: [resolveButtonChildren(_objectSpread(_objectSpread({}, props), {}, {
323
367
  isLoading: isLoading,
324
368
  prominence: prominence,
@@ -330,21 +374,6 @@ var Button = /*#__PURE__*/forwardRef(function (_ref, ref) {
330
374
  }));
331
375
  });
332
376
  Button.displayName = 'Button';
333
- /**
334
- * Prevent click events when the component is "disabled".
335
- * Note: we don't want to actually disable a button element for several reasons.
336
- * One being because that would prohibit the use of tooltips.
337
- */
338
-
339
- function getPreventableClickHandler(onClick, disabled) {
340
- return function handleClick(event) {
341
- if (disabled) {
342
- event.preventDefault();
343
- } else {
344
- onClick === null || onClick === void 0 ? void 0 : onClick(event);
345
- }
346
- };
347
- }
348
377
 
349
378
  function Loading(_ref2) {
350
379
  var tone = _ref2.tone;
@@ -407,4 +436,4 @@ var ButtonLink = forwardRefWithAs(function (_ref, ref) {
407
436
  }));
408
437
  });
409
438
 
410
- export { Button, ButtonLink };
439
+ export { BaseButton, Button, ButtonLink };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spark-web/button",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "license": "MIT",
5
5
  "main": "dist/spark-web-button.cjs.js",
6
6
  "module": "dist/spark-web-button.esm.js",
@@ -8,16 +8,16 @@
8
8
  "dist"
9
9
  ],
10
10
  "dependencies": {
11
- "@babel/runtime": "^7.14.6",
12
- "@emotion/css": "^11.7.1",
13
- "@spark-web/a11y": "^1.0.4",
14
- "@spark-web/box": "^1.0.4",
15
- "@spark-web/icon": "^1.1.2",
16
- "@spark-web/link": "^1.0.4",
17
- "@spark-web/spinner": "^1.0.1",
18
- "@spark-web/text": "^1.0.4",
19
- "@spark-web/theme": "^3.0.0",
20
- "@spark-web/utils": "^1.1.2"
11
+ "@babel/runtime": "^7.18.3",
12
+ "@emotion/css": "^11.9.0",
13
+ "@spark-web/a11y": "^1.1.0",
14
+ "@spark-web/box": "^1.0.6",
15
+ "@spark-web/icon": "^1.1.4",
16
+ "@spark-web/link": "^1.0.6",
17
+ "@spark-web/spinner": "^1.0.4",
18
+ "@spark-web/text": "^1.0.6",
19
+ "@spark-web/theme": "^3.0.2",
20
+ "@spark-web/utils": "^1.1.5"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/react": "^17.0.12",