@transferwise/components 0.0.0-experimental-9254674 → 0.0.0-experimental-fbd2ec1

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 (128) hide show
  1. package/build/i18n/de.json +0 -1
  2. package/build/i18n/de.json.js +0 -1
  3. package/build/i18n/de.json.js.map +1 -1
  4. package/build/i18n/de.json.mjs +0 -1
  5. package/build/i18n/de.json.mjs.map +1 -1
  6. package/build/i18n/es.json +0 -1
  7. package/build/i18n/es.json.js +0 -1
  8. package/build/i18n/es.json.js.map +1 -1
  9. package/build/i18n/es.json.mjs +0 -1
  10. package/build/i18n/es.json.mjs.map +1 -1
  11. package/build/i18n/fr.json +0 -1
  12. package/build/i18n/fr.json.js +0 -1
  13. package/build/i18n/fr.json.js.map +1 -1
  14. package/build/i18n/fr.json.mjs +0 -1
  15. package/build/i18n/fr.json.mjs.map +1 -1
  16. package/build/i18n/hu.json +0 -1
  17. package/build/i18n/hu.json.js +0 -1
  18. package/build/i18n/hu.json.js.map +1 -1
  19. package/build/i18n/hu.json.mjs +0 -1
  20. package/build/i18n/hu.json.mjs.map +1 -1
  21. package/build/i18n/id.json +0 -1
  22. package/build/i18n/id.json.js +0 -1
  23. package/build/i18n/id.json.js.map +1 -1
  24. package/build/i18n/id.json.mjs +0 -1
  25. package/build/i18n/id.json.mjs.map +1 -1
  26. package/build/i18n/it.json +0 -1
  27. package/build/i18n/it.json.js +0 -1
  28. package/build/i18n/it.json.js.map +1 -1
  29. package/build/i18n/it.json.mjs +0 -1
  30. package/build/i18n/it.json.mjs.map +1 -1
  31. package/build/i18n/ja.json +0 -1
  32. package/build/i18n/ja.json.js +0 -1
  33. package/build/i18n/ja.json.js.map +1 -1
  34. package/build/i18n/ja.json.mjs +0 -1
  35. package/build/i18n/ja.json.mjs.map +1 -1
  36. package/build/i18n/pl.json +0 -1
  37. package/build/i18n/pl.json.js +0 -1
  38. package/build/i18n/pl.json.js.map +1 -1
  39. package/build/i18n/pl.json.mjs +0 -1
  40. package/build/i18n/pl.json.mjs.map +1 -1
  41. package/build/i18n/pt.json +0 -1
  42. package/build/i18n/pt.json.js +0 -1
  43. package/build/i18n/pt.json.js.map +1 -1
  44. package/build/i18n/pt.json.mjs +0 -1
  45. package/build/i18n/pt.json.mjs.map +1 -1
  46. package/build/i18n/ro.json +0 -1
  47. package/build/i18n/ro.json.js +0 -1
  48. package/build/i18n/ro.json.js.map +1 -1
  49. package/build/i18n/ro.json.mjs +0 -1
  50. package/build/i18n/ro.json.mjs.map +1 -1
  51. package/build/i18n/ru.json +0 -1
  52. package/build/i18n/ru.json.js +0 -1
  53. package/build/i18n/ru.json.js.map +1 -1
  54. package/build/i18n/ru.json.mjs +0 -1
  55. package/build/i18n/ru.json.mjs.map +1 -1
  56. package/build/i18n/th.json +0 -1
  57. package/build/i18n/th.json.js +0 -1
  58. package/build/i18n/th.json.js.map +1 -1
  59. package/build/i18n/th.json.mjs +0 -1
  60. package/build/i18n/th.json.mjs.map +1 -1
  61. package/build/i18n/tr.json +0 -1
  62. package/build/i18n/tr.json.js +0 -1
  63. package/build/i18n/tr.json.js.map +1 -1
  64. package/build/i18n/tr.json.mjs +0 -1
  65. package/build/i18n/tr.json.mjs.map +1 -1
  66. package/build/i18n/zh-CN.json +0 -1
  67. package/build/i18n/zh-CN.json.js +0 -1
  68. package/build/i18n/zh-CN.json.js.map +1 -1
  69. package/build/i18n/zh-CN.json.mjs +0 -1
  70. package/build/i18n/zh-CN.json.mjs.map +1 -1
  71. package/build/i18n/zh-HK.json +0 -1
  72. package/build/i18n/zh-HK.json.js +0 -1
  73. package/build/i18n/zh-HK.json.js.map +1 -1
  74. package/build/i18n/zh-HK.json.mjs +0 -1
  75. package/build/i18n/zh-HK.json.mjs.map +1 -1
  76. package/build/main.css +0 -24
  77. package/build/stepper/Stepper.js +12 -1
  78. package/build/stepper/Stepper.js.map +1 -1
  79. package/build/stepper/Stepper.mjs +12 -1
  80. package/build/stepper/Stepper.mjs.map +1 -1
  81. package/build/styles/main.css +0 -24
  82. package/build/styles/stepper/Stepper.css +0 -24
  83. package/build/typeahead/Typeahead.js +22 -8
  84. package/build/typeahead/Typeahead.js.map +1 -1
  85. package/build/typeahead/Typeahead.mjs +22 -9
  86. package/build/typeahead/Typeahead.mjs.map +1 -1
  87. package/build/typeahead/typeaheadInput/TypeaheadInput.js +5 -2
  88. package/build/typeahead/typeaheadInput/TypeaheadInput.js.map +1 -1
  89. package/build/typeahead/typeaheadInput/TypeaheadInput.mjs +5 -2
  90. package/build/typeahead/typeaheadInput/TypeaheadInput.mjs.map +1 -1
  91. package/build/typeahead/typeaheadOption/TypeaheadOption.js +9 -6
  92. package/build/typeahead/typeaheadOption/TypeaheadOption.js.map +1 -1
  93. package/build/typeahead/typeaheadOption/TypeaheadOption.mjs +9 -6
  94. package/build/typeahead/typeaheadOption/TypeaheadOption.mjs.map +1 -1
  95. package/build/types/stepper/Stepper.d.ts.map +1 -1
  96. package/build/types/typeahead/Typeahead.d.ts +1 -1
  97. package/build/types/typeahead/Typeahead.d.ts.map +1 -1
  98. package/build/types/typeahead/typeaheadInput/TypeaheadInput.d.ts +2 -0
  99. package/build/types/typeahead/typeaheadInput/TypeaheadInput.d.ts.map +1 -1
  100. package/build/types/typeahead/typeaheadOption/TypeaheadOption.d.ts +2 -1
  101. package/build/types/typeahead/typeaheadOption/TypeaheadOption.d.ts.map +1 -1
  102. package/package.json +4 -4
  103. package/src/flowNavigation/__snapshots__/FlowNavigation.spec.js.snap +1 -2
  104. package/src/i18n/de.json +0 -1
  105. package/src/i18n/es.json +0 -1
  106. package/src/i18n/fr.json +0 -1
  107. package/src/i18n/hu.json +0 -1
  108. package/src/i18n/id.json +0 -1
  109. package/src/i18n/it.json +0 -1
  110. package/src/i18n/ja.json +0 -1
  111. package/src/i18n/pl.json +0 -1
  112. package/src/i18n/pt.json +0 -1
  113. package/src/i18n/ro.json +0 -1
  114. package/src/i18n/ru.json +0 -1
  115. package/src/i18n/th.json +0 -1
  116. package/src/i18n/tr.json +0 -1
  117. package/src/i18n/zh-CN.json +0 -1
  118. package/src/i18n/zh-HK.json +0 -1
  119. package/src/main.css +0 -24
  120. package/src/stepper/Stepper.css +0 -24
  121. package/src/stepper/Stepper.less +0 -17
  122. package/src/stepper/Stepper.spec.js +11 -5
  123. package/src/stepper/Stepper.tsx +13 -2
  124. package/src/typeahead/Typeahead.spec.js +8 -0
  125. package/src/typeahead/Typeahead.tsx +29 -13
  126. package/src/typeahead/typeaheadInput/TypeaheadInput.tsx +9 -0
  127. package/src/typeahead/typeaheadOption/TypeaheadOption.spec.js +8 -0
  128. package/src/typeahead/typeaheadOption/TypeaheadOption.tsx +9 -6
