@transferwise/components 46.30.1 → 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.
- package/build/index.js +190 -57
- package/build/index.js.map +1 -1
- package/build/index.mjs +189 -58
- package/build/index.mjs.map +1 -1
- package/build/types/dateInput/DateInput.d.ts +5 -4
- package/build/types/dateInput/DateInput.d.ts.map +1 -1
- package/build/types/dateLookup/DateLookup.d.ts +11 -4
- package/build/types/dateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/field/Field.d.ts +12 -0
- package/build/types/field/Field.d.ts.map +1 -0
- package/build/types/index.d.ts +4 -0
- package/build/types/index.d.ts.map +1 -1
- package/build/types/inputs/Input.d.ts.map +1 -1
- package/build/types/inputs/SelectInput.d.ts +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/inputs/TextArea.d.ts.map +1 -1
- package/build/types/inputs/_common.d.ts +2 -2
- package/build/types/inputs/_common.d.ts.map +1 -1
- package/build/types/inputs/contexts.d.ts +24 -0
- package/build/types/inputs/contexts.d.ts.map +1 -0
- package/build/types/label/Label.d.ts +9 -0
- package/build/types/label/Label.d.ts.map +1 -0
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts +1 -1
- package/build/types/phoneNumberInput/PhoneNumberInput.d.ts.map +1 -1
- package/build/types/radioGroup/RadioGroup.d.ts.map +1 -1
- package/build/types/switch/Switch.d.ts +6 -3
- package/build/types/switch/Switch.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/dateInput/DateInput.rtl.spec.tsx +17 -0
- package/src/dateInput/DateInput.tsx +28 -22
- package/src/dateLookup/DateLookup.keyboardEvents.spec.js +2 -2
- package/src/dateLookup/DateLookup.rtl.spec.tsx +21 -0
- package/src/dateLookup/DateLookup.state.spec.js +5 -5
- package/src/dateLookup/DateLookup.tests.story.tsx +4 -11
- package/src/dateLookup/DateLookup.tsx +24 -9
- package/src/dateLookup/DateLookup.view.spec.js +11 -11
- package/src/field/Field.spec.tsx +95 -0
- package/src/field/Field.story.tsx +59 -0
- package/src/field/Field.tsx +70 -0
- package/src/index.ts +4 -0
- package/src/inputs/Input.tsx +5 -3
- package/src/inputs/SelectInput.spec.tsx +10 -0
- package/src/inputs/SelectInput.tsx +9 -4
- package/src/inputs/TextArea.tsx +6 -3
- package/src/inputs/_ButtonInput.tsx +2 -2
- package/src/inputs/_common.ts +2 -2
- package/src/inputs/contexts.tsx +45 -0
- package/src/label/Label.spec.tsx +26 -0
- package/src/label/Label.story.tsx +37 -0
- package/src/label/Label.tsx +20 -0
- package/src/phoneNumberInput/PhoneNumberInput.story.tsx +16 -22
- package/src/phoneNumberInput/PhoneNumberInput.tsx +14 -2
- package/src/radioGroup/RadioGroup.rtl.spec.tsx +14 -0
- package/src/radioGroup/RadioGroup.story.tsx +26 -0
- package/src/radioGroup/RadioGroup.tsx +4 -1
- package/src/switch/Switch.spec.tsx +10 -0
- package/src/switch/Switch.tsx +22 -13
- 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 {
|
|
1
|
+
import type { Meta, StoryObj } from '@storybook/react';
|
|
2
2
|
|
|
3
3
|
import PhoneNumberInput from './PhoneNumberInput';
|
|
4
4
|
|
|
5
|
-
|
|
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
|
|
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
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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?:
|
|
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
|
});
|
package/src/switch/Switch.tsx
CHANGED
|
@@ -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 {
|
|
6
|
+
import type { CommonProps } from '../common';
|
|
7
|
+
import { useInputAttributes } from '../inputs/contexts';
|
|
8
8
|
|
|
9
9
|
export type SwitchProps = CommonProps & {
|
|
10
|
-
/**
|
|
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 {
|
|
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
|
|
54
|
-
|
|
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
|
|
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}
|