@transferwise/components 0.0.0-experimental-ad2ab5f → 0.0.0-experimental-9c03743
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/alert/Alert.js +1 -1
- package/build/alert/Alert.mjs +1 -1
- package/build/button/legacyUtils/legacyUtils.js +1 -1
- package/build/button/legacyUtils/legacyUtils.mjs +1 -1
- package/build/drawer/Drawer.js +1 -1
- package/build/drawer/Drawer.mjs +1 -1
- package/build/inputs/SelectInput.js +38 -4
- package/build/inputs/SelectInput.js.map +1 -1
- package/build/inputs/SelectInput.mjs +38 -4
- package/build/inputs/SelectInput.mjs.map +1 -1
- package/build/markdown/Markdown.js +1 -1
- package/build/markdown/Markdown.mjs +1 -1
- package/build/popover/Popover.js +1 -1
- package/build/popover/Popover.mjs +1 -1
- package/build/types/inputs/SelectInput.d.ts.map +1 -1
- package/build/types/utilities/deprecatedProperty/deprecatedProperty.d.ts +6 -8
- package/build/types/utilities/deprecatedProperty/deprecatedProperty.d.ts.map +1 -1
- package/build/types/utilities/deprecatedProperty/index.d.ts +1 -1
- package/build/types/utilities/deprecatedProperty/index.d.ts.map +1 -1
- package/build/types/utilities/index.d.ts +2 -2
- package/build/types/utilities/index.d.ts.map +1 -1
- package/build/types/utilities/logActionRequired.d.ts +2 -2
- package/build/types/utilities/logActionRequired.d.ts.map +1 -1
- package/build/utilities/logActionRequired.js.map +1 -1
- package/build/utilities/logActionRequired.mjs.map +1 -1
- package/package.json +3 -3
- package/src/inputWithDisplayFormat/InputWithDisplayFormat.story.js +85 -0
- package/src/inputs/SelectInput.docs.mdx +19 -0
- package/src/inputs/SelectInput.spec.tsx +80 -1
- package/src/inputs/SelectInput.story.tsx +2 -2
- package/src/inputs/SelectInput.tsx +44 -2
- package/src/moneyInput/MoneyInput.rtl.spec.tsx +9 -0
- package/src/utilities/deprecatedProperty/{deprecatedProperty.ts → deprecatedProperty.js} +4 -23
- package/src/utilities/{logActionRequired.ts → logActionRequired.js} +2 -2
- package/build/types/test-utils/style-mock.d.ts +0 -1
- package/build/types/test-utils/style-mock.d.ts.map +0 -1
- package/src/inputWithDisplayFormat/InputWithDisplayFormat.story.tsx +0 -78
- /package/src/test-utils/{style-mock.ts → style-mock.js} +0 -0
- /package/src/utilities/deprecatedProperty/{index.ts → index.js} +0 -0
- /package/src/utilities/{index.ts → index.js} +0 -0
|
@@ -3,7 +3,7 @@ import { userEvent } from '@testing-library/user-event';
|
|
|
3
3
|
|
|
4
4
|
import { render, mockMatchMedia, mockResizeObserver } from '../test-utils';
|
|
5
5
|
|
|
6
|
-
import { SelectInput } from './SelectInput';
|
|
6
|
+
import { SelectInput, type SelectInputOptionItem, type SelectInputProps } from './SelectInput';
|
|
7
7
|
import { Field } from '../field/Field';
|
|
8
8
|
|
|
9
9
|
mockMatchMedia();
|
|
@@ -216,4 +216,83 @@ describe('SelectInput', () => {
|
|
|
216
216
|
);
|
|
217
217
|
expect(screen.getByLabelText(/Currency/)).toHaveAttribute('aria-haspopup');
|
|
218
218
|
});
|
|
219
|
+
|
|
220
|
+
describe('listbox label', () => {
|
|
221
|
+
const fieldLabel = 'Fruits';
|
|
222
|
+
const triggerLabel = 'Select fruit';
|
|
223
|
+
const options: SelectInputOptionItem[] = [
|
|
224
|
+
{ type: 'option', value: 'Banana' },
|
|
225
|
+
{ type: 'option', value: 'Orange' },
|
|
226
|
+
{ type: 'option', value: 'Olive' },
|
|
227
|
+
];
|
|
228
|
+
const setup = (props: Omit<SelectInputProps<string | null>, 'items'> = {}) =>
|
|
229
|
+
render(
|
|
230
|
+
<Field label={fieldLabel} id="selectId">
|
|
231
|
+
<SelectInput {...props} items={options} />
|
|
232
|
+
</Field>,
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
it("should propagate trigger's label if nothing is selected", async () => {
|
|
236
|
+
setup({
|
|
237
|
+
UNSAFE_triggerButtonProps: {
|
|
238
|
+
id: undefined,
|
|
239
|
+
'aria-labelledby': undefined,
|
|
240
|
+
'aria-describedby': undefined,
|
|
241
|
+
'aria-invalid': undefined,
|
|
242
|
+
'aria-label': triggerLabel,
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
const trigger = screen.getByRole('combobox');
|
|
246
|
+
await userEvent.click(trigger);
|
|
247
|
+
expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("should propagate trigger's label if an option is selected", async () => {
|
|
251
|
+
setup({
|
|
252
|
+
UNSAFE_triggerButtonProps: {
|
|
253
|
+
id: undefined,
|
|
254
|
+
'aria-labelledby': undefined,
|
|
255
|
+
'aria-describedby': undefined,
|
|
256
|
+
'aria-invalid': undefined,
|
|
257
|
+
'aria-label': triggerLabel,
|
|
258
|
+
},
|
|
259
|
+
value: options[1].value,
|
|
260
|
+
});
|
|
261
|
+
const trigger = screen.getByRole('combobox');
|
|
262
|
+
await userEvent.click(trigger);
|
|
263
|
+
expect(screen.getByRole('listbox', { name: triggerLabel })).toBeInTheDocument();
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("should propagate trigger's label by id", async () => {
|
|
267
|
+
const customLabelId = 'customLabelId';
|
|
268
|
+
setup({
|
|
269
|
+
UNSAFE_triggerButtonProps: {
|
|
270
|
+
id: undefined,
|
|
271
|
+
'aria-labelledby': customLabelId,
|
|
272
|
+
'aria-describedby': undefined,
|
|
273
|
+
'aria-invalid': undefined,
|
|
274
|
+
'aria-label': undefined,
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
const trigger = screen.getByRole('combobox');
|
|
278
|
+
await userEvent.click(trigger);
|
|
279
|
+
expect(screen.getByRole('listbox')).toHaveAttribute('aria-labelledby', customLabelId);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("should propagate input's label by id", async () => {
|
|
283
|
+
setup();
|
|
284
|
+
const trigger = screen.getByRole('combobox');
|
|
285
|
+
await userEvent.click(trigger);
|
|
286
|
+
expect(screen.getByRole('listbox', { name: fieldLabel })).toBeInTheDocument();
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
it('should have no label if none of the above are provided', async () => {
|
|
290
|
+
render(<SelectInput items={options} />);
|
|
291
|
+
const trigger = screen.getByRole('combobox');
|
|
292
|
+
await userEvent.click(trigger);
|
|
293
|
+
const listBox = screen.getByRole('listbox');
|
|
294
|
+
expect(listBox).not.toHaveAttribute('aria-label');
|
|
295
|
+
expect(listBox).not.toHaveAttribute('aria-labelledby');
|
|
296
|
+
});
|
|
297
|
+
});
|
|
219
298
|
});
|
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
} from './SelectInput';
|
|
21
21
|
|
|
22
22
|
const meta = {
|
|
23
|
-
title: '
|
|
23
|
+
title: 'Forms/SelectInput',
|
|
24
24
|
component: SelectInput,
|
|
25
25
|
tags: ['autodocs'],
|
|
26
26
|
parameters: { actions: { argTypesRegex: '' } },
|
|
@@ -347,7 +347,7 @@ export const WithinField = {
|
|
|
347
347
|
args: Months.args,
|
|
348
348
|
decorators: [
|
|
349
349
|
(Story) => (
|
|
350
|
-
<Field message="Something went wrong" sentiment="negative">
|
|
350
|
+
<Field message="Something went wrong" sentiment="negative" label="Month">
|
|
351
351
|
<Story />
|
|
352
352
|
</Field>
|
|
353
353
|
),
|
|
@@ -258,8 +258,9 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
258
258
|
onClose,
|
|
259
259
|
onClear,
|
|
260
260
|
}: SelectInputProps<T, M>) {
|
|
261
|
-
const inputAttributes = useInputAttributes();
|
|
261
|
+
const inputAttributes = useInputAttributes({ nonLabelable: true });
|
|
262
262
|
const id = idProp ?? inputAttributes.id;
|
|
263
|
+
|
|
263
264
|
const [open, setOpen] = useState(false);
|
|
264
265
|
|
|
265
266
|
const initialized = useRef(false);
|
|
@@ -295,6 +296,40 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
295
296
|
const listboxRef = useRef<HTMLDivElement>(null);
|
|
296
297
|
const controllerRef = filterable ? searchInputRef : listboxRef;
|
|
297
298
|
|
|
299
|
+
/**
|
|
300
|
+
* There are few ways to assign a label to a listbox:
|
|
301
|
+
* 1. (preferred) if the trigger button has a meaningful label
|
|
302
|
+
* already, we can use the same thing. Example: MoneyInput
|
|
303
|
+
* 2. (fallback) We can use the input's main label
|
|
304
|
+
* 3. (ignored) we could follow Deque's suggestion and use the
|
|
305
|
+
* option header, but this becomes highly complicated as we can
|
|
306
|
+
* have multiple groups, or search or virtualisation enabled.
|
|
307
|
+
*/
|
|
308
|
+
const getListBoxLabelProps = (): {
|
|
309
|
+
listBoxLabel?: string;
|
|
310
|
+
listBoxLabelledBy?: string;
|
|
311
|
+
} => {
|
|
312
|
+
if (UNSAFE_triggerButtonProps?.['aria-label']) {
|
|
313
|
+
return {
|
|
314
|
+
listBoxLabel: UNSAFE_triggerButtonProps['aria-label'],
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (UNSAFE_triggerButtonProps?.['aria-labelledby']) {
|
|
319
|
+
return {
|
|
320
|
+
listBoxLabelledBy: UNSAFE_triggerButtonProps['aria-labelledby'],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (inputAttributes['aria-labelledby']) {
|
|
325
|
+
return {
|
|
326
|
+
listBoxLabelledBy: inputAttributes['aria-labelledby'],
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
return {};
|
|
331
|
+
};
|
|
332
|
+
|
|
298
333
|
return (
|
|
299
334
|
<ListboxBase
|
|
300
335
|
name={name}
|
|
@@ -388,7 +423,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
388
423
|
}}
|
|
389
424
|
>
|
|
390
425
|
<SelectInputOptions
|
|
391
|
-
id={`${id}Search`}
|
|
426
|
+
id={id ? `${id}Search` : undefined}
|
|
392
427
|
items={items}
|
|
393
428
|
renderValue={renderValue}
|
|
394
429
|
renderFooter={renderFooter}
|
|
@@ -398,6 +433,7 @@ export function SelectInput<T = string, M extends boolean = false>({
|
|
|
398
433
|
listboxRef={listboxRef}
|
|
399
434
|
filterQuery={deferredFilterQuery}
|
|
400
435
|
onFilterChange={setFilterQuery}
|
|
436
|
+
{...getListBoxLabelProps()}
|
|
401
437
|
/>
|
|
402
438
|
</OptionsOverlay>
|
|
403
439
|
);
|
|
@@ -496,6 +532,8 @@ interface SelectInputOptionsProps<T = string>
|
|
|
496
532
|
listboxRef: React.MutableRefObject<HTMLDivElement | null>;
|
|
497
533
|
filterQuery: string;
|
|
498
534
|
onFilterChange: (query: string) => void;
|
|
535
|
+
listBoxLabel?: string;
|
|
536
|
+
listBoxLabelledBy?: string;
|
|
499
537
|
}
|
|
500
538
|
|
|
501
539
|
function SelectInputOptions<T = string>({
|
|
@@ -509,6 +547,8 @@ function SelectInputOptions<T = string>({
|
|
|
509
547
|
listboxRef,
|
|
510
548
|
filterQuery,
|
|
511
549
|
onFilterChange,
|
|
550
|
+
listBoxLabel,
|
|
551
|
+
listBoxLabelledBy,
|
|
512
552
|
}: SelectInputOptionsProps<T>) {
|
|
513
553
|
const intl = useIntl();
|
|
514
554
|
|
|
@@ -662,6 +702,8 @@ function SelectInputOptions<T = string>({
|
|
|
662
702
|
id={listboxId}
|
|
663
703
|
role="listbox"
|
|
664
704
|
aria-orientation="vertical"
|
|
705
|
+
aria-label={listBoxLabel}
|
|
706
|
+
aria-labelledby={listBoxLabelledBy}
|
|
665
707
|
tabIndex={0}
|
|
666
708
|
className="np-select-input-listbox"
|
|
667
709
|
>
|
|
@@ -137,4 +137,13 @@ describe('MoneyInput', () => {
|
|
|
137
137
|
messages.selectCurrencyLabel.defaultMessage,
|
|
138
138
|
);
|
|
139
139
|
});
|
|
140
|
+
|
|
141
|
+
it('should have a listbox label', async () => {
|
|
142
|
+
render(<MoneyInput {...props} />);
|
|
143
|
+
const trigger = screen.getByRole('combobox');
|
|
144
|
+
await userEvent.click(trigger);
|
|
145
|
+
const triggerLabel = trigger.getAttribute('aria-label');
|
|
146
|
+
expect(triggerLabel).toBeTruthy();
|
|
147
|
+
expect(screen.getByRole('listbox', { name: triggerLabel ?? '' })).toBeInTheDocument();
|
|
148
|
+
});
|
|
140
149
|
});
|
|
@@ -1,22 +1,6 @@
|
|
|
1
1
|
import { logActionRequired } from '../logActionRequired';
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
component: string;
|
|
5
|
-
propName: string;
|
|
6
|
-
message: string;
|
|
7
|
-
expiryDate?: Date;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
type ValidatorType = (props: Record<string, any>, propName: string, ...rest: any[]) => Error | null;
|
|
11
|
-
|
|
12
|
-
type DeprecatedOptionsType = {
|
|
13
|
-
component: string;
|
|
14
|
-
message?: string;
|
|
15
|
-
newProp?: string;
|
|
16
|
-
expiryDate?: Date;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const deprecatedMessage = ({ component, propName, message, expiryDate }: DeprecatedMessageType) => {
|
|
3
|
+
const deprecatedMessage = ({ component, propName, message, expiryDate }) => {
|
|
20
4
|
const messages = [`${component} has deprecated the use of ${propName}.`];
|
|
21
5
|
|
|
22
6
|
if (message) {
|
|
@@ -37,14 +21,11 @@ const deprecatedMessage = ({ component, propName, message, expiryDate }: Depreca
|
|
|
37
21
|
};
|
|
38
22
|
|
|
39
23
|
const deprecated =
|
|
40
|
-
(
|
|
41
|
-
|
|
42
|
-
{ component, message = '', newProp: newProperty, expiryDate }: DeprecatedOptionsType,
|
|
43
|
-
) =>
|
|
44
|
-
(props: Record<string, any>, propertyName: string, ...rest: any[]) => {
|
|
24
|
+
(validator, { component, message = '', newProp: newProperty = null, expiryDate = null }) =>
|
|
25
|
+
(props, propertyName, ...rest) => {
|
|
45
26
|
const newPropertyMessage = newProperty ? `Please use ${newProperty} instead.` : message;
|
|
46
27
|
|
|
47
|
-
if (props[propertyName]
|
|
28
|
+
if (props[propertyName] != null && typeof props[propertyName] !== 'undefined') {
|
|
48
29
|
logActionRequired(
|
|
49
30
|
deprecatedMessage({
|
|
50
31
|
component,
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
export function logActionRequired(message
|
|
1
|
+
export function logActionRequired(message) {
|
|
2
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
|
}
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
export function logActionRequiredIf(message
|
|
8
|
+
export function logActionRequiredIf(message, conditional) {
|
|
9
9
|
if (conditional) {
|
|
10
10
|
logActionRequired(message);
|
|
11
11
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
//# sourceMappingURL=style-mock.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"style-mock.d.ts","sourceRoot":"","sources":["../../../src/test-utils/style-mock.ts"],"names":[],"mappings":""}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { userEvent, within, fn } from '@storybook/test';
|
|
2
|
-
|
|
3
|
-
import InputWithDisplayFormat, { InputWithDisplayFormatProps } from '.';
|
|
4
|
-
import { Field, FieldProps } from '../field/Field';
|
|
5
|
-
import { Meta, type StoryObj } from '@storybook/react';
|
|
6
|
-
|
|
7
|
-
type Story = StoryObj<InputWithDisplayFormatProps & { label: string }>;
|
|
8
|
-
|
|
9
|
-
const meta = {
|
|
10
|
-
title: 'Forms/InputWithDisplayFormat',
|
|
11
|
-
component: InputWithDisplayFormat,
|
|
12
|
-
render: function Render({ label, ...args }: InputWithDisplayFormatProps & FieldProps) {
|
|
13
|
-
return (
|
|
14
|
-
<Field label={label} id={args.id}>
|
|
15
|
-
<InputWithDisplayFormat id={args.id} {...args} />
|
|
16
|
-
</Field>
|
|
17
|
-
);
|
|
18
|
-
},
|
|
19
|
-
args: {
|
|
20
|
-
onFocus: fn(),
|
|
21
|
-
onBlur: fn(),
|
|
22
|
-
onChange: fn(),
|
|
23
|
-
},
|
|
24
|
-
} satisfies Meta<typeof InputWithDisplayFormat>;
|
|
25
|
-
export default meta;
|
|
26
|
-
|
|
27
|
-
export const Basic: Story = {
|
|
28
|
-
args: {
|
|
29
|
-
label: 'Sort Code',
|
|
30
|
-
placeholder: '**-**-**',
|
|
31
|
-
displayPattern: '**-**-**',
|
|
32
|
-
},
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
export const CardNumber: Story = {
|
|
36
|
-
args: {
|
|
37
|
-
label: 'Card Number',
|
|
38
|
-
placeholder: '**** **** **** **** ***',
|
|
39
|
-
displayPattern: '**** **** **** **** ***',
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export const CVC: Story = {
|
|
44
|
-
args: {
|
|
45
|
-
label: 'CVC',
|
|
46
|
-
placeholder: '***',
|
|
47
|
-
displayPattern: '***',
|
|
48
|
-
value: '',
|
|
49
|
-
},
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
export const ExpiryDate: Story = {
|
|
53
|
-
args: {
|
|
54
|
-
label: 'Expiry Date',
|
|
55
|
-
placeholder: '** / **',
|
|
56
|
-
displayPattern: '** / **',
|
|
57
|
-
},
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
export const SecurityPin: Story = {
|
|
61
|
-
args: {
|
|
62
|
-
label: 'Security Pin',
|
|
63
|
-
placeholder: '*-*-*-*-*-*',
|
|
64
|
-
displayPattern: '*-*-*-*-*-*',
|
|
65
|
-
},
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export const SecurityPinPlay: Story = {
|
|
69
|
-
args: {
|
|
70
|
-
label: 'Security Pin Play',
|
|
71
|
-
placeholder: '*-*-*-*-*-*',
|
|
72
|
-
displayPattern: '*-*-*-*-*-*',
|
|
73
|
-
},
|
|
74
|
-
play: async ({ canvasElement }) => {
|
|
75
|
-
const canvas = within(canvasElement);
|
|
76
|
-
await userEvent.type(canvas.getByPlaceholderText('*-*-*-*-*-*'), '123456');
|
|
77
|
-
},
|
|
78
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|