package/src/i18n/it.json CHANGED
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(Facoltativo)",
22
22
  "neptune.Link.opensInNewTab": "(si apre in una nuova scheda)",
23
23
  "neptune.MoneyInput.Select.placeholder": "Seleziona un'opzione...",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "Scegli la valuta",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "Seleziona un'opzione...",
26
25
  "neptune.Select.searchPlaceholder": "Cerca...",
27
26
  "neptune.SelectInput.noResultsFound": "Nessun risultato trovato",
package/src/i18n/ja.json CHANGED
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(任意)",
22
22
  "neptune.Link.opensInNewTab": "(新しいタブで開きます)",
23
23
  "neptune.MoneyInput.Select.placeholder": "選択してください...",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "通貨を選択してください",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "選択してください…",
26
25
  "neptune.Select.searchPlaceholder": "検索する... ",
27
26
  "neptune.SelectInput.noResultsFound": "結果が見つかりませんでした",
package/src/i18n/pl.json CHANGED
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(opcjonalne)",
22
22
  "neptune.Link.opensInNewTab": "(otworzy się w nowej zakładce)",
23
23
  "neptune.MoneyInput.Select.placeholder": "Wybierz opcję...",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "Wybierz walutę",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "Wybierz opcję...",
26
25
  "neptune.Select.searchPlaceholder": "Wyszukaj...",
