@transferwise/components 46.30.2 → 46.31.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.
Files changed (58) hide show
  1. package/build/index.js +190 -57
  2. package/build/index.js.map +1 -1
  3. package/build/index.mjs +189 -58
  4. package/build/index.mjs.map +1 -1
  5. package/build/types/dateInput/DateInput.d.ts +5 -4
  6. package/build/types/dateInput/DateInput.d.ts.map +1 -1
  7. package/build/types/dateLookup/DateLookup.d.ts +11 -4
  8. package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
  9. package/build/types/field/Field.d.ts +12 -0
  10. package/build/types/field/Field.d.ts.map +1 -0
  11. package/build/types/index.d.ts +4 -0
  12. package/build/types/index.d.ts.map +1 -1
  13. package/build/types/inputs/Input.d.ts.map +1 -1
  14. package/build/types/inputs/SelectInput.d.ts +1 -1
  15. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  16. package/build/types/inputs/TextArea.d.ts.map +1 -1
  17. package/build/types/inputs/_common.d.ts +2 -2
  18. package/build/types/inputs/_common.d.ts.map +1 -1
  19. package/build/types/inputs/contexts.d.ts +24 -0
  20. package/build/types/inputs/contexts.d.ts.map +1 -0
  21. package/build/types/label/Label.d.ts +9 -0
  22. package/build/types/label/Label.d.ts.map +1 -0
  23. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +1 -1
  24. package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
  25. package/build/types/radioGroup/RadioGroup.d.ts.map +1 -1
  26. package/build/types/switch/Switch.d.ts +6 -3
  27. package/build/types/switch/Switch.d.ts.map +1 -1
  28. package/package.json +3 -3
  29. package/src/dateInput/DateInput.rtl.spec.tsx +17 -0
  30. package/src/dateInput/DateInput.tsx +28 -22
  31. package/src/dateLookup/DateLookup.keyboardEvents.spec.js +2 -2
  32. package/src/dateLookup/DateLookup.rtl.spec.tsx +21 -0
  33. package/src/dateLookup/DateLookup.state.spec.js +5 -5
  34. package/src/dateLookup/DateLookup.tests.story.tsx +4 -11
  35. package/src/dateLookup/DateLookup.tsx +24 -9
  36. package/src/dateLookup/DateLookup.view.spec.js +11 -11
  37. package/src/field/Field.spec.tsx +95 -0
  38. package/src/field/Field.story.tsx +59 -0
  39. package/src/field/Field.tsx +70 -0
  40. package/src/index.ts +4 -0
  41. package/src/inputs/Input.tsx +5 -3
  42. package/src/inputs/SelectInput.spec.tsx +10 -0
  43. package/src/inputs/SelectInput.tsx +9 -4
  44. package/src/inputs/TextArea.tsx +6 -3
  45. package/src/inputs/_ButtonInput.tsx +2 -2
  46. package/src/inputs/_common.ts +2 -2
  47. package/src/inputs/contexts.tsx +45 -0
  48. package/src/label/Label.spec.tsx +26 -0
  49. package/src/label/Label.story.tsx +37 -0
  50. package/src/label/Label.tsx +20 -0
  51. package/src/phoneNumberInput/PhoneNumberInput.story.tsx +16 -22
  52. package/src/phoneNumberInput/PhoneNumberInput.tsx +14 -2
  53. package/src/radioGroup/RadioGroup.rtl.spec.tsx +14 -0
  54. package/src/radioGroup/RadioGroup.story.tsx +26 -0
  55. package/src/radioGroup/RadioGroup.tsx +4 -1
  56. package/src/switch/Switch.spec.tsx +10 -0
  57. package/src/switch/Switch.tsx +22 -13
  58. package/src/utilities/logActionRequired.js +1 -1
