@sats-group/ui-lib 77.1.0 → 78.0.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/eslint.config.mjs CHANGED
@@ -19,13 +19,10 @@ export default [
19
19
  {
20
20
  ignores: [
21
21
  '**/.vscode',
22
- '**/site/build',
23
22
  '**/dist',
24
23
  '**/docs',
25
24
  '**/node_modules',
26
25
  '**/test-results',
27
- '**/site/pages/colors/dark-docs.tsx',
28
- '**/site/pages/colors/light-docs.tsx',
29
26
  ],
30
27
  },
31
28
  ...compat.extends(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sats-group/ui-lib",
3
- "version": "77.1.0",
3
+ "version": "78.0.0",
4
4
  "description": "SATS web user interface library",
5
5
  "engines": {
6
6
  "node": "^18 || ^20",
@@ -1,19 +1,23 @@
1
1
  import * as React from 'react';
2
2
 
3
- import { Button as Props } from './button.types';
3
+ import type { Button as Props } from './button.types';
4
4
 
5
5
  import VisuallyButton from '../visually-button';
6
6
 
7
- const Button: React.FunctionComponent<Props> & {
8
- sizes: typeof VisuallyButton.sizes;
9
- themes: typeof VisuallyButton.themes;
10
- variants: typeof VisuallyButton.variants;
11
- } = ({ type = 'button', ...rest }) => (
12
- <VisuallyButton elementName="button" type={type} {...rest} />
7
+ const ButtonRef = React.forwardRef<HTMLButtonElement, Props>(
8
+ ({ type = 'button', ...rest }, ref) => (
9
+ <VisuallyButton elementName="button" ref={ref} type={type} {...rest} />
10
+ ),
13
11
  );
14
12
 
15
- Button.sizes = VisuallyButton.sizes;
16
- Button.themes = VisuallyButton.themes;
17
- Button.variants = VisuallyButton.variants;
13
+ ButtonRef.displayName = 'Button';
14
+
15
+ const Button: typeof ButtonRef & {
16
+ sizes: typeof VisuallyButton.sizes;
17
+ variants: typeof VisuallyButton.variants;
18
+ } = Object.assign(ButtonRef, {
19
+ sizes: VisuallyButton.sizes,
20
+ variants: VisuallyButton.variants,
21
+ });
18
22
 
19
23
  export default Button;
@@ -1,3 +1,3 @@
1
- import { VisuallyButtonButton } from '../visually-button/visually-button.types';
1
+ import type { VisuallyButtonButton } from '../visually-button/visually-button.types';
2
2
 
3
- export type Button = VisuallyButtonButton;
3
+ export type Button = Omit<VisuallyButtonButton, 'elementName'>;
@@ -10,6 +10,7 @@ export const names = [
10
10
  'confirmation',
11
11
  'context-menu',
12
12
  'cropped-image',
13
+ 'div-button',
13
14
  'dropdown-list',
14
15
  'expander',
15
16
  'filter',
@@ -51,6 +52,7 @@ export const selectors = [
51
52
  '.confirmation',
52
53
  '.context-menu',
53
54
  '.cropped-image',
55
+ '.div-button',
54
56
  '.dropdown-list',
55
57
  '.expander',
56
58
  '.filter',
@@ -90,10 +90,11 @@ const ContextMenu: React.FunctionComponent<Props> & {
90
90
  <div>
91
91
  <Button
92
92
  {...close}
93
- variant={Button.variants.tertiary}
94
- size={Button.sizes.small}
93
+ iconOnly
95
94
  leadingIcon={<Close />}
96
95
  onClick={onClose}
96
+ size={Button.sizes.small}
97
+ variant={Button.variants.tertiary}
97
98
  />
98
99
  </div>
99
100
  </div>
@@ -0,0 +1,15 @@
1
+ import * as React from 'react';
2
+
3
+ import VisuallyButton from '../visually-button/visually-button';
4
+
5
+ import { DivButton as Props } from './div-button.types';
6
+
7
+ const DivButton: React.FunctionComponent<Props> & {
8
+ sizes: typeof VisuallyButton.sizes;
9
+ variants: typeof VisuallyButton.variants;
10
+ } = props => <VisuallyButton elementName="div" {...props} />;
11
+
12
+ DivButton.sizes = VisuallyButton.sizes;
13
+ DivButton.variants = VisuallyButton.variants;
14
+
15
+ export default DivButton;
@@ -0,0 +1,3 @@
1
+ import type { VisuallyButtonDiv } from '../visually-button/visually-button.types';
2
+
3
+ export type DivButton = Omit<VisuallyButtonDiv, 'elementName'>;
@@ -0,0 +1,2 @@
1
+ import DivButton from './div-button';
2
+ export default DivButton;
@@ -57,11 +57,12 @@ const FormContent: React.FC<Props> = ({
57
57
  </Text>
58
58
  <div className="form-content__modal-close-small">
59
59
  <Button
60
- variant={Button.variants.secondary}
60
+ iconOnly
61
+ leadingIcon={<Close />}
61
62
  onClick={close}
62
63
  size={Button.sizes.small}
63
- leadingIcon={<Close />}
64
- ariaLabel={closeLabel}
64
+ text={closeLabel}
65
+ variant={Button.variants.secondary}
65
66
  />
66
67
  </div>
67
68
  </div>
@@ -1,5 +1,3 @@
1
- import { VisuallyButtonLink } from '../visually-button/visually-button.types';
1
+ import type { VisuallyButtonLink } from '../visually-button/visually-button.types';
2
2
 
3
- export type LinkButton = VisuallyButtonLink & {
4
- href: string;
5
- };
3
+ export type LinkButton = Omit<VisuallyButtonLink, 'elementName'>;
@@ -53,12 +53,12 @@ const MessageField: React.FunctionComponent<Props> & {
53
53
  <div>
54
54
  <Button
55
55
  {...send}
56
- leadingIcon={<Send />}
57
56
  disabled={!hasTyped || isSubmitting}
58
- theme={isSubmitting ? Button.themes.spinner : Button.themes.normal}
59
- variant={Button.variants.complete}
57
+ leadingIcon={<Send />}
60
58
  onClick={onClickFunc}
61
59
  size={size === sizes.small ? Button.sizes.small : Button.sizes.basic}
60
+ spinning={isSubmitting}
61
+ variant={Button.variants.complete}
62
62
  />
63
63
  </div>
64
64
  </div>
@@ -61,11 +61,12 @@ const Modal: React.FC<React.PropsWithChildren<Props>> & {
61
61
  })}
62
62
  >
63
63
  <Button
64
- ariaLabel={closeLabel}
65
64
  leadingIcon={<Close />}
66
65
  onClick={onClose}
67
66
  size={Button.sizes.small}
67
+ text={closeLabel}
68
68
  variant={Button.variants.secondary}
69
+ iconOnly
69
70
  />
70
71
  </div>
71
72
  {title || explanation ? (
@@ -180,6 +180,11 @@
180
180
 
181
181
  &__clear-button {
182
182
  align-self: center;
183
+ display: none;
184
+
185
+ &--visible {
186
+ display: block;
187
+ }
183
188
  }
184
189
 
185
190
  &__close-button,
@@ -1,11 +1,13 @@
1
+ import classNames from 'classnames';
1
2
  import React, { useRef, useState } from 'react';
2
3
 
3
- import Text from '../text';
4
- import SearchIcon from '../icons/24/search';
5
- import CloseIcon from '../icons/18/close';
6
- import useInputValidation from '../use-input-validation';
4
+ import Button from '../button';
7
5
  import useClickOutside from '../hooks/use-click-outside';
8
6
  import { useIsMounted } from '../hooks/use-is-mounted';
7
+ import CloseIcon from '../icons/18/close';
8
+ import SearchIcon from '../icons/24/search';
9
+ import Text from '../text';
10
+ import useInputValidation from '../use-input-validation';
9
11
 
10
12
  import {
11
13
  Search as Props,
@@ -14,8 +16,6 @@ import {
14
16
  variants,
15
17
  expandDirections,
16
18
  } from './search.types';
17
- import classNames from 'classnames';
18
- import Button from '../button';
19
19
 
20
20
  const RefSearch = React.forwardRef<HTMLInputElement, Props>(
21
21
  (
@@ -28,6 +28,7 @@ const RefSearch = React.forwardRef<HTMLInputElement, Props>(
28
28
  isLabelVisible = true,
29
29
  name,
30
30
  onChangeFunc = () => {},
31
+ onClear = () => {},
31
32
  expandable,
32
33
  placeholder,
33
34
  required,
@@ -41,7 +42,7 @@ const RefSearch = React.forwardRef<HTMLInputElement, Props>(
41
42
  },
42
43
  ref,
43
44
  ) => {
44
- const [validationOnChange, onInvalid, error] =
45
+ const [validationOnChange, handleInvalid, error] =
45
46
  useInputValidation(customErrorMessages);
46
47
  const searchArea = useRef<HTMLDivElement>(null);
47
48
  const [expand, setExpand] = useState(false);
@@ -51,7 +52,25 @@ const RefSearch = React.forwardRef<HTMLInputElement, Props>(
51
52
 
52
53
  const [inputValue, setInputValue] = useState(value);
53
54
 
54
- const onMessageChange = (message: string) => setInputValue(message);
55
+ const setValue = (value: string) => {
56
+ onChangeFunc(value);
57
+ setInputValue(value);
58
+ };
59
+
60
+ const handleChange: React.ChangeEventHandler<HTMLInputElement> = e => {
61
+ setValue(e.target.value);
62
+ validationOnChange(e);
63
+ };
64
+
65
+ const handleClear = () => {
66
+ setValue('');
67
+ onClear();
68
+ };
69
+
70
+ const handleClose = () => {
71
+ handleClear();
72
+ setExpand(false);
73
+ };
55
74
 
56
75
  return (
57
76
  <div
@@ -106,14 +125,10 @@ const RefSearch = React.forwardRef<HTMLInputElement, Props>(
106
125
  })}
107
126
  disabled={disabled}
108
127
  name={name}
109
- onChange={e => {
110
- onChangeFunc(e.target.value);
111
- validationOnChange(e);
112
- onMessageChange(e.target.value);
113
- }}
128
+ onChange={handleChange}
114
129
  onClick={() => setExpand(true)}
115
130
  value={inputValue}
116
- onInvalid={e => onInvalid(e)}
131
+ onInvalid={handleInvalid}
117
132
  placeholder={placeholder}
118
133
  ref={ref}
119
134
  required={required}
@@ -125,27 +140,27 @@ const RefSearch = React.forwardRef<HTMLInputElement, Props>(
125
140
  }
126
141
  />
127
142
  </label>
128
- {inputValue ? (
143
+ <div
144
+ className={classNames('search__clear-button', {
145
+ 'search__clear-button--visible': inputValue,
146
+ })}
147
+ >
129
148
  <Button
130
149
  {...clear}
131
- className="search__clear-button"
132
- onClick={() => {
133
- onChangeFunc('');
134
- setInputValue('');
135
- }}
150
+ onClick={handleClear}
151
+ size={
152
+ inputSize === inputSizes.small
153
+ ? Button.sizes.small
154
+ : Button.sizes.basic
155
+ }
136
156
  type="button"
137
157
  variant={
138
158
  variant === variants.fixed
139
159
  ? Button.variants.fixedTertiary
140
160
  : Button.variants.tertiary
141
161
  }
142
- size={
143
- inputSize === inputSizes.small
144
- ? Button.sizes.small
145
- : Button.sizes.basic
146
- }
147
162
  />
148
- ) : null}
163
+ </div>
149
164
  {button ? (
150
165
  <Button
151
166
  {...button}
@@ -167,22 +182,19 @@ const RefSearch = React.forwardRef<HTMLInputElement, Props>(
167
182
  <Button
168
183
  {...expandable.close}
169
184
  className="search__close-button"
170
- variant={
171
- variant === variants.fixed
172
- ? Button.variants.fixedTertiary
173
- : Button.variants.tertiary
174
- }
185
+ onClick={handleClose}
175
186
  leadingIcon={<CloseIcon />}
176
- onClick={() => {
177
- setExpand(false);
178
- onChangeFunc('');
179
- setInputValue('');
180
- }}
181
187
  size={
182
188
  inputSize === inputSizes.small
183
189
  ? Button.sizes.small
184
190
  : Button.sizes.basic
185
191
  }
192
+ type="button"
193
+ variant={
194
+ variant === variants.fixed
195
+ ? Button.variants.fixedTertiary
196
+ : Button.variants.tertiary
197
+ }
186
198
  />
187
199
  ) : null}
188
200
  </div>
@@ -27,17 +27,18 @@ export type Search = {
27
27
  button?: Button;
28
28
  clear: Button;
29
29
  customErrorMessages?: Messages;
30
- inputSize?: ObjectValues<typeof inputSizes>; //NOTE: This is not named `size` due to InputHTMLProps having a `size` attribute that will clash
31
- label: string;
32
- isLabelVisible?: boolean;
33
- name: string;
34
- onChangeFunc?: (arg: string) => void;
35
30
  expandable?: {
36
31
  close: Button;
37
32
  direction?: ObjectValues<typeof expandDirections>;
38
33
  };
34
+ helpIcon?: ReactElement;
35
+ helpText?: string;
36
+ inputSize?: ObjectValues<typeof inputSizes>; //NOTE: This is not named `size` due to InputHTMLProps having a `size` attribute that will clash
37
+ isLabelVisible?: boolean;
38
+ label: string;
39
+ name: string;
40
+ onChangeFunc?: (arg: string) => void;
41
+ onClear?: () => void;
39
42
  theme?: ObjectValues<typeof themes>;
40
43
  variant?: ObjectValues<typeof variants>;
41
- helpText?: string;
42
- helpIcon?: ReactElement;
43
44
  } & InputHtmlProps;
package/react/types.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import {
2
2
  AnchorHTMLAttributes,
3
3
  ButtonHTMLAttributes,
4
+ HTMLAttributes,
4
5
  ImgHTMLAttributes,
5
6
  InputHTMLAttributes,
6
7
  OptionHTMLAttributes,
@@ -29,6 +30,7 @@ export type ObjectValues<T> = Flatten<T[keyof T]>;
29
30
 
30
31
  // NOTE: The below types include all of the supported props for "html" react components (like `button` or `a`).
31
32
  export type ButtonHtmlProps = ButtonHTMLAttributes<HTMLButtonElement>;
33
+ export type DivHtmlProps = HTMLAttributes<HTMLDivElement>;
32
34
  export type LinkHtmlProps = AnchorHTMLAttributes<HTMLAnchorElement>;
33
35
  export type InputHtmlProps = InputHTMLAttributes<HTMLInputElement>;
34
36
  export type ImageHtmlProps = ImgHTMLAttributes<HTMLImageElement>;
@@ -0,0 +1,36 @@
1
+ import type { ValueOf } from '../indexed-access-type';
2
+ import { sizes as textSizes, themes as textThemes } from '../text/text.types';
3
+ import { variants, sizes } from './visually-button.types';
4
+
5
+ export const italicMap: Partial<Record<ValueOf<typeof variants>, true>> = {
6
+ cta: true,
7
+ 'cta-secondary': true,
8
+ 'cta-secondary-white': true,
9
+ };
10
+
11
+ export const sizeMap: Record<keyof typeof sizes, keyof typeof textSizes> = {
12
+ small: textSizes.small,
13
+ basic: textSizes.basic,
14
+ large: textSizes.large,
15
+ };
16
+
17
+ export const themeMap: Record<
18
+ ValueOf<typeof variants>,
19
+ keyof typeof textThemes
20
+ > = {
21
+ complete: textThemes.emphasis,
22
+ cta: textThemes.headline,
23
+ 'cta-secondary': textThemes.headline,
24
+ 'cta-secondary-white': textThemes.headline,
25
+ primary: textThemes.emphasis,
26
+ 'primary-destructive': textThemes.emphasis,
27
+ 'primary-white': textThemes.emphasis,
28
+ secondary: textThemes.emphasis,
29
+ 'secondary-white': textThemes.emphasis,
30
+ 'secondary-destructive': textThemes.emphasis,
31
+ tertiary: textThemes.emphasis,
32
+ 'tertiary-destructive': textThemes.emphasis,
33
+ 'fixed-tertiary': textThemes.emphasis,
34
+ waitlist: textThemes.emphasis,
35
+ 'waitlist-secondary': textThemes.emphasis,
36
+ };
@@ -102,7 +102,7 @@
102
102
  }
103
103
 
104
104
  &[disabled] {
105
- &.visually-button--theme-spinner {
105
+ &.visually-button--spinning {
106
106
  background: light.$buttons-cta-default;
107
107
  color: transparent;
108
108
 
@@ -129,7 +129,7 @@
129
129
  }
130
130
 
131
131
  &[disabled] {
132
- &.visually-button--theme-spinner {
132
+ &.visually-button--spinning {
133
133
  background: transparent;
134
134
  color: transparent;
135
135
  border-color: light.$buttons-secondary-default !important; //NOTE: This overrites the default "disabled" color for spinner style.
@@ -154,7 +154,7 @@
154
154
  }
155
155
 
156
156
  &[disabled] {
157
- &.visually-button--theme-spinner {
157
+ &.visually-button--spinning {
158
158
  background: transparent;
159
159
  color: transparent;
160
160
  border-color: light.$buttons-clean-secondary-outline;
@@ -178,7 +178,7 @@
178
178
  }
179
179
 
180
180
  &[disabled] {
181
- &.visually-button--theme-spinner {
181
+ &.visually-button--spinning {
182
182
  background: transparent;
183
183
  color: transparent;
184
184
  border-color: light.$buttons-destructive-outlined-default;
@@ -204,7 +204,7 @@
204
204
  }
205
205
 
206
206
  &[disabled] {
207
- &.visually-button--theme-spinner {
207
+ &.visually-button--spinning {
208
208
  background: light.$buttons-primary-default;
209
209
  color: transparent;
210
210
 
@@ -229,7 +229,7 @@
229
229
  }
230
230
 
231
231
  &[disabled] {
232
- &.visually-button--theme-spinner {
232
+ &.visually-button--spinning {
233
233
  background: light.$buttons-clean-default;
234
234
  color: light.$on-buttons-on-clean-default;
235
235
 
@@ -254,7 +254,7 @@
254
254
  }
255
255
 
256
256
  &[disabled] {
257
- &.visually-button--theme-spinner {
257
+ &.visually-button--spinning {
258
258
  background: light.$buttons-destructive-default;
259
259
  color: light.$on-buttons-on-destructive-default;
260
260
 
@@ -289,7 +289,7 @@
289
289
  }
290
290
 
291
291
  &[disabled] {
292
- &.visually-button--theme-spinner {
292
+ &.visually-button--spinning {
293
293
  color: transparent;
294
294
 
295
295
  .visually-button__spinner {
@@ -319,7 +319,7 @@
319
319
  }
320
320
 
321
321
  &[disabled] {
322
- &.visually-button--theme-spinner {
322
+ &.visually-button--spinning {
323
323
  .visually-button__spinner {
324
324
  display: flex;
325
325
  color: light.$on-buttons-on-destructive-outlined-default;
@@ -346,7 +346,7 @@
346
346
  }
347
347
 
348
348
  &[disabled] {
349
- &.visually-button--theme-spinner {
349
+ &.visually-button--spinning {
350
350
  .visually-button__spinner {
351
351
  display: flex;
352
352
  color: light.$on-buttons-on-fixed-link-default;
@@ -368,7 +368,7 @@
368
368
  }
369
369
 
370
370
  &[disabled] {
371
- &.visually-button--theme-spinner {
371
+ &.visually-button--spinning {
372
372
  background: light.$buttons-waiting-list-default;
373
373
  color: transparent;
374
374
 
@@ -394,7 +394,7 @@
394
394
  }
395
395
 
396
396
  &[disabled] {
397
- &.visually-button--theme-spinner {
397
+ &.visually-button--spinning {
398
398
  background: transparent;
399
399
  color: transparent;
400
400
  border-color: light.$buttons-waiting-list-outlined-default !important; //NOTE: This overrides the default "disabled" color for spinner style.
@@ -408,21 +408,17 @@
408
408
  }
409
409
 
410
410
  &__icon {
411
- display: block;
411
+ display: grid;
412
412
  height: 19px;
413
+ place-items: center;
413
414
  width: 19px;
414
- position: relative;
415
415
 
416
- > * {
416
+ svg {
417
417
  display: block;
418
- left: 50%;
419
- position: absolute;
420
- top: 50%;
421
- transform: translate(-50%, -50%);
422
418
  }
423
419
 
424
420
  &[disabled] {
425
- &.visually-button--spinner {
421
+ &.visually-button--spinning {
426
422
  .visually-button__spinner {
427
423
  display: flex;
428
424
  }
@@ -2,104 +2,78 @@ import cn from 'classnames';
2
2
  import * as React from 'react';
3
3
 
4
4
  import Text from '../text';
5
- import type { ValueOf } from '../indexed-access-type';
6
- import { themes as textThemes, sizes as textSizes } from '../text/text.types';
7
5
 
6
+ import { italicMap, sizeMap, themeMap } from './maps';
8
7
  import {
9
8
  sizes,
10
- themes,
11
9
  variants,
12
- VisuallyButton as Props,
10
+ type VisuallyButton as Props,
13
11
  } from './visually-button.types';
14
12
 
15
- const italicMap: Partial<Record<ValueOf<typeof variants>, true>> = {
16
- cta: true,
17
- 'cta-secondary': true,
18
- 'cta-secondary-white': true,
19
- };
20
-
21
- const sizeMap: Record<keyof typeof sizes, keyof typeof textSizes> = {
22
- small: textSizes.small,
23
- basic: textSizes.basic,
24
- large: textSizes.large,
25
- };
26
-
27
- const themeMap: Record<ValueOf<typeof variants>, keyof typeof textThemes> = {
28
- complete: textThemes.emphasis,
29
- cta: textThemes.headline,
30
- 'cta-secondary': textThemes.headline,
31
- 'cta-secondary-white': textThemes.headline,
32
- primary: textThemes.emphasis,
33
- 'primary-destructive': textThemes.emphasis,
34
- 'primary-white': textThemes.emphasis,
35
- secondary: textThemes.emphasis,
36
- 'secondary-white': textThemes.emphasis,
37
- 'secondary-destructive': textThemes.emphasis,
38
- tertiary: textThemes.emphasis,
39
- 'tertiary-destructive': textThemes.emphasis,
40
- 'fixed-tertiary': textThemes.emphasis,
41
- waitlist: textThemes.emphasis,
42
- 'waitlist-secondary': textThemes.emphasis,
43
- };
44
-
45
- const VisuallyButton: React.FunctionComponent<Props> & {
46
- sizes: typeof sizes;
47
- variants: typeof variants;
48
- themes: typeof themes;
49
- } = ({
50
- ariaLabel,
51
- className,
52
- elementName = 'button',
53
- leadingIcon,
54
- trailingIcon,
55
- hasStackedIcon,
56
- size = sizes.basic,
57
- testId,
58
- text,
59
- theme = themes.normal,
60
- variant = variants.primary,
61
- wide,
62
- ...rest
63
- }) =>
64
- React.createElement(
65
- elementName,
13
+ const VisuallyButtonRef = React.forwardRef<HTMLElement, Props>(
14
+ (
66
15
  {
67
- 'aria-label': leadingIcon && ariaLabel ? ariaLabel : undefined,
68
- className: cn(
69
- 'visually-button',
70
- {
71
- 'visually-button--icon': leadingIcon && !text,
72
- 'visually-button--icon-text': (leadingIcon || trailingIcon) && text,
73
- 'visually-button--icon-text--stacked': hasStackedIcon,
74
- [`visually-button--size-${size}`]: size,
75
- [`visually-button--variant-${variant}`]: variant,
76
- 'visually-button--wide': wide,
77
- [`visually-button--theme-${theme}`]: theme
78
- ? themes[theme]
79
- : undefined,
80
- },
81
- className,
82
- ),
83
- 'data-testid': testId,
84
- ...rest,
16
+ iconOnly,
17
+ className,
18
+ elementName,
19
+ leadingIcon,
20
+ trailingIcon,
21
+ hasStackedIcon,
22
+ size = sizes.basic,
23
+ spinning,
24
+ testId,
25
+ text,
26
+ variant = variants.primary,
27
+ wide,
28
+ ...rest
85
29
  },
86
- <React.Fragment>
87
- {leadingIcon ? (
30
+ ref,
31
+ ) => {
32
+ const hasIcon = Boolean(leadingIcon || trailingIcon);
33
+ const renderIconOnly = Boolean(iconOnly && hasIcon);
34
+ const role = renderIconOnly && elementName === 'div' ? 'img' : undefined;
35
+
36
+ return React.createElement(
37
+ elementName,
38
+ {
39
+ 'aria-label': renderIconOnly ? text : undefined,
40
+ className: cn(
41
+ 'visually-button',
42
+ {
43
+ 'visually-button--icon': renderIconOnly,
44
+ 'visually-button--icon-text':
45
+ (leadingIcon || trailingIcon) && !iconOnly,
46
+ 'visually-button--icon-text--stacked': hasStackedIcon,
47
+ [`visually-button--size-${size}`]: size,
48
+ 'visually-button--spinning': spinning,
49
+ [`visually-button--variant-${variant}`]: variant,
50
+ 'visually-button--wide': wide,
51
+ },
52
+ className,
53
+ ),
54
+ 'data-testid': testId,
55
+ role,
56
+ ...rest,
57
+ ref,
58
+ },
59
+ leadingIcon ? (
88
60
  <div className="visually-button__icon">{leadingIcon}</div>
89
- ) : null}
90
- <Text
91
- className="visually-button__text"
92
- italic={italicMap[variant]}
93
- size={sizeMap[size]}
94
- theme={themeMap[variant]}
95
- elementName="span"
96
- >
97
- {text}
98
- </Text>
99
- {trailingIcon ? (
61
+ ) : null,
62
+ renderIconOnly ? null : (
63
+ <Text
64
+ className="visually-button__text"
65
+ italic={italicMap[variant]}
66
+ size={sizeMap[size]}
67
+ theme={themeMap[variant]}
68
+ elementName="span"
69
+ >
70
+ {text}
71
+ </Text>
72
+ ),
73
+ trailingIcon ? (
100
74
  <div className="visually-button__icon">{trailingIcon}</div>
101
- ) : null}
102
- {theme === 'spinner' && (
75
+ ) : null,
76
+ spinning ? (
103
77
  <div className="visually-button__spinner">
104
78
  <svg
105
79
  className="visually-button__spinner-icon"
@@ -119,12 +93,19 @@ const VisuallyButton: React.FunctionComponent<Props> & {
119
93
  />
120
94
  </svg>
121
95
  </div>
122
- )}
123
- </React.Fragment>,
124
- );
96
+ ) : null,
97
+ );
98
+ },
99
+ );
100
+
101
+ VisuallyButtonRef.displayName = 'VisuallyButton';
125
102
 
126
- VisuallyButton.sizes = sizes;
127
- VisuallyButton.variants = variants;
128
- VisuallyButton.themes = themes;
103
+ const VisuallyButton: typeof VisuallyButtonRef & {
104
+ sizes: typeof sizes;
105
+ variants: typeof variants;
106
+ } = Object.assign(VisuallyButtonRef, {
107
+ sizes,
108
+ variants,
109
+ });
129
110
 
130
111
  export default VisuallyButton;
@@ -1,6 +1,11 @@
1
- import { ReactElement } from 'react';
1
+ import type { ReactElement } from 'react';
2
2
 
3
- import { ButtonHtmlProps, ObjectValues, LinkHtmlProps } from '../types';
3
+ import type {
4
+ ButtonHtmlProps,
5
+ DivHtmlProps,
6
+ LinkHtmlProps,
7
+ ObjectValues,
8
+ } from '../types';
4
9
 
5
10
  export const sizes = {
6
11
  small: 'small',
@@ -26,46 +31,29 @@ export const variants = {
26
31
  waitlistSecondary: 'waitlist-secondary',
27
32
  } as const;
28
33
 
29
- export const themes = {
30
- spinner: 'spinner',
31
- normal: 'normal',
32
- } as const;
33
-
34
- type TextProp = {
35
- text: string;
36
- ariaLabel?: never;
37
- };
38
-
39
- type AriaLabelProp = {
40
- ariaLabel: string;
41
- text?: never;
42
- };
43
-
44
- type BasicProps = {
34
+ type CommonProps<ElementName extends string> = {
45
35
  className?: string;
46
- elementName?: string;
47
- leadingIcon?: ReactElement;
48
- trailingIcon?: ReactElement;
36
+ elementName: ElementName;
49
37
  hasStackedIcon?: boolean;
38
+ iconOnly?: boolean;
39
+ leadingIcon?: ReactElement;
50
40
  size?: ObjectValues<typeof sizes>;
41
+ spinning?: boolean;
51
42
  testId?: string;
52
- theme?: ObjectValues<typeof themes>;
43
+ text: string;
44
+ trailingIcon?: ReactElement;
53
45
  variant?: ObjectValues<typeof variants>;
54
46
  wide?: boolean;
55
47
  };
56
48
 
57
- type CommonProps = (BasicProps & TextProp) | (BasicProps & AriaLabelProp);
49
+ type Props<ElementProps, ElementName extends string> = ElementProps &
50
+ CommonProps<ElementName>;
58
51
 
59
- export type VisuallyButtonButton = CommonProps & ButtonHtmlProps;
60
-
61
- export type VisuallyButtonLink = CommonProps & LinkHtmlProps;
62
-
63
- // NOTE: If the element isn't `a` or `button` we don't restrict the type of the rest props
64
- type VisuallyButtonOther = CommonProps & {
65
- [key: string]: unknown;
66
- };
52
+ export type VisuallyButtonButton = Props<ButtonHtmlProps, 'button'>;
53
+ export type VisuallyButtonDiv = Props<DivHtmlProps, 'div'>;
54
+ export type VisuallyButtonLink = Props<LinkHtmlProps, 'a'> & { href: string };
67
55
 
68
56
  export type VisuallyButton =
69
- | ({ elementName: 'button' } & VisuallyButtonButton)
70
- | ({ elementName: 'a' } & VisuallyButtonLink)
71
- | ({ elementName: string } & VisuallyButtonOther);
57
+ | VisuallyButtonButton
58
+ | VisuallyButtonDiv
59
+ | VisuallyButtonLink;