27
26
  "neptune.SelectInput.noResultsFound": "Nie znaleziono wyników",
package/src/i18n/pt.json CHANGED
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(Opcional)",
22
22
  "neptune.Link.opensInNewTab": "(abre em uma nova aba)",
23
23
  "neptune.MoneyInput.Select.placeholder": "Escolha uma opção...",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "Selecione a moeda",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "Escolha uma opção...",
26
25
  "neptune.Select.searchPlaceholder": "Buscar...",
27
26
  "neptune.SelectInput.noResultsFound": "Nenhum resultado encontrado",
package/src/i18n/ro.json CHANGED
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(Opțional)",
22
22
  "neptune.Link.opensInNewTab": "(se deschide într-o filă nouă)",
23
23
  "neptune.MoneyInput.Select.placeholder": "Selectează o opţiune...",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "Selectează moneda",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "Selectează o opțiune...",
26
25
  "neptune.Select.searchPlaceholder": "Caută...",
27
26
  "neptune.SelectInput.noResultsFound": "Nu s-a găsit niciun rezultat",
package/src/i18n/ru.json CHANGED
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(необязательно)",
22
22
  "neptune.Link.opensInNewTab": "(откроется в новой вкладке)",
23
23
  "neptune.MoneyInput.Select.placeholder": "Выберите вариант...",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "Выберите валюту",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "Выберите вариант...",
26
25
  "neptune.Select.searchPlaceholder": "Поиск...",
27
26
  "neptune.SelectInput.noResultsFound": "Ничего не найдено",
package/src/i18n/th.json CHANGED
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(ไม่บังคับ)",
22
22
  "neptune.Link.opensInNewTab": "(เปิดในแท็บใหม่)",
23
23
  "neptune.MoneyInput.Select.placeholder": "เลือกตัวเลือก...",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "เลือกสกุลเงิน",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "เลือกตัวเลือก...",
26
25
  "neptune.Select.searchPlaceholder": "ค้นหา...",
27
26
  "neptune.SelectInput.noResultsFound": "ไม่พบผลลัพธ์",
package/src/i18n/tr.json CHANGED
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(İsteğe bağlı)",
22
22
  "neptune.Link.opensInNewTab": "(yeni sekmede açılır)",
23
23
  "neptune.MoneyInput.Select.placeholder": "Bir seçenek seçin...",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "Para birimi seçin",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "Bir seçenek seçin...",
26
25
  "neptune.Select.searchPlaceholder": "Ara...",
27
26
  "neptune.SelectInput.noResultsFound": "Sonuç bulunamadı",
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(可选)",
22
22
  "neptune.Link.opensInNewTab": "(在新标签页中打开)",
23
23
  "neptune.MoneyInput.Select.placeholder": "请选择...",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "选择货币",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "选择其中一项...",
