@transferwise/components 46.131.2 → 46.132.1

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 (121) hide show
  1. package/build/actionOption/ActionOption.js.map +1 -1
  2. package/build/actionOption/ActionOption.mjs.map +1 -1
  3. package/build/alert/Alert.js +1 -1
  4. package/build/alert/Alert.js.map +1 -1
  5. package/build/alert/Alert.mjs +1 -1
  6. package/build/alert/Alert.mjs.map +1 -1
  7. package/build/checkboxOption/CheckboxOption.js.map +1 -1
  8. package/build/checkboxOption/CheckboxOption.mjs.map +1 -1
  9. package/build/common/Option/Option.js.map +1 -1
  10. package/build/common/Option/Option.mjs.map +1 -1
  11. package/build/common/liveRegion/LiveRegion.js +46 -7
  12. package/build/common/liveRegion/LiveRegion.js.map +1 -1
  13. package/build/common/liveRegion/LiveRegion.mjs +46 -7
  14. package/build/common/liveRegion/LiveRegion.mjs.map +1 -1
  15. package/build/flowNavigation/FlowNavigation.js +1 -0
  16. package/build/flowNavigation/FlowNavigation.js.map +1 -1
  17. package/build/flowNavigation/FlowNavigation.mjs +1 -0
  18. package/build/flowNavigation/FlowNavigation.mjs.map +1 -1
  19. package/build/legacylistItem/LegacyListItem.js.map +1 -1
  20. package/build/legacylistItem/LegacyListItem.mjs.map +1 -1
  21. package/build/main.css +52 -1
  22. package/build/navigationOption/NavigationOption.js.map +1 -1
  23. package/build/navigationOption/NavigationOption.mjs.map +1 -1
  24. package/build/overlayHeader/OverlayHeader.js +1 -0
  25. package/build/overlayHeader/OverlayHeader.js.map +1 -1
  26. package/build/overlayHeader/OverlayHeader.mjs +1 -0
  27. package/build/overlayHeader/OverlayHeader.mjs.map +1 -1
  28. package/build/prompt/InfoPrompt/InfoPrompt.js +2 -0
  29. package/build/prompt/InfoPrompt/InfoPrompt.js.map +1 -1
  30. package/build/prompt/InfoPrompt/InfoPrompt.mjs +2 -0
  31. package/build/prompt/InfoPrompt/InfoPrompt.mjs.map +1 -1
  32. package/build/radioOption/RadioOption.js.map +1 -1
  33. package/build/radioOption/RadioOption.mjs.map +1 -1
  34. package/build/styles/common/liveRegion/LiveRegion.css +3 -0
  35. package/build/styles/css/neptune.css +48 -1
  36. package/build/styles/main.css +52 -1
  37. package/build/styles/styles/less/neptune.css +48 -1
  38. package/build/summary/Summary.js +1 -1
  39. package/build/summary/Summary.js.map +1 -1
  40. package/build/summary/Summary.mjs +1 -1
  41. package/build/summary/Summary.mjs.map +1 -1
  42. package/build/switchOption/SwitchOption.js +1 -1
  43. package/build/switchOption/SwitchOption.js.map +1 -1
  44. package/build/switchOption/SwitchOption.mjs +1 -1
  45. package/build/switchOption/SwitchOption.mjs.map +1 -1
  46. package/build/types/actionOption/ActionOption.d.ts +1 -1
  47. package/build/types/alert/Alert.d.ts +1 -1
  48. package/build/types/checkboxOption/CheckboxOption.d.ts +1 -1
  49. package/build/types/common/Option/Option.d.ts +3 -0
  50. package/build/types/common/Option/Option.d.ts.map +1 -1
  51. package/build/types/common/liveRegion/LiveRegion.d.ts +5 -2
  52. package/build/types/common/liveRegion/LiveRegion.d.ts.map +1 -1
  53. package/build/types/legacylistItem/LegacyListItem.d.ts +1 -1
  54. package/build/types/navigationOption/NavigationOption.d.ts +1 -1
  55. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts +2 -2
  56. package/build/types/prompt/InfoPrompt/InfoPrompt.d.ts.map +1 -1
  57. package/build/types/radioOption/RadioOption.d.ts +1 -1
  58. package/build/types/summary/Summary.d.ts +1 -1
  59. package/build/types/switchOption/SwitchOption.d.ts +1 -1
  60. package/package.json +2 -2
  61. package/src/actionOption/ActionOption.story.tsx +2 -1
  62. package/src/actionOption/ActionOption.tsx +1 -1
  63. package/src/alert/Alert.story.tsx +1 -7
  64. package/src/alert/Alert.tsx +1 -1
  65. package/src/button/_stories/Button.story.tsx +0 -5
  66. package/src/checkboxButton/CheckboxButton.story.tsx +0 -1
  67. package/src/checkboxOption/CheckboxOption.story.tsx +2 -1
  68. package/src/checkboxOption/CheckboxOption.tsx +1 -1
  69. package/src/circularButton/CircularButton.story.tsx +0 -1
  70. package/src/common/Option/Option.tsx +3 -0
  71. package/src/common/liveRegion/LiveRegion.css +3 -0
  72. package/src/common/liveRegion/LiveRegion.less +3 -0
  73. package/src/common/liveRegion/LiveRegion.test.tsx +69 -2
  74. package/src/common/liveRegion/LiveRegion.tsx +77 -8
  75. package/src/display/Display.story.tsx +15 -1
  76. package/src/expressiveMoneyInput/ExpressiveMoneyInput.story.tsx +0 -1
  77. package/src/header/Header.story.tsx +0 -5
  78. package/src/inputWithDisplayFormat/InputWithDisplayFormat.story.tsx +0 -1
  79. package/src/inputs/SelectInput/_stories/SelectInput.docs.mdx +62 -0
  80. package/src/inputs/SelectInput/_stories/SelectInput.story.tsx +796 -220
  81. package/src/inputs/SelectInput/_stories/SelectInput.test.story.tsx +433 -4
  82. package/src/legacylistItem/LegacyListItem.story.tsx +2 -1
  83. package/src/legacylistItem/LegacyListItem.tsx +1 -1
  84. package/src/listItem/AdditionalInfo/ListItemAdditionalInfo.story.tsx +0 -5
  85. package/src/listItem/AvatarLayout/ListItemAvatarLayout.story.tsx +0 -5
  86. package/src/listItem/AvatarView/ListItemAvatarView.story.tsx +0 -5
  87. package/src/listItem/Button/ListItemButton.story.tsx +0 -5
  88. package/src/listItem/Checkbox/ListItemCheckbox.story.tsx +0 -5
  89. package/src/listItem/IconButton/ListItemIconButton.story.tsx +0 -5
  90. package/src/listItem/Image/ListItemImage.story.tsx +0 -5
  91. package/src/listItem/Navigation/ListItemNavigation.story.tsx +0 -5
  92. package/src/listItem/Prompt/ListItemPrompt.story.tsx +1 -5
  93. package/src/listItem/Radio/ListItemRadio.story.tsx +0 -5
  94. package/src/listItem/Switch/ListItemSwitch.story.tsx +0 -5
  95. package/src/listItem/_stories/ListItem.disabled.story.tsx +0 -1
  96. package/src/listItem/_stories/ListItem.scenarios.story.tsx +0 -1
  97. package/src/listItem/_stories/ListItem.story.tsx +1 -6
  98. package/src/main.css +52 -1
  99. package/src/main.less +1 -0
  100. package/src/modal/Modal.story.tsx +0 -1
  101. package/src/navigationOption/NavigationOption.story.tsx +2 -1
  102. package/src/navigationOption/NavigationOption.tsx +1 -1
  103. package/src/popover/Popover.story.tsx +0 -1
  104. package/src/prompt/ActionPrompt/ActionPrompt.story.tsx +0 -5
  105. package/src/prompt/InfoPrompt/InfoPrompt.story.tsx +0 -5
  106. package/src/prompt/InfoPrompt/InfoPrompt.test.story.tsx +142 -5
  107. package/src/prompt/InfoPrompt/InfoPrompt.test.tsx +11 -6
  108. package/src/prompt/InfoPrompt/InfoPrompt.tsx +4 -3
  109. package/src/prompt/InlinePrompt/InlinePrompt.story.tsx +0 -5
  110. package/src/provider/theme/ThemeProvider.story.tsx +8 -0
  111. package/src/radioOption/RadioOption.story.tsx +2 -1
  112. package/src/radioOption/RadioOption.tsx +1 -1
  113. package/src/sentimentSurface/SentimentSurface.story.tsx +0 -5
  114. package/src/sticky/Sticky.story.tsx +0 -1
  115. package/src/styles/less/core/_typography.less +15 -2
  116. package/src/styles/less/neptune.css +48 -1
  117. package/src/summary/Summary.story.tsx +1 -1
  118. package/src/summary/Summary.tsx +1 -1
  119. package/src/switchOption/SwitchOption.story.tsx +2 -1
  120. package/src/switchOption/SwitchOption.tsx +1 -1
  121. package/src/tokens/tokens.story.tsx +1 -1
