@spark-web/button 1.0.3 → 1.1.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
@@ -45,95 +45,56 @@ options are: `low` and `high`.
45
45
  Defaults to `high`.
46
46
 
47
47
  ```jsx live
48
- <Stack gap="large">
49
- <Text weight="strong">High prominence</Text>
50
- <Inline gap="small">
51
- <Button prominence="high" tone="primary">
52
- <LightBulbIcon />
53
- Primary
54
- </Button>
55
- <Button prominence="high" tone="secondary">
56
- <LightBulbIcon />
57
- Secondary
58
- </Button>
59
- <Button prominence="high" tone="neutral">
60
- <LightBulbIcon />
61
- Neutral
62
- </Button>
63
- <Button prominence="high" tone="positive">
64
- <LightBulbIcon />
65
- Positive
66
- </Button>
67
- <Button prominence="high" tone="critical">
68
- <LightBulbIcon />
69
- Critical
70
- </Button>
71
- </Inline>
72
- <Divider />
73
- <Text weight="strong">Low prominence</Text>
74
- <Inline gap="small">
75
- <Button prominence="low" tone="primary">
76
- <LightBulbIcon />
77
- Primary
78
- </Button>
79
- <Button prominence="low" tone="secondary">
80
- <LightBulbIcon />
81
- Secondary
82
- </Button>
83
- <Button prominence="low" tone="neutral">
84
- <LightBulbIcon />
85
- Neutral
86
- </Button>
87
- <Button prominence="low" tone="positive">
88
- <LightBulbIcon />
89
- Positive
90
- </Button>
91
- <Button prominence="low" tone="critical">
92
- <LightBulbIcon />
93
- Critical
94
- </Button>
95
- <Button prominence="low" tone="caution">
96
- <LightBulbIcon />
97
- Critical
98
- </Button>
99
- <Button prominence="low" tone="info">
100
- <LightBulbIcon />
101
- Informative
102
- </Button>
103
- </Inline>
104
- <Divider />
105
- <Text weight="strong">None prominence</Text>
106
- <Inline gap="small">
107
- <Button prominence="none" tone="primary">
108
- <LightBulbIcon />
109
- Primary
110
- </Button>
111
- <Button prominence="none" tone="secondary">
112
- <LightBulbIcon />
113
- Secondary
114
- </Button>
115
- <Button prominence="none" tone="neutral">
116
- <LightBulbIcon />
117
- Neutral
118
- </Button>
119
- <Button prominence="none" tone="positive">
120
- <LightBulbIcon />
121
- Positive
122
- </Button>
123
- <Button prominence="none" tone="critical">
124
- <LightBulbIcon />
125
- Critical
126
- </Button>
127
- <Button prominence="none" tone="caution">
128
- <LightBulbIcon />
129
- Critical
130
- </Button>
131
- <Button prominence="none" tone="info">
132
- <LightBulbIcon />
133
- Informative
134
- </Button>
135
- </Inline>
136
- </Stack>
48
+ const baseButtonTones = [
49
+ { label: 'Primary', tone: 'primary' },
50
+ { label: 'Secondary', tone: 'secondary' },
51
+ { label: 'Neutral', tone: 'neutral' },
52
+ { label: 'Positive', tone: 'positive' },
53
+ { label: 'Critical', tone: 'critical' },
54
+ ];
55
+
56
+ const extraButtonTones = [
57
+ { label: 'Caution', tone: 'caution' },
58
+ { label: 'Informative', tone: 'info' },
59
+ ];
60
+
61
+ return (
62
+ <Stack gap="large" dividers>
63
+ <Stack gap="large">
64
+ <Text weight="strong">High prominence</Text>
65
+ <Inline gap="small">
66
+ {baseButtonTones.map(({ label, tone }) => (
67
+ <Button key={label} tone={tone} prominence="high">
68
+ <LightBulbIcon />
69
+ {label}
70
+ </Button>
71
+ ))}
72
+ </Inline>
73
+ </Stack>
74
+ <Stack gap="large">
75
+ <Text weight="strong">Low prominence</Text>
76
+ <Inline gap="small">
77
+ {baseButtonTones.concat(extraButtonTones).map(({ label, tone }) => (
78
+ <Button key={label} tone={tone} prominence="low">
79
+ <LightBulbIcon />
80
+ {label}
81
+ </Button>
82
+ ))}
83
+ </Inline>
84
+ </Stack>
85
+ <Stack gap="large">
86
+ <Text weight="strong">None prominence</Text>
87
+ <Inline gap="small">
88
+ {baseButtonTones.concat(extraButtonTones).map(({ label, tone }) => (
89
+ <Button key={label} tone={tone} prominence="none">
90
+ <LightBulbIcon />
91
+ {label}
92
+ </Button>
93
+ ))}
94
+ </Inline>
95
+ </Stack>
96
+ </Stack>
97
+ );
137
98
  ```