26
25
  "neptune.Select.searchPlaceholder": "搜索",
27
26
  "neptune.SelectInput.noResultsFound": "找不到结果",
@@ -21,7 +21,6 @@
21
21
  "neptune.Label.optional": "(可選)",
22
22
  "neptune.Link.opensInNewTab": "(在新分頁中開啟)",
23
23
  "neptune.MoneyInput.Select.placeholder": "選擇一個選項…",
24
- "neptune.MoneyInput.Select.selectCurrencyLabel": "選擇貨幣",
25
24
  "neptune.PhoneNumberInput.SelectInput.placeholder": "選擇其中一項…",
26
25
  "neptune.Select.searchPlaceholder": "搜尋…",
27
26
  "neptune.SelectInput.noResultsFound": "找不到任何結果",
package/src/main.css CHANGED
@@ -4368,35 +4368,11 @@ html:not([dir="rtl"]) .np-navigation-option {
4368
4368
  padding: 0 ;
4369
4369
  }
4370
4370
  .progress-bar {
4371
- float: left;
4372
4371
  -webkit-backface-visibility: hidden;
4373
- height: 100%;
4374
4372
  background-color: var(--color-interactive-primary);
4375
- border-top-left-radius: 1px;
4376
- border-bottom-left-radius: 1px;
4377
4373
  transition: width 0.6s ease-in-out;
4378
4374
  will-change: width;
4379
4375
  }
4380
- [dir="rtl"] .progress-bar {
4381
- float: right;
4382
- }
4383
- .progress-bar::after {
4384
- float: right;
4385
- width: 8px;
4386
- height: 8px;
4387
- margin-top: -3px;
4388
- margin-right: -4px;
4389
- content: "";
4390
- border-radius: 4px;
4391
- }
4392
- [dir="rtl"] .progress-bar::after {
4393
- float: left;
4394
- }
4395
- [dir="rtl"] .progress-bar::after {
4396
- margin-left: -4px;
4397
- margin-right: 0;
4398
- margin-right: initial;
4399
- }
4400
4376
  .btn-unstyled {
4401
4377
  background: none;
4402
4378
  border: none;
@@ -70,35 +70,11 @@
70
70
  padding: 0 ;
71
71
  }
72
72
  .progress-bar {
73
- float: left;
74
73
  -webkit-backface-visibility: hidden;
75
- height: 100%;
76
74
  background-color: var(--color-interactive-primary);
77
- border-top-left-radius: 1px;
78
- border-bottom-left-radius: 1px;
79
75
  transition: width 0.6s ease-in-out;
80
76
  will-change: width;
81
77
  }
82
- [dir="rtl"] .progress-bar {
83
- float: right;
84
- }
85
- .progress-bar::after {
86
- float: right;
87
- width: 8px;
88
- height: 8px;
89
- margin-top: -3px;
90
- margin-right: -4px;
91
- content: "";
92
- border-radius: 4px;
93
- }
94
- [dir="rtl"] .progress-bar::after {
95
- float: left;
96
- }
97
- [dir="rtl"] .progress-bar::after {
98
- margin-left: -4px;
99
- margin-right: 0;
100
- margin-right: initial;
101
- }
102
78
  .btn-unstyled {
103
79
  background: none;
104
80
  border: none;
@@ -78,27 +78,10 @@
78
78
  }
79
79
 
80
80
  .progress-bar {
81
- .float(left);
82
-
83
81
  -webkit-backface-visibility: hidden;
84
- height: 100%;
85
82
  background-color: var(--color-interactive-primary);
86
- border-top-left-radius: 1px;
87
- border-bottom-left-radius: 1px;
88
83
  transition: width 0.6s ease-in-out;
89
84
  will-change: width;
90
-
91
- &::after {
92
- .float(right);
93
-
94
- width: 8px;
95
- height: 8px;
96
- margin-top: -3px;
97
- .margin(right, -4px);
98
-
99
- content: "";
100
- border-radius: 4px;
101
- }
102
85
  }
103
86
 