@@ -1,23 +1,43 @@
1
- import { render, screen } from '@testing-library/react';
2
- import { LiveRegion, LiveRegionProps } from './LiveRegion';
1
+ import { act, render, screen } from '@testing-library/react';
2
+ import { WDS_LIVE_REGION_DELAY_MS } from '../constants';
3
+ import { LiveRegion, LiveRegionProps, resetLiveRegionAnnouncementQueue } from './LiveRegion';
3
4
 
4
5
  describe('LiveRegion', () => {
6
+ beforeEach(() => {
7
+ jest.useFakeTimers();
8
+ resetLiveRegionAnnouncementQueue();
9
+ });
10
+
11
+ afterEach(() => {
12
+ jest.clearAllTimers();
13
+ jest.useRealTimers();
14
+ });
15
+
5
16
  const renderLiveRegion = (props: Partial<LiveRegionProps> & Pick<LiveRegionProps, 'aria-live'>) =>
6
17
  render(<LiveRegion {...props}>{props.children ?? 'Live content'}</LiveRegion>);
7
18
 
19
+ const enableLiveRegion = (delay = WDS_LIVE_REGION_DELAY_MS) => {
20
+ act(() => {
21
+ jest.advanceTimersByTime(delay);
22
+ });
23
+ };
24
+
8
25
  describe('when aria-live is "polite"', () => {
9
26
  it('renders with role="status"', () => {
10
27
  renderLiveRegion({ 'aria-live': 'polite' });
28
+ enableLiveRegion();
11
29
  expect(screen.getByRole('status')).toBeInTheDocument();
12
30
  });
13
31
 
14
32
  it('sets aria-live="polite"', () => {
15
33
  renderLiveRegion({ 'aria-live': 'polite' });
34
+ enableLiveRegion();
16
35
  expect(screen.getByRole('status')).toHaveAttribute('aria-live', 'polite');
17
36
  });
18
37
 
19
38
  it('sets aria-atomic="true"', () => {
20
39
  renderLiveRegion({ 'aria-live': 'polite' });
40
+ enableLiveRegion();
21
41
  expect(screen.getByRole('status')).toHaveAttribute('aria-atomic', 'true');
22
42
  });
23
43
  });
@@ -25,32 +45,79 @@ describe('LiveRegion', () => {
25
45
  describe('when aria-live is "assertive"', () => {
26
46
  it('renders with role="alert"', () => {
27
47
  renderLiveRegion({ 'aria-live': 'assertive' });
48
+ enableLiveRegion();
28
49
  expect(screen.getByRole('alert')).toBeInTheDocument();
29
50
  });
30
51
 
31
52
  it('sets aria-live="assertive"', () => {
32
53
  renderLiveRegion({ 'aria-live': 'assertive' });
54
+ enableLiveRegion();
33
55
  expect(screen.getByRole('alert')).toHaveAttribute('aria-live', 'assertive');
34
56
  });
35
57
 
36
58
  it('sets aria-atomic="true"', () => {
37
59
  renderLiveRegion({ 'aria-live': 'assertive' });
60
+ enableLiveRegion();
38
61
  expect(screen.getByRole('alert')).toHaveAttribute('aria-atomic', 'true');
39
62
  });
40
63
  });
41
64
 
65
+ it('delays live-region activation before the configured timeout', () => {
66
+ renderLiveRegion({ 'aria-live': 'polite', children: 'Delayed content' });
67
+
68
+ const liveRegion = screen.getByRole('status');
69
+ expect(liveRegion).toBeInTheDocument();
70
+ expect(liveRegion.firstElementChild).toHaveAttribute('aria-hidden', 'true');
71
+
72
+ enableLiveRegion(WDS_LIVE_REGION_DELAY_MS - 1);
73
+ expect(liveRegion.firstElementChild).toHaveAttribute('aria-hidden', 'true');
74
+
75
+ enableLiveRegion(1);
76
+ expect(liveRegion.firstElementChild).not.toHaveAttribute('aria-hidden');
77
+ expect(liveRegion).toHaveTextContent('Delayed content');
78
+ });
79
+
80
+ it('queues multiple assertive regions so each one gets announced', () => {
81
+ render(
82
+ <>
83
+ <LiveRegion aria-live="assertive">First prompt</LiveRegion>
84
+ <LiveRegion aria-live="assertive">Second prompt</LiveRegion>
85
+ </>,
86
+ );
87
+
88
+ const liveRegions = screen.getAllByRole('alert');
89
+ expect(liveRegions).toHaveLength(2);
90
+ expect(liveRegions[0].firstElementChild).toHaveAttribute('aria-hidden', 'true');
91
+ expect(liveRegions[1].firstElementChild).toHaveAttribute('aria-hidden', 'true');
92
+
93
+ enableLiveRegion();
94
+ const firstEnabledLiveRegions = screen.getAllByRole('alert');
95
+ expect(firstEnabledLiveRegions[0].firstElementChild).not.toHaveAttribute('aria-hidden');
96
+ expect(firstEnabledLiveRegions[1].firstElementChild).toHaveAttribute('aria-hidden', 'true');
97
+ expect(firstEnabledLiveRegions[0]).toHaveTextContent('First prompt');
98
+
99
+ enableLiveRegion();
100
+ const secondEnabledLiveRegions = screen.getAllByRole('alert');
101
+ expect(secondEnabledLiveRegions[1].firstElementChild).not.toHaveAttribute('aria-hidden');
102
+ expect(secondEnabledLiveRegions[0]).toHaveTextContent('First prompt');
103
+ expect(secondEnabledLiveRegions[1]).toHaveTextContent('Second prompt');
104
+ });
105
+
42
106
  it('renders children', () => {
43
107
  renderLiveRegion({ 'aria-live': 'polite', children: 'Transfer sent' });
108
+ enableLiveRegion();
44
109
  expect(screen.getByText('Transfer sent')).toBeInTheDocument();
45
110
  });
46
111
 
47
112
  it('passes additional HTML attributes to the wrapper div', () => {
48
113
  renderLiveRegion({ 'aria-live': 'polite', className: 'custom' });
114
+ enableLiveRegion();
49
115
  expect(screen.getByRole('status')).toHaveClass('custom');
50
116
  });
51
117
 
52
118
  it('supports data-testid prop', () => {
53
119
  renderLiveRegion({ 'aria-live': 'polite', 'data-testid': 'live-region' });
120
+ enableLiveRegion();
54
121
  expect(screen.getByTestId('live-region')).toBeInTheDocument();
55
122
  });
56
123
  });
@@ -1,11 +1,52 @@
1
+ import { useEffect, useState } from 'react';
1
2
  import type { HTMLAttributes, ReactNode } from 'react';
2
3
 
3
- const ARIA_LIVE_ROLE_MAP = {
4
+ import { WDS_LIVE_REGION_DELAY_MS } from '../constants';
5
+
6
+ export type AriaLive = 'off' | 'polite' | 'assertive';
7
+
8
+ type LivePoliteness = Exclude<AriaLive, 'off'>;
9
+
10
+ const LIVE_REGION_ROLE_BY_POLITENESS: Record<LivePoliteness, 'status' | 'alert'> = {
4
11
  assertive: 'alert',
5
12
  polite: 'status',
6
- } as const;
13
+ };
7
14
 
8
- export type AriaLive = 'off' | 'polite' | 'assertive';
15
+ let nextPoliteAnnouncementAt = 0;
16
+ let nextAssertiveAnnouncementAt = 0;
17
+
18
+ const getNextAnnouncementAt = (politeness: LivePoliteness): number => {
19
+ if (politeness === 'polite') {
20
+ return nextPoliteAnnouncementAt;
21
+ }
22
+
23
+ return nextAssertiveAnnouncementAt;
24
+ };
25
+
26
+ const setNextAnnouncementAt = (politeness: LivePoliteness, value: number): void => {
27
+ if (politeness === 'polite') {
28
+ nextPoliteAnnouncementAt = value;
29
+ return;
30
+ }
31
+
32
+ nextAssertiveAnnouncementAt = value;
33
+ };
34
+
35
+ export const resetLiveRegionAnnouncementQueue = (): void => {
36
+ nextPoliteAnnouncementAt = 0;
37
+ nextAssertiveAnnouncementAt = 0;
38
+ };
39
+
40
+ const calcAnnouncementDelayMs = (politeness: LivePoliteness, now: number): number => {
41
+ return Math.max(now + WDS_LIVE_REGION_DELAY_MS, getNextAnnouncementAt(politeness)) - now;
42
+ };
43
+
44
+ const scheduleAnnouncement = (politeness: LivePoliteness): number => {
45
+ const now = Date.now();
46
+ const delayMs = calcAnnouncementDelayMs(politeness, now);
47
+ setNextAnnouncementAt(politeness, now + delayMs + WDS_LIVE_REGION_DELAY_MS);
48
+ return delayMs;
49
+ };
9
50
 
10
51
  export interface LiveRegionProps extends Omit<
11
52
  HTMLAttributes<HTMLDivElement>,
@@ -15,6 +56,8 @@ export interface LiveRegionProps extends Omit<
15
56
  * Determines urgency: 'assertive' interrupts, 'polite' waits for idle, 'off' disables live region.
16
57
  */
17
58
  'aria-live': AriaLive;
59
+ /** Optional stable key that triggers a new announcement when it changes. */
60
+ announceOnChange?: string | number;
18
61
  /** Test ID for testing tools */
19
62
  'data-testid'?: string;
20
63
  children?: ReactNode;
@@ -25,25 +68,51 @@ export interface LiveRegionProps extends Omit<
25
68
  *
26
69
  * - `aria-live="polite"` → `role="status"`
27
70
  * - `aria-live="assertive"` → `role="alert"`
28
- * - `aria-live="off"` → no live region
71
+ * - `aria-live="off"` → no live region (renders children unwrapped)
29
72
  *
30
73
  * The `role` prop is intentionally excluded from the public API
31
74
  * to prevent mismatches between `aria-live` and `role`.
32
75
  */
33
- export const LiveRegion = ({ 'aria-live': ariaLive, children, ...props }: LiveRegionProps) => {
76
+ export const LiveRegion = ({
77
+ 'aria-live': ariaLive,
78
+ announceOnChange,
79
+ children,
80
+ className,
81
+ ...props
82
+ }: LiveRegionProps) => {
83
+ const [shouldAnnounce, setShouldAnnounce] = useState(false);
84
+ const announcementTrigger =
85
+ announceOnChange ??
86
+ (typeof children === 'string' || typeof children === 'number' ? children : undefined);
87
+
88
+ useEffect(() => {
89
+ setShouldAnnounce(false);
90
+
91
+ if (ariaLive === 'off') {
92
+ return;
93
+ }
94
+
95
+ const timeoutId = window.setTimeout(
96
+ () => setShouldAnnounce(true),
97
+ scheduleAnnouncement(ariaLive),
98
+ );
99
+
100
+ return () => window.clearTimeout(timeoutId);
101
+ }, [ariaLive, announcementTrigger]);
102
+
34
103
  if (ariaLive === 'off') {
35
104
  return <>{children}</>;
36
105
  }
37
106
 
38
107
  return (
39
108
  <div
40
- role={ARIA_LIVE_ROLE_MAP[ariaLive]}
109
+ role={LIVE_REGION_ROLE_BY_POLITENESS[ariaLive]}
41
110
  aria-live={ariaLive}
42
111
  aria-atomic="true"
43
- style={{ display: 'contents' }}
112
+ className={`wds-LiveRegion ${className ?? ''}`}
44
113
  {...props}
45
114
  >
46
- {children}
115
+ <div aria-hidden={shouldAnnounce ? undefined : 'true'}>{children}</div>
47
116
  </div>
48
117
  );
49
118
  };
@@ -14,6 +14,8 @@ export const Basic = () => {
14
14
  const DE = 'äöüßabcdefghijklmnopqrstuvwxyz';
15
15
  const UA = 'Ми будуємо найбільш міжнародний рахунок у світі';
16
16
  const JA = 'ぁあぃいぅうぇえぉおかがきぎくぐけげこごさざしじすずせぜそぞただちぢっつづてで';
17
+ const ZN =
18
+ '的一是在不了有和人这中大为上个国我以要他时来用们生到作地于出就分对成会可主发年样能下过子说产种面而方后多定行学法所民得经十三之进着等部度家电力里如水化高自二理起小物现实加量都两体制机当使点从业本去最性性齉龘龘靐齉爩鱻猋驫麤籲爨癵驫鲙鬯鬻厵纛';
17
19
  return (
18
20
  <>
19
21
  <div lang="en">
@@ -77,7 +79,7 @@ export const Basic = () => {
77
79
  </div>
78
80
  <hr />
79
81
  <div lang="ja">
80
- <h1>Everything eles, e.g 🇯🇵</h1>
82
+ <h1>Japanese</h1>
81
83
  Large
82
84
  <Display type={Typography.DISPLAY_LARGE}>{JA}</Display>
83
85
  <hr />
@@ -87,6 +89,18 @@ export const Basic = () => {
87
89
  Small
88
90
  <Display type={Typography.DISPLAY_SMALL}>{JA}</Display>
89
91
  </div>
92
+ <hr />
93
+ <div lang="zh-CN">
94
+ <h1>Simplified Chinese</h1>
95
+ Large
96
+ <Display type={Typography.DISPLAY_LARGE}>{ZN}</Display>
97
+ <hr />
98
+ Medium
99
+ <Display type={Typography.DISPLAY_MEDIUM}>{ZN}</Display>
100
+ <hr />
101
+ Small
102
+ <Display type={Typography.DISPLAY_SMALL}>{ZN}</Display>
103
+ </div>
90
104
  </>
91
105
  );
92
106
  };
@@ -55,7 +55,6 @@ export default {
55
55
  tags: ['contribution'],
56
56
  parameters: {
57
57
  docs: {
58
- toc: true,
59
58
  canvas: {
60
59
  sourceState: 'hidden',
61
60
  },
@@ -63,11 +63,6 @@ const meta: Meta<typeof Header> = {
63
63
  },
64
64
  tags: ['autodocs'],
65
65
  decorators: [withContainer],
66
- parameters: {
67
- docs: {
68
- toc: true,
69
- },
70
- },
71
66
  };
72
67
 
73
68
  export default meta;
@@ -16,7 +16,6 @@ const meta: Meta<typeof InputWithDisplayFormat> = {
16
16
  </Field>
17
17
  );
18
18
  },
19
-
20
19
  args: {
21
20
  onFocus: fn(),
22
21
  onBlur: fn(),
@@ -4,6 +4,60 @@ import { Meta } from '@storybook/addon-docs/blocks';
4
4
 
5
5
  # Accessibility
6
6
 
7
+ ## Keyboard navigation
8
+
9
+ The component uses [Headless UI Listbox](https://headlessui.com/react/listbox) under the hood, which provides full keyboard support out of the box.
10
+
11
+ <table>
12
+ <thead>
13
+ <tr>
14
+ <th>Key</th>
15
+ <th>Action</th>
16
+ </tr>
17
+ </thead>
18
+ <tbody>
19
+ <tr>
20
+ <td>
21
+ <code>Tab</code> / click
22
+ </td>
23
+ <td>Focus the trigger button</td>
24
+ </tr>
25
+ <tr>
26
+ <td>
27
+ <code>Enter</code> / <code>Space</code>
28
+ </td>
29
+ <td>Open the listbox</td>
30
+ </tr>
31
+ <tr>
32
+ <td>
33
+ <code>↑</code> / <code>↓</code>
34
+ </td>
35
+ <td>Navigate options</td>
36
+ </tr>
37
+ <tr>
38
+ <td>
39
+ <code>Enter</code>
40
+ </td>
41
+ <td>Select the focused option</td>
42
+ </tr>
43
+ <tr>
44
+ <td>
45
+ <code>Escape</code> / <code>Tab</code>
46
+ </td>
47
+ <td>Close without selecting</td>
48
+ </tr>
49
+ <tr>
50
+ <td>
51
+ Typing (when <code>filterable</code>)
52
+ </td>
53
+ <td>
54
+ Narrows the list in the search input; <code>↑</code> / <code>↓</code> still navigate the
55
+ filtered results
56
+ </td>
57
+ </tr>
58
+ </tbody>
59
+ </table>
60
+
7
61
  ## Labelling
8
62
 
9
63
  In order for the `<SelectInput />` to be considered accessible, it must be provided with a matching label, preferably via the <a href="/?path=/docs/field--docs">Field</a> component.
@@ -18,3 +72,11 @@ Additionally, the `listbox` container that holds all the options is also expecte
18
72
  3. Correctly paired input `<label />` text, ideally via the `Field` component.
19
73
 
20
74
  > Using option group heading is possible but complicated as we can have multiple groups, and those are not necessarily rendered consistently if search or virtualisation are enabled.
75
+
76
+ ## Custom triggers
77
+
78
+ When using `renderTrigger`, the interactive element **must** be `SelectInputTriggerButton`. A plain `<button>` will not receive the ARIA attributes (`aria-expanded`, `aria-haspopup`, `aria-controls`) that the component manages, breaking screen reader announcements for the listbox state.
79
+
80
+ ## Multiple selection
81
+
82
+ With `multiple`, selected items show a visible checkmark in the list. The trigger content is **consumer-controlled** via the `withinTrigger` boolean passed to `renderValue` — consumers are responsible for communicating the current selection accessibly inside the trigger (e.g. `"USD, EUR"` or `"3 currencies selected"`).