@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.
Files changed (40) hide show
  1. package/build/alert/Alert.js +1 -1
  2. package/build/alert/Alert.mjs +1 -1
  3. package/build/button/legacyUtils/legacyUtils.js +1 -1
  4. package/build/button/legacyUtils/legacyUtils.mjs +1 -1
  5. package/build/drawer/Drawer.js +1 -1
  6. package/build/drawer/Drawer.mjs +1 -1
  7. package/build/inputs/SelectInput.js +38 -4
  8. package/build/inputs/SelectInput.js.map +1 -1
  9. package/build/inputs/SelectInput.mjs +38 -4
  10. package/build/inputs/SelectInput.mjs.map +1 -1
  11. package/build/markdown/Markdown.js +1 -1
  12. package/build/markdown/Markdown.mjs +1 -1
  13. package/build/popover/Popover.js +1 -1
  14. package/build/popover/Popover.mjs +1 -1
  15. package/build/types/inputs/SelectInput.d.ts.map +1 -1
  16. package/build/types/utilities/deprecatedProperty/deprecatedProperty.d.ts +6 -8
  17. package/build/types/utilities/deprecatedProperty/deprecatedProperty.d.ts.map +1 -1
  18. package/build/types/utilities/deprecatedProperty/index.d.ts +1 -1
  19. package/build/types/utilities/deprecatedProperty/index.d.ts.map +1 -1
  20. package/build/types/utilities/index.d.ts +2 -2
  21. package/build/types/utilities/index.d.ts.map +1 -1
  22. package/build/types/utilities/logActionRequired.d.ts +2 -2
  23. package/build/types/utilities/logActionRequired.d.ts.map +1 -1
  24. package/build/utilities/logActionRequired.js.map +1 -1
  25. package/build/utilities/logActionRequired.mjs.map +1 -1
  26. package/package.json +3 -3
  27. package/src/inputWithDisplayFormat/InputWithDisplayFormat.story.js +85 -0
  28. package/src/inputs/SelectInput.docs.mdx +19 -0
  29. package/src/inputs/SelectInput.spec.tsx +80 -1
  30. package/src/inputs/SelectInput.story.tsx +2 -2
  31. package/src/inputs/SelectInput.tsx +44 -2
  32. package/src/moneyInput/MoneyInput.rtl.spec.tsx +9 -0
  33. package/src/utilities/deprecatedProperty/{deprecatedProperty.ts → deprecatedProperty.js} +4 -23
  34. package/src/utilities/{logActionRequired.ts → logActionRequired.js} +2 -2
  35. package/build/types/test-utils/style-mock.d.ts +0 -1
  36. package/build/types/test-utils/style-mock.d.ts.map +0 -1
  37. package/src/inputWithDisplayFormat/InputWithDisplayFormat.story.tsx +0 -78
  38. /package/src/test-utils/{style-mock.ts → style-mock.js} +0 -0
  39. /package/src/utilities/deprecatedProperty/{index.ts → index.js} +0 -0
  40. /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: 'components/SelectInput',
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
- type DeprecatedMessageType = {
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
- validator: ValidatorType,
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] !== null && props[propertyName] !== undefined) {
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: string) {
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: string, conditional: boolean) {
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