104
87
  .btn-unstyled {
@@ -40,15 +40,21 @@ describe('Stepper', () => {
40
40
  });
41
41
 
42
42
  it('sets the widths of the progress bar to match where you are in the flow', () => {
43
- expect(totalWidth()).toBe('0%');
43
+ expect(totalWidth()).toBe('0px');
44
44
  activeStep(2);
45
- expect(totalWidth()).toBe('100%');
45
+ expect(totalWidth()).toBe(
46
+ 'calc(100% + var(--progress-bar-start-shift) + var(--progress-bar-border-width))',
47
+ );
46
48
  steps(5);
47
- expect(totalWidth()).toBe('50%');
49
+ expect(totalWidth()).toBe(
50
+ 'calc(50% + var(--progress-bar-start-shift) + var(--progress-bar-border-width))',
51
+ );
48
52
  activeStep(10000);
49
- expect(totalWidth()).toBe('100%');
53
+ expect(totalWidth()).toBe(
54
+ 'calc(100% + var(--progress-bar-start-shift) + var(--progress-bar-border-width))',
55
+ );
50
56
  activeStep(-10);
51
- expect(totalWidth()).toBe('0%');
57
+ expect(totalWidth()).toBe('0px');
52
58
  });
53
59
  });
54
60
 
@@ -38,6 +38,18 @@ const Stepper = ({ steps, activeStep = 0, className }: StepperProps) => {
38
38
  const stepPercentage = 1 / (steps.length - 1);
39
39
  const percentageCompleted = activeStepIndex / (steps.length - 1);
40
40
 
41
+ const getProgressWidth = (): string => {
42
+ if (percentageCompleted === 0) {
43
+ return '0px';
44
+ }
45
+ /**
46
+ * Progress bar starts with left/right (depends on rtl) shift `--progress-bar-start-shift` for hiding Progress bar's left and right borders
47
+ * which are used for progress vertical delimiter.
48
+ * When progress is completed, we need to add `--progress-bar-border-width` to the width to allow the right border be outside of the progress area.
49
+ */
50
+ return `calc(${percentageCompleted * 100}% + var(--progress-bar-start-shift) + var(--progress-bar-border-width))`;
51
+ };
52
+
41
53
  const renderStep = (step: Step, index: number) => {
42
54
  const active = index === activeStepIndex;
43
55
  const clickable = step.onClick && !active;
@@ -85,9 +97,8 @@ const Stepper = ({ steps, activeStep = 0, className }: StepperProps) => {
85
97
  return (
86
98
  <div className={clsx('tw-stepper', className)}>
87
99
  <div className="progress">
88
- <div className="progress-bar" style={{ width: `${percentageCompleted * 100}%` }} />
100
+ <div className="progress-bar" style={{ width: getProgressWidth() }} />
89
101
  </div>
90
-
91
102
  <ol className="tw-stepper-steps p-t-1 m-b-0">{steps.map(renderStep)}</ol>
92
103
  </div>
93
104
  );
@@ -258,6 +258,14 @@ describe('Typeahead', () => {
258
258
  expect(event.preventDefault).toHaveBeenCalledTimes(1);
259
259
  });
260
260
 
261
+ it('sets aria-activedescendant correctly based on focus', () => {
262
+ const inputElement = input().getDOMNode();
263
+
264
+ doTimes(3, () => input().simulate('keyDown', fakeKeyDownEventForKey('ArrowDown')));
265
+
266
+ expect(inputElement).toHaveAttribute('aria-activedescendant', 'menu-test option-Test2');
267
+ });
268
+
261
269
  it('displays alert when alert is provided and chips are valid or alert type is error', () => {
262
270
  const event = {
263
271
  key: 'Enter',
@@ -3,7 +3,7 @@ import { clsx } from 'clsx';
3
3
  import { DebouncedFunc } from 'lodash';
4
4
  import clamp from 'lodash.clamp';
5
5
  import debounce from 'lodash.debounce';
6
- import { Component, ReactNode } from 'react';
6
+ import React, { Component, ReactNode } from 'react';
7
7
  import { injectIntl, WrappedComponentProps } from 'react-intl';
8
8
 
9
9
  import Chip from '../chips/Chip';
@@ -101,6 +101,7 @@ class Typeahead<T> extends Component<TypeaheadPropsWithInputAttributes<T>, Typea
101
101
  size: Size.MEDIUM,
102
102
  validateChip: () => true,
103
103
  } satisfies Partial<TypeaheadProps<unknown>>;
104
+ optionRefs: (React.RefObject<HTMLLIElement> | null)[];
104
105
 
105
106
  constructor(props: TypeaheadPropsWithInputAttributes<T>) {
106
107
  super(props);
@@ -115,6 +116,7 @@ class Typeahead<T> extends Component<TypeaheadPropsWithInputAttributes<T>, Typea
115
116
  optionsShown: false,
116
117
  isFocused: false,
117
118
  };
119
+ this.optionRefs = [] as (React.RefObject<HTMLLIElement> | null)[];
118
120
  }
119
121
 
120
122
  handleSearchDebounced: DebouncedFunc<Typeahead<T>['handleSearch']>;
@@ -228,7 +230,6 @@ class Typeahead<T> extends Component<TypeaheadPropsWithInputAttributes<T>, Typea
228
230
  }
229
231
  }
230
232
  };
231
-
232
233
  moveFocusedOption(offset: number) {
233
234
  this.setState((previousState) => {
234
235
  const { keyboardFocusedOptionIndex } = previousState;
@@ -237,6 +238,10 @@ class Typeahead<T> extends Component<TypeaheadPropsWithInputAttributes<T>, Typea
237
238
  if (keyboardFocusedOptionIndex !== null) {
238
239
  index = clamp(keyboardFocusedOptionIndex + offset, 0, options.length - 1);
239
240
  }
241
+ const optionRef = this.optionRefs[index];
242
+ if (optionRef?.current) {
243
+ optionRef.current.focus(); // Set focus on the option element
244
+ }
240
245
  return {
241
246
  keyboardFocusedOptionIndex: index,
242
247
  };
@@ -402,17 +407,23 @@ class Typeahead<T> extends Component<TypeaheadPropsWithInputAttributes<T>, Typea
402
407
  id={`menu-${id}`}
403
408
  >
404
409
  <ul className="dropdown-menu" role="menu" aria-labelledby="dropdownMenu1">
405
- {optionsToRender.map((option, idx) => (
406
- <TypeaheadOption
407
- key={`${option.label}${idx.toString()}`}
408
- query={query}
409
- option={option}
410
- selected={keyboardFocusedOptionIndex === idx}
411
- onClick={(event) => {
412
- this.onOptionSelected(event, option);
413
- }}
414
- />
415
- ))}
410
+ {optionsToRender.map((option, idx) => {
411
+ const ref = React.createRef<HTMLLIElement>();
412
+ this.optionRefs[idx] = ref;
413
+ return (
414
+ <TypeaheadOption
415
+ key={`${option.label}${idx.toString()}`}
416
+ ref={ref}
417
+ query={query}
418
+ option={option}
419
+ selected={keyboardFocusedOptionIndex === idx}
420
+ id={String(option.label.replace(/\s+/g, '-'))}
421
+ onClick={(event) => {
422
+ this.onOptionSelected(event, option);
423
+ }}
424
+ />
425
+ );
426
+ })}
416
427
  {footer}
417
428
  </ul>
418
429
  </div>
@@ -507,6 +518,11 @@ class Typeahead<T> extends Component<TypeaheadPropsWithInputAttributes<T>, Typea
507
518
  typeaheadId={id}
508
519
  renderChip={this.renderChip}
509
520
  autoComplete={inputAutoComplete}
521
+ ariaActivedescendant={
522
+ keyboardFocusedOptionIndex !== null && options[keyboardFocusedOptionIndex]
523
+ ? `option-${options[keyboardFocusedOptionIndex].label.replace(/\s+/g, '-')}`
524
+ : undefined
525
+ }
510
526
  onChange={this.handleOnChange}
511
527
  onKeyDown={this.handleOnKeyDown}
512
528
  onFocus={this.handleOnFocus}
@@ -20,10 +20,12 @@ export type TypeaheadInputProps<T> = {
20
20
  onFocus: () => void;
21
21
  onPaste: React.ClipboardEventHandler<HTMLInputElement>;
22
22
  renderChip: (chip: TypeaheadOption<T>, index: number) => ReactNode;
23
+ ariaActivedescendant?: string;
23
24
  } & Pick<TypeaheadProps<T>, 'id' | 'name' | 'autoFocus' | 'multiple' | 'placeholder' | 'maxHeight'>;
24
25
 
25
26
  type TypeaheadInputState = {
26
27
  inputWidth: number;
28
+ keyboardFocusedOptionIndex: number | null;
27
29
  };
28
30
 
29
31
  export default class TypeaheadInput<T> extends Component<
@@ -37,6 +39,7 @@ export default class TypeaheadInput<T> extends Component<
37
39
  super(props);
38
40
  this.state = {
39
41
  inputWidth: DEFAULT_INPUT_MIN_WIDTH,
42
+ keyboardFocusedOptionIndex: null,
40
43
  };
41
44
  }
42
45
 
@@ -76,6 +79,7 @@ export default class TypeaheadInput<T> extends Component<
76
79
  onFocus,
77
80
  onPaste,
78
81
  autoComplete,
82
+ ariaActivedescendant, // Declare the ariaActivedescendant variable
79
83
  } = this.props;
80
84
  const { inputWidth } = this.state;
81
85
 
@@ -86,6 +90,11 @@ export default class TypeaheadInput<T> extends Component<
86
90
  className={clsx(multiple && 'typeahead__input')}
87
91
  name={name}
88
92
  id={`input-${typeaheadId}`}
93
+ aria-activedescendant={
94
+ ariaActivedescendant
95
+ ? `menu-${typeaheadId} ${ariaActivedescendant}`
96
+ : `menu-${typeaheadId}`
97
+ }
89
98
  autoFocus={autoFocus}
90
99
  placeholder={hasPlaceholder ? placeholder : ''}
91
100
  aria-autocomplete="list"
@@ -51,6 +51,14 @@ describe('Typeahead Option', () => {
51
51
  expect(component.is('.tw-dropdown-item--focused')).toBe(true);
52
52
  });
53
53
 
54
+ it('expect aria-selected to be true', () => {
55
+ component.setProps({
56
+ selected: true,
57
+ });
58
+
59
+ expect(component.prop('aria-selected')).toBe('true');
60
+ });
61
+
54
62
  it('calls onClick when clicked', () => {
55
63
  const onClick = jest.fn();
56
64
  component.setProps({
@@ -1,5 +1,5 @@
1
- /* eslint-disable jsx-a11y/anchor-is-valid */
2
1
  import { clsx } from 'clsx';
2
+ import { forwardRef } from 'react';
3
3
 
4
4
  import { TypeaheadOption } from '../Typeahead';
5
5
  import highlight from '../util/highlight';
@@ -9,21 +9,24 @@ export type TypeaheadOptionProps<T> = {
9
9
  selected?: boolean;
10
10
  onClick?: React.MouseEventHandler;
11
11
  query?: string;
12
+ id?: string;
12
13
  };
13
14
 
14
- const Option = <T,>(props: TypeaheadOptionProps<T>) => {
15
- const { option, selected = false, onClick = () => {}, query = '' } = props;
15
+ const Option = forwardRef<HTMLLIElement, TypeaheadOptionProps<any>>((props, ref) => {
16
+ const { option, selected = false, onClick = () => {}, query = '', id } = props;
16
17
  const { label, note, secondary } = option;
17
18
 
18
19
  return (
19
20
  <li
21
+ ref={ref}
20
22
  role="option"
21
- aria-selected="false"
23
+ aria-selected={selected ? 'true' : 'false'}
24
+ id={`option-${id}`}
22
25
  className={clsx('typeahead__option tw-dropdown-item clickable', {
23
26
  'tw-dropdown-item--focused': selected,
24
27
  })}
25
28
  >
26
- <a className="dropdown-item" href="#" tabIndex={-1} onClick={onClick}>
29
+ <a className="dropdown-item" href="#" tabIndex={0} onClick={onClick}>
27
30
  <span>{highlight(label, query)}</span>
28
31
  {note && <span className="np-text-body-default m-l-1">{note}</span>}
29
32
  {secondary && (
@@ -32,6 +35,6 @@ const Option = <T,>(props: TypeaheadOptionProps<T>) => {
32
35
  </a>
33
36
  </li>
34
37
  );
35
- };
38
+ });
36
39
 
37
40
  export default Option;