@@ -0,0 +1,20 @@
1
+ import classNames from 'classnames';
2
+
3
+ export type LabelProps = {
4
+ id?: string;
5
+ htmlFor?: string;
6
+ className?: string;
7
+ children?: React.ReactNode;
8
+ };
9
+
10
+ export const Label = ({ id, htmlFor, className, children }: LabelProps) => {
11
+ return (
12
+ <label
13
+ id={id}
14
+ htmlFor={htmlFor}
15
+ className={classNames('control-label d-flex flex-column gap-y-1 m-b-0', className)}
16
+ >
17
+ {children}
18
+ </label>
19
+ );
20
+ };
@@ -1,29 +1,23 @@
1
- import { select, boolean, object } from '@storybook/addon-knobs';
1
+ import type { Meta, StoryObj } from '@storybook/react';
2
2
 
3
3
  import PhoneNumberInput from './PhoneNumberInput';
4
4
 
5
- export default {
5
+ const meta = {
6
6
  component: PhoneNumberInput,
7
7
  title: 'Forms/PhoneNumberInput',
8
- };
8
+ tags: ['autodocs'],
9
+ } satisfies Meta<typeof PhoneNumberInput>;
9
10
 
10
- export const Basic = () => {
11
- const disabled = boolean('disabled', false);
12
- const required = boolean('required', false);
13
- const size = select('size', ['sm', 'md', 'lg'], 'md');
14
- const selectProps = object('selectProps', {
15
- className: 'custom-class',
16
- });
11
+ export default meta;
17
12
 
18
- return (
19
- <PhoneNumberInput
20
- disabled={disabled}
21
- required={required}
22
- size={size}
23
- selectProps={selectProps}
24
- searchPlaceholder="searchPlaceholder"
25
- placeholder="placeholder"
26
- onChange={console.log}
27
- />
28
- );
29
- };
13
+ type Story = StoryObj<typeof meta>;
14
+
15
+ export const Basic = {
16
+ args: {
17
+ searchPlaceholder: 'searchPlaceholder',
18
+ placeholder: 'placeholder',
19
+ selectProps: {
20
+ className: 'custom-class',
21
+ },
22
+ },
23
+ } satisfies Story;
@@ -28,7 +28,7 @@ export interface PhoneNumberInputProps {
28
28
  initialValue?: string;
29
29
  onChange: (value: string | null, prefix: string) => void;
30
30
  onFocus?: React.FocusEventHandler<HTMLInputElement>;
31
- onBlur?: React.FocusEventHandler<HTMLInputElement>;
31
+ onBlur?: () => void;
32
32
  countryCode?: string;
33
33
  searchPlaceholder?: string;
34
34
  size?: SizeSmall | SizeMedium | SizeLarge;
@@ -73,6 +73,13 @@ const PhoneNumberInput = ({
73
73
  });
74
74
  const [broadcastedValue, setBroadcastedValue] = useState<PhoneNumber | null>(null);
75
75
 
76
+ const [suffixDirty, setSuffixDirty] = useState(false);
77
+ useEffect(() => {
78
+ if (internalValue.suffix) {
79
+ setSuffixDirty(true);
80
+ }
81
+ }, [internalValue.suffix]);
82
+
76
83
  const countriesByPrefix = useMemo(
77
84
  () =>
78
85
  groupCountriesByPrefix(
@@ -168,6 +175,11 @@ const PhoneNumberInput = ({
168
175
  const country = prefix != null ? findCountryByPrefix(prefix) : null;
169
176
  setInternalValue((prev) => ({ ...prev, prefix, format: country?.phoneFormat }));
170
177
  }}
178
+ onClose={() => {
179
+ if (suffixDirty) {
180
+ onBlur?.();
181
+ }
182
+ }}
171
183
  {...selectProps}
172
184
  />
173
185
  </div>
@@ -186,7 +198,7 @@ const PhoneNumberInput = ({
186
198
  onChange={onSuffixChange}
187
199
  onPaste={onPaste}
188
200
  onFocus={onFocus}
189
- onBlur={onBlur}
201
+ onBlur={() => onBlur?.()}
190
202
  />
191
203
  </div>
192
204
  </div>
@@ -1,6 +1,7 @@
1
1
  import { render, screen } from '@testing-library/react';
2
2
 
3
3
  import RadioGroup from '.';
4
+ import { Field } from '../field/Field';
4
5
 
5
6
  describe('RadioGroup', () => {
6
7
  it('has accessible role', () => {
@@ -13,4 +14,17 @@ describe('RadioGroup', () => {
13
14
  );
14
15
  expect(screen.getByRole('radiogroup')).toBeInTheDocument();
15
16
  });
17
+
18
+ it('supports `Field` for labeling', () => {
19
+ render(
20
+ <Field label="Currency">
21
+ <RadioGroup
22
+ name="currency"
23
+ radios={[{ label: 'USD' }, { label: 'EUR' }]}
24
+ onChange={() => {}}
25
+ />
26
+ </Field>,
27
+ );
28
+ expect(screen.getByRole('radiogroup')).toHaveAccessibleName(/^Currency/);
29
+ });
16
30
  });
@@ -5,6 +5,7 @@ import { Flag } from '@wise/art';
5
5
  import Avatar, { AvatarType } from '../avatar';
6
6
 
7
7
  import RadioGroup from './RadioGroup';
8
+ import { Field } from '../field/Field';
8
9
 
9
10
  export default {
10
11
  component: RadioGroup,
@@ -53,3 +54,28 @@ export const Basic = () => {
53
54
  </div>
54
55
  );
55
56
  };
57
+
58
+ export const Labeled = () => {
59
+ return (
60
+ <Field label="Do you like our product?">
61
+ <RadioGroup
62
+ name="radio-group"
63
+ radios={[
64
+ {
65
+ value: 'yes',
66
+ label: 'Yes',
67
+ },
68
+ {
69
+ value: 'definitely',
70
+ label: 'Definitely',
71
+ },
72
+ {
73
+ value: 'absolutely',
74
+ label: 'Absolutely',
75
+ },
76
+ ]}
77
+ onChange={(v) => action(v)}
78
+ />
79
+ </Field>
80
+ );
81
+ };
@@ -2,6 +2,7 @@ import { useState } from 'react';
2
2
 
3
3
  import Radio from '../radio';
4
4
  import { RadioProps } from '../radio/Radio';
5
+ import { useInputAttributes } from '../inputs/contexts';
5
6
 
6
7
  export type RadioGroupRadio<T extends string | number = string> = Omit<
7
8
  RadioProps<T>,
@@ -21,10 +22,12 @@ export default function RadioGroup<T extends string | number = never>({
21
22
  selectedValue: controlledValue,
22
23
  onChange,
23
24
  }: RadioGroupProps<T>) {
25
+ const inputAttributes = useInputAttributes({ nonLabelable: true });
26
+
24
27
  const [uncontrolledValue, setUncontrolledValue] = useState(controlledValue);
25
28
 
26
29
  return radios.length > 0 ? (
27
- <div role="radiogroup">
30
+ <div role="radiogroup" {...inputAttributes}>
28
31
  {radios.map(({ value = '' as T, ...restProps }, index) => (
29
32
  <Radio
30
33
  // eslint-disable-next-line react/no-array-index-key
@@ -1,3 +1,4 @@
1
+ import { Field } from '../field/Field';
1
2
  import { render, fireEvent, screen } from '../test-utils';
2
3
 
3
4
  import Switch from './Switch';
@@ -81,4 +82,13 @@ describe('Switch', () => {
81
82
  fireEvent.click(input);
82
83
  expect(mockCallback).not.toHaveBeenCalled();
83
84
  });
85
+
86
+ it('supports `Field` for labeling', () => {
87
+ render(
88
+ <Field label="Dark mode">
89
+ <Switch checked onClick={props.onClick} />
90
+ </Field>,
91
+ );
92
+ expect(screen.getByLabelText('Dark mode')).toHaveAttribute('role', 'switch');
93
+ });
84
94
  });
@@ -1,13 +1,16 @@
1
1
  import { CheckCircleFill, CrossCircleFill } from '@transferwise/icons';
2
2
  import { useTheme } from '@wise/components-theming';
3
3
  import classnames from 'classnames';
4
- import { KeyboardEventHandler, MouseEvent } from 'react';
4
+ import type { KeyboardEventHandler, MouseEvent } from 'react';
5
5
 
6
- import { CommonProps } from '../common';
7
- import { logActionRequiredIf } from '../utilities';
6
+ import type { CommonProps } from '../common';
7
+ import { useInputAttributes } from '../inputs/contexts';
8
8
 
9
9
  export type SwitchProps = CommonProps & {
10
- /** Used to describe the purpose of the switch. To be used if there is no external label (i.e. aria-labelledby is null) */
10
+ /**
11
+ * Used to describe the purpose of the switch. To be used if there is no external label (i.e. aria-labelledby is null)
12
+ * @deprecated Use `Field` wrapper or the `aria-labelledby` attribute instead.
13
+ */
11
14
  'aria-label'?: string;
12
15
  /** A reference to a label that describes the purpose of the switch. Ignored if aria-label is provided */
13
16
  'aria-labelledby'?: string;
@@ -21,8 +24,18 @@ export type SwitchProps = CommonProps & {
21
24
  };
22
25
 
23
26
  const Switch = (props: SwitchProps) => {
27
+ const inputAttributes = useInputAttributes({ nonLabelable: true });
28
+
24
29
  const { isModern } = useTheme();
25
- const { checked, className, id, onClick, disabled } = props;
30
+ const {
31
+ checked,
32
+ className,
33
+ id = inputAttributes.id,
34
+ 'aria-label': ariaLabel,
35
+ 'aria-labelledby': ariaLabelledbyProp,
36
+ onClick,
37
+ disabled,
38
+ } = props;
26
39
 
27
40
  const handleKeyDown: KeyboardEventHandler = (event) => {
28
41
  if (event.key === ' ') {
@@ -50,13 +63,8 @@ const Switch = (props: SwitchProps) => {
50
63
  );
51
64
  };
52
65
 
53
- const ariaLabel = props['aria-label'];
54
- const ariaLabelledby = ariaLabel ? undefined : props['aria-labelledby'];
55
-
56
- logActionRequiredIf(
57
- 'Switch now expects either `aria-label` or `aria-labelledby`, and will soon make these props required. Please update your usage to provide one or the other.',
58
- !ariaLabel && !ariaLabelledby,
59
- );
66
+ const ariaLabelledby =
67
+ (ariaLabel ? undefined : ariaLabelledbyProp) ?? inputAttributes['aria-labelledby'];
60
68
 
61
69
  return (
62
70
  <span
@@ -66,7 +74,7 @@ const Switch = (props: SwitchProps) => {
66
74
  {
67
75
  'np-switch--unchecked': !checked,
68
76
  'np-switch--checked': checked,
69
- disabled: disabled,
77
+ disabled,
70
78
  },
71
79
  className,
72
80
  )}
@@ -74,6 +82,7 @@ const Switch = (props: SwitchProps) => {
74
82
  role="switch"
75
83
  aria-checked={checked}
76
84
  aria-label={ariaLabel}
85
+ {...inputAttributes}
77
86
  aria-labelledby={ariaLabelledby}
78
87
  id={id}
79
88
  aria-disabled={disabled}
@@ -1,5 +1,5 @@
1
1
  export function logActionRequired(message) {
2
- if (['development', 'test'].includes(process?.env?.NODE_ENV)) {
2
+ if (typeof process !== 'undefined' && process.env?.NODE_ENV !== 'production') {
3
3
  // eslint-disable-next-line no-console
4
4
  console.warn(message);
5
5
  }