138
99
 
139
100
  ## Size
@@ -186,6 +147,44 @@ users of assistive technology.
186
147
  </Inline>
187
148
  ```
188
149
 
150
+ ## Loading
151
+
152
+ Buttons have an optional `loading` prop to indicate that an action is in
153
+ progress. When this is true a spinner will be displayed.
154
+
155
+ Note: buttons will not be interative when `loading` is true.
156
+
157
+ ```jsx live
158
+ const [loading, setLoading] = React.useState(false);
159
+ const toggle = event => setLoading(event.target.checked);
160
+
161
+ return (
162
+ <Stack gap="large">
163
+ <Checkbox size="medium" checked={loading} onChange={toggle}>
164
+ <Text>Toggle loading state</Text>
165
+ </Checkbox>
166
+ <Inline gap="large">
167
+ <Button label="Download" loading={loading}>
168
+ <DownloadIcon />
169
+ </Button>
170
+ <Button loading={loading}>
171
+ <DownloadIcon />
172
+ Download
173
+ </Button>
174
+ </Inline>
175
+ <Inline gap="large">
176
+ <Button label="Download" size="large" loading={loading}>
177
+ <DownloadIcon />
178
+ </Button>
179
+ <Button size="large" loading={loading}>
180
+ <DownloadIcon />
181
+ Download
182
+ </Button>
183
+ </Inline>
184
+ </Stack>
185
+ );
186
+ ```
187
+
189
188
  ## ButtonLink
190
189
 
191
190
  The appearance of a button, with the semantics of a link — shares `Button` API,
@@ -205,13 +204,17 @@ with the exception of `href` vs `onClick` props.
205
204
  | aria-describedby? | string | | Identifies the element (or elements) that describes the object. Only applicable for `Button`. |
206
205
  | aria-expanded? | string | | Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. Only applicable for `Button`. |
207
206
  | children | string \| React.ReactElement\<IconProps> | | Children element to be rendered inside the button. |
208
- | data? | Object | | Allows setting of data attributes on the button. |
207
+ | data? | [DataAttributeMap][data-attribute-map] | | Allows setting of data attributes on the button. |
209
208
  | disabled? | boolean | | When true, prevents `onClick` from firing. Only applicable for `Button`. |
210
209
  | href | string | | Specifies the url the button should redirect to upon being clicked. Only applicable for `ButtonLink`. |
211
210
  | id? | string | | Unique identifier for the button. |
212
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
213
  | onClick? | Function | | Function to be fired following a click event of the button. Only applicable for `Button`. |
214
214
  | prominence? | 'high' \| 'low' | 'high' | Sets the visual prominence of the button. |
215
215
  | size? | 'medium' \| 'large' | 'medium' | Sets the size of the button. |
216
216
  | tone? | 'primary' \| 'secondary' \| 'neutral' \| 'positive' \| 'caution' \| 'critical' \| 'info' | 'primary' | Sets the tone of the button. |
217
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
@@ -1,22 +1,34 @@
1
- import * as React from 'react';
1
+ import type { MouseEvent as ReactMouseEvent } from 'react';
2
2
  import type { CommonButtonProps, NativeButtonProps } from './types';
3
3
  export declare type ButtonProps = CommonButtonProps & {
4
+ /**
5
+ * Identifies the element (or elements) whose contents or presence
6
+ * are controlled by the current element.
7
+ */
4
8
  'aria-controls'?: NativeButtonProps['aria-controls'];
9
+ /** Identifies the element (or elements) that describes the object. */
5
10
  'aria-describedby'?: NativeButtonProps['aria-describedby'];
11
+ /** Indicates whether the element, or another grouping element it controls, is currently expanded or collapsed. */
6
12
  'aria-expanded'?: NativeButtonProps['aria-expanded'];
13
+ /** When true, prevents onClick from firing. */
14
+ disabled?: boolean;
15
+ /** When true, the button will display a loading spinner. */
16
+ loading?: boolean;
17
+ /** Function to be fired following a click event of the button. Only applicable for Button. */
7
18
  onClick?: NativeButtonProps['onClick'];
19
+ /** The size of the button. */
8
20
  size?: CommonButtonProps['size'];
21
+ /** Provide an alternate type if the button is within a form. */
9
22
  type?: 'button' | 'submit' | 'reset';
10
- disabled?: boolean;
11
23
  };
12
24
  /**
13
25
  * Buttons are used to initialize an action, their label should express what
14
26
  * action will occur when the user interacts with it.
15
27
  */
16
- export declare const Button: React.ForwardRefExoticComponent<ButtonProps & React.RefAttributes<HTMLButtonElement>>;
28
+ export declare const Button: import("react").ForwardRefExoticComponent<ButtonProps & import("react").RefAttributes<HTMLButtonElement>>;
17
29
  /**
18
30
  * Prevent click events when the component is "disabled".
19
31
  * Note: we don't want to actually disable a button element for several reasons.
20
32
  * One being because that would prohibit the use of tooltips.
21
33
  */
22
- export declare function getPreventableClickHandler(onClick: NativeButtonProps['onClick'], disabled: boolean): (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
34
+ export declare function getPreventableClickHandler(onClick: NativeButtonProps['onClick'], disabled: boolean): (event: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => void;
@@ -1,8 +1,9 @@
1
1
  import type { ButtonChildrenProps, ButtonProminence, ButtonSize, ButtonTone } from './types';
2
2
  declare type ResolveButtonChildren = ButtonChildrenProps & {
3
+ isLoading: boolean;
3
4
  prominence: ButtonProminence;
4
5
  size: ButtonSize;
5
6
  tone: ButtonTone;
6
7
  };
7
- export declare const resolveButtonChildren: ({ children, prominence, size, tone, }: ResolveButtonChildren) => JSX.Element[];
8
+ export declare const resolveButtonChildren: ({ children, isLoading, prominence, size, tone, }: ResolveButtonChildren) => JSX.Element[];
8
9
  export {};
@@ -11,13 +11,19 @@ declare type ChildrenWithText = {
11
11
  children: string | [ReactElement<IconProps>, string] | [string, ReactElement<IconProps>];
12
12
  };
13
13
  declare type IconOnly = {
14
+ /**
15
+ * Implicit label for buttons only required for icon-only buttons
16
+ * for accessibility reasons.
17
+ */
14
18
  label: string;
15
19
  children: ReactElement<IconProps>;
16
20
  };
17
21
  export declare type ButtonChildrenProps = ChildrenWithText | IconOnly;
18
22
  export declare type NativeButtonProps = ButtonHTMLAttributes<HTMLButtonElement>;
19
23
  export declare type CommonButtonProps = {
24
+ /** Allows setting of data attributes on the underlying element. */
20
25
  data?: DataAttributeMap;
26
+ /** Unique identifier for the underlying element. */
21
27
  id?: string;
22
28
  } & ButtonChildrenProps & ButtonStyleProps;
23
29
  export declare type ButtonStyleProps = {
@@ -4,37 +4,18 @@ 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');
7
8
  var box = require('@spark-web/box');
9
+ var spinner = require('@spark-web/spinner');
8
10
  var internal = require('@spark-web/utils/internal');
9
- var React = require('react');
11
+ var react = require('react');
12
+ var css = require('@emotion/css');
10
13
  var text = require('@spark-web/text');
11
14
  var jsxRuntime = require('react/jsx-runtime');
12
- var css = require('@emotion/css');
13
- var a11y = require('@spark-web/a11y');
14
15
  var theme = require('@spark-web/theme');
15
16
  var link = require('@spark-web/link');
16
17
  var ts = require('@spark-web/utils/ts');
17
18
 
18
- function _interopNamespace(e) {
19
- if (e && e.__esModule) return e;
20
- var n = Object.create(null);
21
- if (e) {
22
- Object.keys(e).forEach(function (k) {
23
- if (k !== 'default') {
24
- var d = Object.getOwnPropertyDescriptor(e, k);
25
- Object.defineProperty(n, k, d.get ? d : {
26
- enumerable: true,
27
- get: function () { return e[k]; }
28
- });
29
- }
30
- });
31
- }
32
- n["default"] = e;
33
- return Object.freeze(n);
34
- }
35
-
36
- var React__namespace = /*#__PURE__*/_interopNamespace(React);
37
-
38
19
  var variants = {
39
20
  high: {
40
21
  primary: {
@@ -179,32 +160,39 @@ var mapTokens = {
179
160
 
180
161
  var resolveButtonChildren = function resolveButtonChildren(_ref) {
181
162
  var children = _ref.children,
163
+ isLoading = _ref.isLoading,
182
164
  prominence = _ref.prominence,
183
165
  size = _ref.size,
184
166
  tone = _ref.tone;
185
167
  var variant = variants[prominence][tone];
186
- return React.Children.map(children, function (child) {
168
+ return react.Children.map(children, function (child) {
187
169
  if (typeof child === 'string') {
188
- return /*#__PURE__*/jsxRuntime.jsx(text.Text, {
189
- as: "span",
190
- baseline: false,
191
- overflowStrategy: "nowrap",
192
- weight: "strong",
193
- size: mapTokens.fontSize[size],
194
- tone: variant === null || variant === void 0 ? void 0 : variant.textTone,
195
- children: child
170
+ return /*#__PURE__*/jsxRuntime.jsx(HiddenWhenLoading, {
171
+ isLoading: isLoading,
172
+ children: /*#__PURE__*/jsxRuntime.jsx(text.Text, {
173
+ as: "span",
174
+ baseline: false,
175
+ overflowStrategy: "nowrap",
176
+ weight: "semibold",
177
+ size: mapTokens.fontSize[size],
178
+ tone: variant === null || variant === void 0 ? void 0 : variant.textTone,
179
+ children: child
180
+ })
196
181
  });
197
182
  }
198
183
 
199
- if ( /*#__PURE__*/React.isValidElement(child)) {
200
- return /*#__PURE__*/React.cloneElement(child, {
201
- // Dismiss buttons need to be `xxsmall`
202
- // For everything else, we force them to be `xsmall`
203
- size: child.props.size === 'xxsmall' ? child.props.size : 'xsmall',
204
- // If the button is low prominence with a decorative tone we want to force
205
- // the tone to be the same as the button
206
- // We also don't want users to override the tone of the icon inside of the button
207
- tone: variant === null || variant === void 0 ? void 0 : variant.textTone
184
+ if ( /*#__PURE__*/react.isValidElement(child)) {
185
+ return /*#__PURE__*/jsxRuntime.jsx(HiddenWhenLoading, {
186
+ isLoading: isLoading,
187
+ children: /*#__PURE__*/react.cloneElement(child, {
188
+ // Dismiss buttons need to be `xxsmall`
189
+ // For everything else, we force them to be `xsmall`
190
+ size: child.props.size === 'xxsmall' ? child.props.size : 'xsmall',
191
+ // If the button is low prominence with a decorative tone we want to force
192
+ // the tone to be the same as the button
193
+ // We also don't want users to override the tone of the icon inside of the button
194
+ tone: variant === null || variant === void 0 ? void 0 : variant.textTone
195
+ })
208
196
  });
209
197
  }
210
198
 
@@ -212,6 +200,17 @@ var resolveButtonChildren = function resolveButtonChildren(_ref) {
212
200
  });
213
201
  };
214
202
 
203
+ function HiddenWhenLoading(_ref2) {
204
+ var children = _ref2.children,
205
+ isLoading = _ref2.isLoading;
206
+ return /*#__PURE__*/jsxRuntime.jsx("span", {
207
+ className: isLoading ? css.css({
208
+ opacity: 0
209
+ }) : undefined,
210
+ children: children
211
+ });
212
+ }
213
+
215
214
  function useButtonStyles(_ref) {
216
215
  var iconOnly = _ref.iconOnly,
217
216
  prominence = _ref.prominence,
@@ -269,13 +268,13 @@ function useButtonStyles(_ref) {
269
268
  return buttonStyleProps;
270
269
  }
271
270
 
272
- var _excluded$1 = ["aria-controls", "aria-describedby", "aria-expanded", "data", "disabled", "id", "onClick", "prominence", "size", "tone", "type"];
271
+ var _excluded$1 = ["aria-controls", "aria-describedby", "aria-expanded", "data", "disabled", "id", "loading", "onClick", "prominence", "size", "tone", "type"];
273
272
 
274
273
  /**
275
274
  * Buttons are used to initialize an action, their label should express what
276
275
  * action will occur when the user interacts with it.
277
276
  */
278
- var Button = /*#__PURE__*/React__namespace.forwardRef(function (_ref, ref) {
277
+ var Button = /*#__PURE__*/react.forwardRef(function (_ref, ref) {
279
278
  var ariaControls = _ref['aria-controls'],
280
279
  ariaDescribedBy = _ref['aria-describedby'],
281
280
  ariaExpanded = _ref['aria-expanded'],
@@ -283,6 +282,8 @@ var Button = /*#__PURE__*/React__namespace.forwardRef(function (_ref, ref) {
283
282
  _ref$disabled = _ref.disabled,
284
283
  disabled = _ref$disabled === void 0 ? false : _ref$disabled,
285
284
  id = _ref.id,
285
+ _ref$loading = _ref.loading,
286
+ loading = _ref$loading === void 0 ? false : _ref$loading,
286
287
  onClick = _ref.onClick,
287
288
  _ref$prominence = _ref.prominence,
288
289
  prominence = _ref$prominence === void 0 ? 'high' : _ref$prominence,
@@ -300,18 +301,17 @@ var Button = /*#__PURE__*/React__namespace.forwardRef(function (_ref, ref) {
300
301
  size: size,
301
302
  tone: tone,
302
303
  prominence: prominence
303
- }); // TODO: add loading state for button
304
-
305
- var isDisabled = disabled;
306
- /* || loading */
307
-
304
+ });
305
+ var isDisabled = disabled || loading;
306
+ var isLoading = loading && !disabled;
307
+ var variant = variants[prominence][tone];
308
308
  /**
309
309
  * handle "disabled" behaviour w/o disabling buttons
310
310
  * @see https://axesslab.com/disabled-buttons-suck/
311
311
  */
312
312
 
313
313
  var handleClick = getPreventableClickHandler(onClick, isDisabled);
314
- return /*#__PURE__*/jsxRuntime.jsx(box.Box, _objectSpread(_objectSpread(_objectSpread({
314
+ return /*#__PURE__*/jsxRuntime.jsxs(box.Box, _objectSpread(_objectSpread(_objectSpread({
315
315
  "aria-controls": ariaControls,
316
316
  "aria-describedby": ariaDescribedBy,
317
317
  "aria-disabled": isDisabled,
@@ -323,14 +323,17 @@ var Button = /*#__PURE__*/React__namespace.forwardRef(function (_ref, ref) {
323
323
  ref: ref,
324
324
  type: type
325
325
  }, buttonStyleProps), data ? internal.buildDataAttributes(data) : undefined), {}, {
326
- children: resolveButtonChildren(_objectSpread(_objectSpread({}, props), {}, {
326
+ children: [resolveButtonChildren(_objectSpread(_objectSpread({}, props), {}, {
327
+ isLoading: isLoading,
327
328
  prominence: prominence,
328
329
  size: size,
329
330
  tone: tone
330
- }))
331
+ })), isLoading && /*#__PURE__*/jsxRuntime.jsx(Loading, {
332
+ tone: variant === null || variant === void 0 ? void 0 : variant.textTone
333
+ })]
331
334
  }));
332
335
  });
333
- Button.displayName = 'Buttton';
336
+ Button.displayName = 'Button';
334
337
  /**
335
338
  * Prevent click events when the component is "disabled".
336
339
  * Note: we don't want to actually disable a button element for several reasons.
@@ -347,6 +350,27 @@ function getPreventableClickHandler(onClick, disabled) {
347
350
  };
348
351
  }
349
352
 
353
+ function Loading(_ref2) {
354
+ var tone = _ref2.tone;
355
+ return /*#__PURE__*/jsxRuntime.jsxs(box.Box, {
356
+ as: "span",
357
+ position: "absolute",
358
+ top: 0,
359
+ bottom: 0,
360
+ left: 0,
361
+ right: 0,
362
+ display: "flex",
363
+ alignItems: "center",
364
+ justifyContent: "center",
365
+ children: [/*#__PURE__*/jsxRuntime.jsx(a11y.VisuallyHidden, {
366
+ children: "button loading indicator"
367
+ }), /*#__PURE__*/jsxRuntime.jsx(spinner.Spinner, {
368
+ size: "xsmall",
369
+ tone: tone
370
+ })]
371
+ });
372
+ }
373
+
350
374
  var _excluded = ["data", "href", "id", "prominence", "size", "tone"];
351
375
 
352
376
  /** The appearance of a `Button`, with the semantics of a link. */
@@ -366,9 +390,9 @@ var ButtonLink = ts.forwardRefWithAs(function (_ref, ref) {
366
390
  var iconOnly = Boolean(props.label);
367
391
  var buttonStyleProps = useButtonStyles({
368
392
  iconOnly: iconOnly,
393
+ prominence: prominence,
369
394
  size: size,
370
- tone: tone,
371
- prominence: prominence
395
+ tone: tone
372
396
  });
373
397
  return /*#__PURE__*/jsxRuntime.jsx(box.Box, _objectSpread(_objectSpread(_objectSpread({
374
398
  "aria-label": props.label,
@@ -379,6 +403,7 @@ var ButtonLink = ts.forwardRefWithAs(function (_ref, ref) {
379
403
  ref: ref
380
404
  }, buttonStyleProps), data ? internal.buildDataAttributes(data) : undefined), {}, {
381
405
  children: resolveButtonChildren(_objectSpread(_objectSpread({}, props), {}, {
406
+ isLoading: false,
382
407
  prominence: prominence,
383
408
  size: size,
384
409
  tone: tone