@transferwise/components 46.155.1 → 46.157.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.
Files changed (264) hide show
  1. package/build/Calendar/Calendar.constants.js +10 -0
  2. package/build/Calendar/Calendar.constants.js.map +1 -0
  3. package/build/Calendar/Calendar.constants.mjs +8 -0
  4. package/build/Calendar/Calendar.constants.mjs.map +1 -0
  5. package/build/Calendar/Calendar.js +121 -17
  6. package/build/Calendar/Calendar.js.map +1 -1
  7. package/build/Calendar/Calendar.messages.js +12 -0
  8. package/build/Calendar/Calendar.messages.js.map +1 -1
  9. package/build/Calendar/Calendar.messages.mjs +12 -0
  10. package/build/Calendar/Calendar.messages.mjs.map +1 -1
  11. package/build/Calendar/Calendar.mjs +122 -18
  12. package/build/Calendar/Calendar.mjs.map +1 -1
  13. package/build/Calendar/DateHeader/DateHeader.js +5 -1
  14. package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
  15. package/build/Calendar/DateHeader/DateHeader.mjs +5 -1
  16. package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
  17. package/build/Calendar/DayCalendar/DayCalendar.js +20 -2
  18. package/build/Calendar/DayCalendar/DayCalendar.js.map +1 -1
  19. package/build/Calendar/DayCalendar/DayCalendar.mjs +20 -2
  20. package/build/Calendar/DayCalendar/DayCalendar.mjs.map +1 -1
  21. package/build/Calendar/DayCalendar/Table/DayCalendarTable.js +60 -19
  22. package/build/Calendar/DayCalendar/Table/DayCalendarTable.js.map +1 -1
  23. package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs +60 -19
  24. package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs.map +1 -1
  25. package/build/Calendar/MonthCalendar/MonthCalendar.js +6 -0
  26. package/build/Calendar/MonthCalendar/MonthCalendar.js.map +1 -1
  27. package/build/Calendar/MonthCalendar/MonthCalendar.mjs +6 -0
  28. package/build/Calendar/MonthCalendar/MonthCalendar.mjs.map +1 -1
  29. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js +8 -3
  30. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js.map +1 -1
  31. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs +8 -3
  32. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs.map +1 -1
  33. package/build/Calendar/TableLink/TableLink.js +95 -0
  34. package/build/Calendar/TableLink/TableLink.js.map +1 -0
  35. package/build/Calendar/TableLink/TableLink.mjs +91 -0
  36. package/build/Calendar/TableLink/TableLink.mjs.map +1 -0
  37. package/build/Calendar/YearCalendar/Table/YearCalendarTable.js +8 -3
  38. package/build/Calendar/YearCalendar/Table/YearCalendarTable.js.map +1 -1
  39. package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs +8 -3
  40. package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs.map +1 -1
  41. package/build/Calendar/YearCalendar/YearCalendar.js +7 -0
  42. package/build/Calendar/YearCalendar/YearCalendar.js.map +1 -1
  43. package/build/Calendar/YearCalendar/YearCalendar.mjs +7 -0
  44. package/build/Calendar/YearCalendar/YearCalendar.mjs.map +1 -1
  45. package/build/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.js +24 -0
  46. package/build/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.js.map +1 -0
  47. package/build/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.mjs +22 -0
  48. package/build/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.mjs.map +1 -0
  49. package/build/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js +33 -0
  50. package/build/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js.map +1 -0
  51. package/build/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs +31 -0
  52. package/build/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs.map +1 -0
  53. package/build/Calendar/_helpers/isDateDisabled/isDateDisabled.js +40 -0
  54. package/build/Calendar/_helpers/isDateDisabled/isDateDisabled.js.map +1 -0
  55. package/build/Calendar/_helpers/isDateDisabled/isDateDisabled.mjs +38 -0
  56. package/build/Calendar/_helpers/isDateDisabled/isDateDisabled.mjs.map +1 -0
  57. package/build/DateLookup/DateLookup.js +20 -9
  58. package/build/DateLookup/DateLookup.js.map +1 -1
  59. package/build/DateLookup/DateLookup.mjs +20 -9
  60. package/build/DateLookup/DateLookup.mjs.map +1 -1
  61. package/build/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.js +3 -5
  62. package/build/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.js.map +1 -1
  63. package/build/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.mjs +3 -5
  64. package/build/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.mjs.map +1 -1
  65. package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.js.map +1 -1
  66. package/build/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.mjs.map +1 -1
  67. package/build/Markup/Markup.js +138 -0
  68. package/build/Markup/Markup.js.map +1 -0
  69. package/build/Markup/Markup.mjs +133 -0
  70. package/build/Markup/Markup.mjs.map +1 -0
  71. package/build/Markup/constants.js +21 -0
  72. package/build/Markup/constants.js.map +1 -0
  73. package/build/Markup/constants.mjs +15 -0
  74. package/build/Markup/constants.mjs.map +1 -0
  75. package/build/Markup/utils/parseMarkup.js +263 -0
  76. package/build/Markup/utils/parseMarkup.js.map +1 -0
  77. package/build/Markup/utils/parseMarkup.mjs +258 -0
  78. package/build/Markup/utils/parseMarkup.mjs.map +1 -0
  79. package/build/Markup/utils/sanitise.js +111 -0
  80. package/build/Markup/utils/sanitise.js.map +1 -0
  81. package/build/Markup/utils/sanitise.mjs +108 -0
  82. package/build/Markup/utils/sanitise.mjs.map +1 -0
  83. package/build/Typeahead/Typeahead.js +53 -27
  84. package/build/Typeahead/Typeahead.js.map +1 -1
  85. package/build/Typeahead/Typeahead.messages.js +3 -0
  86. package/build/Typeahead/Typeahead.messages.js.map +1 -1
  87. package/build/Typeahead/Typeahead.messages.mjs +3 -0
  88. package/build/Typeahead/Typeahead.messages.mjs.map +1 -1
  89. package/build/Typeahead/Typeahead.mjs +53 -27
  90. package/build/Typeahead/Typeahead.mjs.map +1 -1
  91. package/build/Typeahead/TypeaheadInput/TypeaheadInput.js +2 -2
  92. package/build/Typeahead/TypeaheadInput/TypeaheadInput.js.map +1 -1
  93. package/build/Typeahead/TypeaheadInput/TypeaheadInput.mjs +2 -2
  94. package/build/Typeahead/TypeaheadInput/TypeaheadInput.mjs.map +1 -1
  95. package/build/Typeahead/TypeaheadOption/TypeaheadOption.js +3 -1
  96. package/build/Typeahead/TypeaheadOption/TypeaheadOption.js.map +1 -1
  97. package/build/Typeahead/TypeaheadOption/TypeaheadOption.mjs +3 -1
  98. package/build/Typeahead/TypeaheadOption/TypeaheadOption.mjs.map +1 -1
  99. package/build/i18n/en.json +5 -0
  100. package/build/i18n/en.json.js +5 -0
  101. package/build/i18n/en.json.js.map +1 -1
  102. package/build/i18n/en.json.mjs +5 -0
  103. package/build/i18n/en.json.mjs.map +1 -1
  104. package/build/i18n/hu.json +1 -1
  105. package/build/i18n/hu.json.js +1 -1
  106. package/build/i18n/hu.json.mjs +1 -1
  107. package/build/index.js +4 -2
  108. package/build/index.js.map +1 -1
  109. package/build/index.mjs +2 -1
  110. package/build/index.mjs.map +1 -1
  111. package/build/main.css +168 -43
  112. package/build/styles/Calendar/Calendar.css +112 -37
  113. package/build/styles/Calendar/TableLink/TableLink.css +115 -0
  114. package/build/styles/Markup/Markup.css +28 -0
  115. package/build/styles/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
  116. package/build/styles/Typeahead/Typeahead.css +4 -0
  117. package/build/styles/Typeahead/TypeaheadOption/TypeaheadOption.css +5 -0
  118. package/build/styles/main.css +168 -43
  119. package/build/types/Calendar/Calendar.constants.d.ts +2 -0
  120. package/build/types/Calendar/Calendar.constants.d.ts.map +1 -0
  121. package/build/types/Calendar/Calendar.d.ts +16 -3
  122. package/build/types/Calendar/Calendar.d.ts.map +1 -1
  123. package/build/types/Calendar/Calendar.messages.d.ts +20 -0
  124. package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
  125. package/build/types/Calendar/Calendar.types.d.ts +32 -0
  126. package/build/types/Calendar/Calendar.types.d.ts.map +1 -0
  127. package/build/types/Calendar/DateHeader/DateHeader.d.ts +2 -0
  128. package/build/types/Calendar/DateHeader/DateHeader.d.ts.map +1 -1
  129. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +7 -1
  130. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
  131. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +6 -0
  132. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
  133. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts +1 -0
  134. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts.map +1 -1
  135. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts +2 -1
  136. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts.map +1 -1
  137. package/build/types/Calendar/{tableLink → TableLink}/TableLink.d.ts +5 -1
  138. package/build/types/Calendar/TableLink/TableLink.d.ts.map +1 -0
  139. package/build/types/Calendar/TableLink/index.d.ts.map +1 -0
  140. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts +2 -1
  141. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts.map +1 -1
  142. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts +1 -0
  143. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts.map +1 -1
  144. package/build/types/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.d.ts +8 -0
  145. package/build/types/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.d.ts.map +1 -0
  146. package/build/types/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +3 -0
  147. package/build/types/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +1 -0
  148. package/build/types/Calendar/_helpers/index.d.ts +4 -0
  149. package/build/types/Calendar/_helpers/index.d.ts.map +1 -0
  150. package/build/types/Calendar/_helpers/isDateDisabled/isDateDisabled.d.ts +3 -0
  151. package/build/types/Calendar/_helpers/isDateDisabled/isDateDisabled.d.ts.map +1 -0
  152. package/build/types/Calendar/index.d.ts +1 -0
  153. package/build/types/Calendar/index.d.ts.map +1 -1
  154. package/build/types/DateLookup/DateLookup.d.ts +24 -5
  155. package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
  156. package/build/types/DateLookup/index.d.ts +1 -0
  157. package/build/types/DateLookup/index.d.ts.map +1 -1
  158. package/build/types/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.d.ts +6 -4
  159. package/build/types/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.d.ts.map +1 -1
  160. package/build/types/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts +1 -1
  161. package/build/types/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts.map +1 -1
  162. package/build/types/Markup/Markup.d.ts +18 -0
  163. package/build/types/Markup/Markup.d.ts.map +1 -0
  164. package/build/types/Markup/Markup.types.d.ts +77 -0
  165. package/build/types/Markup/Markup.types.d.ts.map +1 -0
  166. package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts +12 -0
  167. package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts.map +1 -0
  168. package/build/types/Markup/constants.d.ts +6 -0
  169. package/build/types/Markup/constants.d.ts.map +1 -0
  170. package/build/types/Markup/index.d.ts +3 -0
  171. package/build/types/Markup/index.d.ts.map +1 -0
  172. package/build/types/Markup/utils/parseMarkup.d.ts +8 -0
  173. package/build/types/Markup/utils/parseMarkup.d.ts.map +1 -0
  174. package/build/types/Markup/utils/sanitise.d.ts +4 -0
  175. package/build/types/Markup/utils/sanitise.d.ts.map +1 -0
  176. package/build/types/Typeahead/Typeahead.d.ts.map +1 -1
  177. package/build/types/Typeahead/Typeahead.messages.d.ts +5 -0
  178. package/build/types/Typeahead/Typeahead.messages.d.ts.map +1 -1
  179. package/build/types/Typeahead/TypeaheadInput/TypeaheadInput.d.ts.map +1 -1
  180. package/build/types/Typeahead/TypeaheadOption/TypeaheadOption.d.ts +1 -0
  181. package/build/types/Typeahead/TypeaheadOption/TypeaheadOption.d.ts.map +1 -1
  182. package/build/types/index.d.ts +5 -2
  183. package/build/types/index.d.ts.map +1 -1
  184. package/build/types/test-utils/index.d.ts +10 -0
  185. package/build/types/test-utils/index.d.ts.map +1 -1
  186. package/package.json +1 -1
  187. package/src/Calendar/Calendar.constants.ts +5 -0
  188. package/src/Calendar/Calendar.css +112 -37
  189. package/src/Calendar/Calendar.disabledDates.test.tsx +610 -0
  190. package/src/Calendar/Calendar.less +5 -35
  191. package/src/Calendar/Calendar.messages.ts +21 -0
  192. package/src/Calendar/Calendar.test.tsx +342 -32
  193. package/src/Calendar/Calendar.tsx +169 -19
  194. package/src/Calendar/Calendar.types.ts +35 -0
  195. package/src/Calendar/DateHeader/DateHeader.tsx +6 -0
  196. package/src/Calendar/DayCalendar/DayCalendar.tsx +39 -1
  197. package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +67 -27
  198. package/src/Calendar/MonthCalendar/MonthCalendar.tsx +8 -0
  199. package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +15 -3
  200. package/src/Calendar/TableLink/TableLink.css +115 -0
  201. package/src/Calendar/TableLink/TableLink.less +68 -0
  202. package/src/Calendar/TableLink/TableLink.tsx +117 -0
  203. package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +15 -3
  204. package/src/Calendar/YearCalendar/YearCalendar.tsx +9 -0
  205. package/src/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.test.ts +90 -0
  206. package/src/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.ts +25 -0
  207. package/src/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +98 -0
  208. package/src/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +45 -0
  209. package/src/Calendar/_helpers/index.ts +3 -0
  210. package/src/Calendar/_helpers/isDateDisabled/isDateDisabled.test.ts +169 -0
  211. package/src/Calendar/_helpers/isDateDisabled/isDateDisabled.ts +59 -0
  212. package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +118 -0
  213. package/src/Calendar/_stories/Calendar.story.tsx +144 -4
  214. package/src/Calendar/_stories/Calendar.test.story.tsx +114 -0
  215. package/src/Calendar/index.ts +7 -0
  216. package/src/DateLookup/DateLookup.disabledDates.test.tsx +312 -0
  217. package/src/DateLookup/DateLookup.test.tsx +34 -37
  218. package/src/DateLookup/DateLookup.tsx +37 -13
  219. package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +108 -0
  220. package/src/DateLookup/_stories/DateLookup.story.tsx +126 -2
  221. package/src/DateLookup/index.ts +6 -0
  222. package/src/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.tsx +12 -13
  223. package/src/Inputs/SelectInput/SelectInput.test.tsx +37 -0
  224. package/src/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +1 -1
  225. package/src/Markup/Markup.css +28 -0
  226. package/src/Markup/Markup.injection.test.tsx +163 -0
  227. package/src/Markup/Markup.less +29 -0
  228. package/src/Markup/Markup.test.tsx +502 -0
  229. package/src/Markup/Markup.tsx +175 -0
  230. package/src/Markup/Markup.types.ts +62 -0
  231. package/src/Markup/_stories/Markup.docs.mdx +75 -0
  232. package/src/Markup/_stories/Markup.security.docs.mdx +343 -0
  233. package/src/Markup/_stories/Markup.story.tsx +266 -0
  234. package/src/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
  235. package/src/Markup/_stories/Vulnerability/Vulnerability.tsx +30 -0
  236. package/src/Markup/constants.ts +23 -0
  237. package/src/Markup/index.ts +2 -0
  238. package/src/Markup/utils/parseMarkup.test.ts +706 -0
  239. package/src/Markup/utils/parseMarkup.ts +265 -0
  240. package/src/Markup/utils/sanitise.test.ts +499 -0
  241. package/src/Markup/utils/sanitise.ts +129 -0
  242. package/src/Typeahead/Typeahead.css +4 -0
  243. package/src/Typeahead/Typeahead.less +2 -0
  244. package/src/Typeahead/Typeahead.messages.ts +5 -0
  245. package/src/Typeahead/Typeahead.test.tsx +80 -3
  246. package/src/Typeahead/Typeahead.tsx +50 -27
  247. package/src/Typeahead/TypeaheadInput/TypeaheadInput.tsx +2 -6
  248. package/src/Typeahead/TypeaheadOption/TypeaheadOption.css +5 -0
  249. package/src/Typeahead/TypeaheadOption/TypeaheadOption.less +9 -0
  250. package/src/Typeahead/TypeaheadOption/TypeaheadOption.tsx +4 -2
  251. package/src/i18n/en.json +4 -0
  252. package/src/i18n/hu.json +1 -1
  253. package/src/index.ts +5 -2
  254. package/src/main.css +168 -43
  255. package/src/main.less +1 -0
  256. package/build/Calendar/tableLink/TableLink.js +0 -61
  257. package/build/Calendar/tableLink/TableLink.js.map +0 -1
  258. package/build/Calendar/tableLink/TableLink.mjs +0 -57
  259. package/build/Calendar/tableLink/TableLink.mjs.map +0 -1
  260. package/build/types/Calendar/tableLink/TableLink.d.ts.map +0 -1
  261. package/build/types/Calendar/tableLink/index.d.ts.map +0 -1
  262. package/src/Calendar/tableLink/TableLink.tsx +0 -77
  263. /package/build/types/Calendar/{tableLink → TableLink}/index.d.ts +0 -0
  264. /package/src/Calendar/{tableLink → TableLink}/index.ts +0 -0
@@ -0,0 +1,108 @@
1
+ import { Meta } from '@storybook/addon-docs/blocks';
2
+
3
+ <Meta title="Forms/DateLookup/Accessibility" />
4
+
5
+ # Accessibility
6
+
7
+ DateLookup wraps the Calendar component inside a dropdown panel. It provides full keyboard accessibility for opening, navigating, selecting, and dismissing dates.
8
+
9
+ ## Keyboard Support
10
+
11
+ <table>
12
+ <thead>
13
+ <tr>
14
+ <th>Key</th>
15
+ <th>Context</th>
16
+ <th>Action</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <tr>
21
+ <td>
22
+ <code>ArrowLeft/Right/Up/Down</code>
23
+ </td>
24
+ <td>Trigger focused, panel closed</td>
25
+ <td>Opens the panel</td>
26
+ </tr>
27
+ <tr>
28
+ <td>
29
+ <code>ArrowLeft/Right/Up/Down</code>
30
+ </td>
31
+ <td>Panel open</td>
32
+ <td>Navigates between dates (see Calendar)</td>
33
+ </tr>
34
+ <tr>
35
+ <td>
36
+ <code>Enter</code> / <code>Space</code>
37
+ </td>
38
+ <td>Panel open, enabled date focused</td>
39
+ <td>Selects date, closes panel</td>
40
+ </tr>
41
+ <tr>
42
+ <td>
43
+ <code>Enter</code> / <code>Space</code>
44
+ </td>
45
+ <td>Panel open, disabled date focused</td>
46
+ <td>No action (panel stays open)</td>
47
+ </tr>
48
+ <tr>
49
+ <td>
50
+ <code>Escape</code>
51
+ </td>
52
+ <td>Panel open</td>
53
+ <td>Reverts to original value, closes panel</td>
54
+ </tr>
55
+ </tbody>
56
+ </table>
57
+
58
+ <br />
59
+
60
+ ### Click Behaviour
61
+
62
+ - **Clicking an enabled date**: selects it, closes the panel, fires `onChange`
63
+ - **Clicking a disabled date**: does not select, panel stays open, focus remains in the calendar grid
64
+
65
+ ## Screen Reader Announcements
66
+
67
+ DateLookup relies on the Calendar's built-in accessibility features for date announcements. See [Calendar
68
+ Accessibility](?path=/docs/forms-calendar-accessibility--docs) for details on how dates are labelled and described.
69
+
70
+ ### Trigger Button
71
+
72
+ The trigger button announces the currently selected date (or placeholder text when no date is selected). It uses `aria-controls` to associate with the panel.
73
+
74
+ ### Panel
75
+
76
+ The dropdown panel is rendered with `role="dialog"` and is connected to the trigger via `aria-controls`. When opened, focus moves into the calendar grid.
77
+
78
+ ## Disabled Dates (via `disabledRules`)
79
+
80
+ When `disabledRules` is provided, disabled dates remain focusable and keyboard-navigable:
81
+
82
+ - `aria-disabled="true"` marks the cell as unavailable
83
+ - "not available" is appended to the aria-label
84
+ - `aria-describedby` links to the InlinePrompt reason text
85
+ - Keyboard navigation reaches disabled dates (they are not skipped)
86
+ - Clicking or pressing Enter on a disabled date does **not** close the panel
87
+
88
+ ## Out-of-Range Dates (via `min`/`max`)
89
+
90
+ Dates outside `min`/`max` boundaries use `aria-disabled="true"` (not the native HTML `disabled` attribute). They are rendered as interactive buttons in the grid and can be clicked; the `aria-label` appends "not available". Arrow key navigation clamps at the boundary, so keyboard users cannot navigate into out-of-range cells.
91
+
92
+ This distinction is intentional: `min`/`max` defines a hard boundary, while `disabledRules` marks dates that exist but are unavailable (users can discover them and understand why via the InlinePrompt).
93
+
94
+ ### InlinePrompt
95
+
96
+ An InlinePrompt renders below the day grid inside the panel when the visible month contains disabled dates. It displays the configured `disabledRules.reason` text.
97
+
98
+ The prompt is linked to disabled cells via `aria-describedby`, so screen readers announce the reason when a disabled
99
+ date receives focus. See [Calendar Accessibility](?path=/docs/forms-calendar-accessibility--docs) for full details on
100
+ the prompt's announcement behaviour.
101
+
102
+ ## Consumer Callbacks
103
+
104
+ The `onDateSelect` callback fires with `{ date, isEnabled }` whenever the user navigates to or clicks on any date. Consumers can use this to:
105
+
106
+ - Display a custom message explaining why a specific date is disabled
107
+ - Track which dates users attempt to select
108
+ - Provide alternative actions for disabled dates
@@ -1,16 +1,23 @@
1
1
  import { useState } from 'react';
2
2
  import { StoryObj } from '@storybook/react-webpack5';
3
+ import { fn } from 'storybook/test';
4
+ import { action } from 'storybook/actions';
3
5
  import {
4
6
  createSandboxStory,
5
7
  globalScope,
6
8
  } from '../../../.storybook/components/sandbox/SandboxEditor';
9
+ import { storySourceWithoutNoise } from '../../../.storybook/helpers';
7
10
  import { Size } from '../../common';
8
- import DateLookup, { type DateLookupProps } from '..';
9
11
  import Field from '../../Field';
12
+ import DateLookup, { type DateLookupProps } from '..';
10
13
 
11
14
  export default {
12
15
  component: DateLookup,
13
16
  title: 'Forms/DateLookup',
17
+ args: {
18
+ onFocus: fn(),
19
+ onBlur: fn(),
20
+ },
14
21
  argTypes: {
15
22
  size: { control: 'select', options: [Size.SMALL, Size.MEDIUM, Size.LARGE] },
16
23
  disabled: { control: 'boolean' },
@@ -34,7 +41,7 @@ thePast.setUTCDate(epoch.getUTCDate() - 10);
34
41
 
35
42
  export const Sandbox = createSandboxStory({
36
43
  code: `const App = () => {
37
- const [value, setValue] = React.useState(new Date('2024-06-15'));
44
+ const [value, setValue] = React.useState(new Date(2024, 5, 14));
38
45
 
39
46
  return (
40
47
  <DateLookup
@@ -44,6 +51,11 @@ export const Sandbox = createSandboxStory({
44
51
  placeholder="Choose date"
45
52
  clearable
46
53
  value={value}
54
+ disabledRules={{
55
+ weekdays: ['sat', 'sun'],
56
+ dates: [new Date(2024, 5, 19)],
57
+ reason: 'Weekends and holidays are unavailable.',
58
+ }}
47
59
  onChange={(date) => setValue(date)}
48
60
  />
49
61
  );
@@ -120,3 +132,115 @@ export const RightAligned: Story = {
120
132
  );
121
133
  },
122
134
  };
135
+
136
+ /**
137
+ * Use `disabledRules` to disable specific weekdays, individual dates, or date ranges.
138
+ * Disabled dates remain focusable and keyboard-navigable — clicking or focusing one
139
+ * keeps the panel open, fires `onDateSelect` with `isEnabled: false`, and shows a
140
+ * negative inline prompt with the `reason` text. `min`/`max` out-of-range dates show
141
+ * a built-in boundary prompt (e.g. "Please select a date between 9 December 2024 and
142
+ * 22 January 2025.").
143
+ *
144
+ * The `reason` field is **required for accessibility**: it renders as an InlinePrompt
145
+ * below the grid and is linked to every disabled cell via `aria-describedby`, so screen
146
+ * reader users understand why a date is unavailable. For per-date reason customisation
147
+ * see [DisabledDatesWithDynamicReason](#disabled-dates-with-dynamic-reason).
148
+ */
149
+ export const DisabledDates: Story = storySourceWithoutNoise<DateLookupProps>({
150
+ args: {
151
+ size: Size.MEDIUM,
152
+ label: 'Select a date',
153
+ placeholder: 'Choose date',
154
+ monthFormat: 'long',
155
+ min: new Date(2024, 11, 9),
156
+ max: new Date(2025, 0, 22),
157
+ },
158
+ render: function Render(args: DateLookupProps) {
159
+ const min = new Date(2024, 11, 9);
160
+ const max = new Date(2025, 0, 22);
161
+ const bankHolidays = [new Date(2024, 11, 25), new Date(2024, 11, 26), new Date(2025, 0, 1)];
162
+ const [value, setValue] = useState<Date | null>(null);
163
+
164
+ return (
165
+ <DateLookup
166
+ {...args}
167
+ min={args.min ? new Date(args.min) : undefined}
168
+ max={args.max ? new Date(args.max) : undefined}
169
+ value={value}
170
+ disabledRules={{
171
+ weekdays: ['sat', 'sun'],
172
+ dates: bankHolidays,
173
+ ranges: [{ start: new Date(2024, 11, 23), end: new Date(2024, 11, 24) }],
174
+ reason: 'Some dates are unavailable for this service.',
175
+ }}
176
+ onChange={(date) => {
177
+ action('onChange')(date);
178
+ setValue(date);
179
+ }}
180
+ onDateSelect={({ date, isEnabled }) => {
181
+ action('onDateSelect')({ date, isEnabled });
182
+ }}
183
+ />
184
+ );
185
+ },
186
+ });
187
+
188
+ /**
189
+ * When different disabled dates warrant different explanations, drive the `reason`
190
+ * dynamically from `onDateSelect`. The `isEnabled` flag tells you whether the focused
191
+ * date is selectable; from there you can look up the specific reason and update state.
192
+ *
193
+ * `min`/`max` out-of-range dates are handled automatically — the component shows a
194
+ * built-in prompt with the boundary dates, so `determineReason` only needs to cover
195
+ * rule-disabled dates.
196
+ */
197
+ export const DisabledDatesWithDynamicReason: Story = storySourceWithoutNoise<DateLookupProps>({
198
+ args: {
199
+ size: Size.MEDIUM,
200
+ label: 'Select a date',
201
+ placeholder: 'Choose date',
202
+ monthFormat: 'long',
203
+ min: new Date(2024, 11, 9),
204
+ max: new Date(2025, 0, 22),
205
+ },
206
+ render: function Render(args: DateLookupProps) {
207
+ const DEFAULT_REASON = 'Some dates are unavailable for this service.';
208
+ const bankHolidays = [new Date(2024, 11, 25), new Date(2024, 11, 26), new Date(2025, 0, 1)];
209
+ const [value, setValue] = useState<Date | null>(null);
210
+ const [reason, setReason] = useState(DEFAULT_REASON);
211
+
212
+ function determineReason(date: Date, isEnabled: boolean): string {
213
+ if (isEnabled) return DEFAULT_REASON;
214
+ if (bankHolidays.some((holiday) => holiday.toDateString() === date.toDateString())) {
215
+ return 'This date is a bank holiday.';
216
+ }
217
+ if (date.getDay() === 0 || date.getDay() === 6) {
218
+ return 'We do not operate on weekends.';
219
+ }
220
+ return 'This date is unavailable due to scheduled maintenance.';
221
+ }
222
+
223
+ return (
224
+ <DateLookup
225
+ {...args}
226
+ min={args.min ? new Date(args.min) : undefined}
227
+ max={args.max ? new Date(args.max) : undefined}
228
+ value={value}
229
+ disabledRules={{
230
+ weekdays: ['sat', 'sun'],
231
+ dates: bankHolidays,
232
+ ranges: [{ start: new Date(2024, 11, 23), end: new Date(2024, 11, 24) }],
233
+ reason,
234
+ }}
235
+ onChange={(date) => {
236
+ action('onChange')(date);
237
+ setValue(date);
238
+ }}
239
+ onDateSelect={({ date, isEnabled }) => {
240
+ action('onDateSelect')({ date, isEnabled });
241
+ setReason(determineReason(date, isEnabled));
242
+ }}
243
+ />
244
+ );
245
+ },
246
+ });
@@ -1,2 +1,8 @@
1
1
  export { default } from './DateLookup';
2
2
  export type { DateLookupProps } from './DateLookup';
3
+ export type {
4
+ DisabledRules,
5
+ DisabledDateRange,
6
+ WeekDayType,
7
+ OnDateSelectParams,
8
+ } from '../Calendar/Calendar.types';
@@ -1,5 +1,6 @@
1
1
  import type { AvatarLayoutProps } from '../../AvatarLayout';
2
2
  import Button from '../../Button';
3
+ import type { NewButtonProps } from '../../Button';
3
4
  import {
4
5
  SelectInput,
5
6
  SelectInputOptionContent,
@@ -8,13 +9,7 @@ import {
8
9
  import type { CurrencyType, ExpressiveMoneyInputProps } from '../ExpressiveMoneyInput';
9
10
  import { ChevronDown } from '@transferwise/icons';
10
11
  import { Flag } from '@wise/art';
11
- import {
12
- type ButtonHTMLAttributes,
13
- forwardRef,
14
- type MouseEventHandler,
15
- useMemo,
16
- useState,
17
- } from 'react';
12
+ import { forwardRef, type MouseEventHandler, useMemo, useState } from 'react';
18
13
  import { useIntl } from 'react-intl';
19
14
 
20
15
  import messages from '../ExpressiveMoneyInput.messages';
@@ -44,6 +39,11 @@ export type CurrencySelectorProps = {
44
39
  onSearchChange?: (payload: { query: string; resultCount: number }) => void;
45
40
  } & Pick<ExpressiveMoneyInputProps, 'currency'>;
46
41
 
42
+ type ButtonInputProps = Omit<
43
+ Extract<NewButtonProps, { as?: 'button' }>,
44
+ 'v2' | 'size' | 'priority' | 'className' | 'href' | 'target'
45
+ >;
46
+
47
47
  export const CurrencySelector = ({
48
48
  id,
49
49
  currency,
@@ -109,7 +109,6 @@ export const CurrencySelector = ({
109
109
  renderTrigger={() => (
110
110
  <SelectInputTriggerButton
111
111
  as={ButtonInput}
112
- // @ts-expect-error new (v2) ButtonProps
113
112
  addonStart={{
114
113
  type: 'avatar',
115
114
  value: [
@@ -122,11 +121,11 @@ export const CurrencySelector = ({
122
121
  }),
123
122
  },
124
123
  ]
125
- .filter(Boolean)
124
+ .filter((avatar): avatar is NonNullable<typeof avatar> => avatar != null)
126
125
  .filter((avatar) => !(avatar && Object.keys(avatar).length === 0)),
127
126
  }}
128
127
  addonEnd={disabled ? undefined : { type: 'icon', value: <ChevronDown /> }}
129
- onClick={(event) => handleTriggerClick(event)}
128
+ onClick={handleTriggerClick}
130
129
  >
131
130
  <>
132
131
  <span aria-hidden>{formatCurrencyCode(currency)}</span>
@@ -151,9 +150,9 @@ export const CurrencySelector = ({
151
150
  );
152
151
  };
153
152
 
154
- export const ButtonInput = forwardRef(function ButtonInput(
155
- { children, ...rest }: React.PropsWithChildren<ButtonHTMLAttributes<HTMLButtonElement>>,
156
- ref: React.ForwardedRef<HTMLButtonElement | null>,
153
+ export const ButtonInput = forwardRef<HTMLButtonElement, ButtonInputProps>(function ButtonInput(
154
+ { children, ...rest },
155
+ ref,
157
156
  ) {
158
157
  return (
159
158
  <Button
@@ -7,9 +7,12 @@ import { render, mockMatchMedia, mockResizeObserver } from '../../test-utils';
7
7
  import {
8
8
  SelectInput,
9
9
  SelectInputOptionContent,
10
+ SelectInputTriggerButton,
10
11
  type SelectInputOptionItem,
11
12
  type SelectInputProps,
12
13
  } from '.';
14
+ import Button from '../../Button';
15
+ import type { ButtonProps as ResolverButtonProps } from '../../Button/Button.resolver';
13
16
  import Field from '../../Field';
14
17
 
15
18
  mockMatchMedia();
@@ -337,6 +340,40 @@ describe('SelectInput', () => {
337
340
  expect(screen.getByLabelText(/Currency/)).toHaveAttribute('aria-haspopup');
338
341
  });
339
342
 
343
+ it('renders addonStart and addonEnd when using trigger as Button', () => {
344
+ type V2ButtonProps = Extract<ResolverButtonProps, { v2: true }>;
345
+
346
+ render(
347
+ <SelectInput
348
+ items={[{ type: 'option', value: 'USD' }]}
349
+ renderTrigger={({ content, className, size }) => {
350
+ const triggerButtonProps: V2ButtonProps = {
351
+ v2: true,
352
+ size,
353
+ className,
354
+ addonStart: {
355
+ type: 'icon',
356
+ value: <span data-testid="select-trigger-addon-start">Start</span>,
357
+ },
358
+ addonEnd: {
359
+ type: 'icon',
360
+ value: <span data-testid="select-trigger-addon-end">End</span>,
361
+ },
362
+ };
363
+
364
+ return (
365
+ <SelectInputTriggerButton as={Button} {...triggerButtonProps}>
366
+ {content}
367
+ </SelectInputTriggerButton>
368
+ );
369
+ }}
370
+ />,
371
+ );
372
+
373
+ expect(screen.getByTestId('select-trigger-addon-start')).toBeInTheDocument();
374
+ expect(screen.getByTestId('select-trigger-addon-end')).toBeInTheDocument();
375
+ });
376
+
340
377
  it('deduplicates search results across groups using compareValues as key', async () => {
341
378
  interface Currency {
342
379
  code: string;
@@ -5,7 +5,7 @@ import { PolymorphicWithOverrides } from '../../../common/PolymorphicWithOverrid
5
5
  import { Merge } from '../../../utils';
6
6
  import { SelectInputTriggerButtonPropsContext } from '../SelectInput.contexts';
7
7
 
8
- export type SelectInputTriggerButtonElementType = 'button' | React.ComponentType;
8
+ export type SelectInputTriggerButtonElementType = React.ElementType;
9
9
 
10
10
  export type SelectInputTriggerButtonProps<
11
11
  T extends SelectInputTriggerButtonElementType = 'button',
@@ -0,0 +1,28 @@
1
+ .wds-markup-emphasis {
2
+ font-weight: 600;
3
+ font-weight: var(--font-weight-semi-bold);
4
+ font-style: normal;
5
+ }
6
+ .wds-markup-emphasis--important {
7
+ color: #37517e;
8
+ color: var(--color-content-primary);
9
+ }
10
+ .wds-markup-emphasis--positive {
11
+ color: #008026;
12
+ color: var(--color-content-positive);
13
+ }
14
+ .wds-markup-emphasis--negative {
15
+ color: #cf2929;
16
+ color: var(--color-content-negative);
17
+ }
18
+ .wds-markup-strong {
19
+ font-weight: 600;
20
+ font-weight: var(--font-weight-semi-bold);
21
+ }
22
+ .wds-markup-strikethrough {
23
+ -webkit-text-decoration: line-through;
24
+ text-decoration: line-through;
25
+ }
26
+ .wds-markup-paragraph {
27
+ margin: 0;
28
+ }
@@ -0,0 +1,163 @@
1
+ import React from 'react';
2
+
3
+ import { render } from '../test-utils';
4
+
5
+ import { Markup } from './Markup';
6
+
7
+ /**
8
+ * Proof of Concept: Markup Injection via Attacker-Controlled Data
9
+ *
10
+ * These tests demonstrate Laszlo's concern: when untrusted data (e.g. a payee name,
11
+ * company name, or any string from an external source) is interpolated into a markup
12
+ * template string, the attacker can break out of their structural context and inject
13
+ * arbitrary allowed markup — links, emphasis, etc.
14
+ *
15
+ * This is NOT about XSS (script execution is blocked). It's about content spoofing
16
+ * and phishing within trusted UI.
17
+ */
18
+ describe('Markup injection via attacker-controlled data', () => {
19
+ describe('the vulnerability: structural injection', () => {
20
+ it('default allowedHrefs blocks injected external links', () => {
21
+ const attackerControlledName =
22
+ 'Acme Corp</strong>' +
23
+ '<link href="https://evil.example.com">' +
24
+ '<important>ACTION REQUIRED: Verify your identity</important>' +
25
+ '</link>' +
26
+ '<strong>';
27
+
28
+ const template = `Transfer <strong>£35.00</strong> to <strong>${attackerControlledName}</strong>`;
29
+
30
+ const { container } = render(<Markup>{template}</Markup>);
31
+
32
+ // The default allowlist blocks external domains — the link is NOT rendered
33
+ const injectedLink = container.querySelector('a[href="https://evil.example.com"]');
34
+ expect(injectedLink).not.toBeInTheDocument();
35
+
36
+ // But the structural injection still works (emphasis renders)
37
+ const injectedEmphasis = container.querySelector('.wds-markup-emphasis--important');
38
+ expect(injectedEmphasis).toBeInTheDocument();
39
+ expect(injectedEmphasis).toHaveTextContent('ACTION REQUIRED: Verify your identity');
40
+ });
41
+
42
+ it('injected external links render when allowLinks="all"', () => {
43
+ const attackerControlledName =
44
+ 'Acme Corp</strong>' +
45
+ '<link href="https://evil.example.com">' +
46
+ '<important>ACTION REQUIRED: Verify your identity</important>' +
47
+ '</link>' +
48
+ '<strong>';
49
+
50
+ const template = `Transfer <strong>£35.00</strong> to <strong>${attackerControlledName}</strong>`;
51
+
52
+ const { container } = render(<Markup allowLinks="all">{template}</Markup>);
53
+
54
+ // With allowLinks="all", the link renders — demonstrating the structural injection
55
+ const injectedLink = container.querySelector('a[href="https://evil.example.com"]');
56
+ expect(injectedLink).toBeInTheDocument();
57
+ });
58
+
59
+ it('default allowedHrefs blocks phishing links in paragraph injection', () => {
60
+ const attackerNote =
61
+ 'Thanks!</p>' +
62
+ '<p><important>Your account has been compromised. ' +
63
+ 'Contact support at </important>' +
64
+ '<link href="https://phishing.example.com">this secure link</link></p>' +
65
+ '<p>';
66
+
67
+ const template = `<p>Note from sender: ${attackerNote}</p>`;
68
+
69
+ const { container } = render(<Markup>{template}</Markup>);
70
+
71
+ const phishingLink = container.querySelector('a[href="https://phishing.example.com"]');
72
+ expect(phishingLink).not.toBeInTheDocument();
73
+ });
74
+
75
+ it('attacker can inject misleading accessibility labels', () => {
76
+ const attackerValue =
77
+ 'normal text</strong>' +
78
+ '<strong accessibilityLabel="Security alert: click to verify">' +
79
+ '<link href="https://evil.example.com">here</link>' +
80
+ '</strong>' +
81
+ '<strong>';
82
+
83
+ const template = `Your balance: <strong>${attackerValue}</strong>`;
84
+
85
+ const { container } = render(<Markup>{template}</Markup>);
86
+
87
+ const misleadingElement = container.querySelector(
88
+ '[aria-label="Security alert: click to verify"]',
89
+ );
90
+ expect(misleadingElement).toBeInTheDocument();
91
+ });
92
+ });
93
+
94
+ describe('what IS blocked (XSS / script execution)', () => {
95
+ it('cannot inject arbitrary HTML elements', () => {
96
+ const xssAttempt = '<img src=x onerror=alert(1)>';
97
+ const template = `Hello <strong>${xssAttempt}</strong>`;
98
+
99
+ const { container } = render(<Markup>{template}</Markup>);
100
+
101
+ // <img> is not in the allow-list, so it's rendered as text
102
+ expect(container.querySelector('img')).not.toBeInTheDocument();
103
+ expect(container).toHaveTextContent(/<img src=x onerror=alert\(1\)>/);
104
+ });
105
+
106
+ it('cannot inject script tags', () => {
107
+ const xssAttempt = '<script>alert("xss")</script>';
108
+ const template = `Hello <strong>${xssAttempt}</strong>`;
109
+
110
+ const { container } = render(<Markup>{template}</Markup>);
111
+
112
+ expect(container.querySelector('script')).not.toBeInTheDocument();
113
+ });
114
+
115
+ it('blocks javascript: URLs in links', () => {
116
+ // Even if attacker injects a link, javascript: URLs are blocked
117
+ const attack = 'click</strong><link href="javascript:alert(1)">me</link><strong>';
118
+ const template = `<strong>${attack}</strong>`;
119
+
120
+ const { container } = render(<Markup>{template}</Markup>);
121
+
122
+ // The link renders but without the dangerous href (falls back to Fragment)
123
+ expect(container.querySelector('a[href*="javascript"]')).not.toBeInTheDocument();
124
+ });
125
+ });
126
+
127
+ describe('the general nature of this problem', () => {
128
+ it('is identical to HTML injection via innerHTML', () => {
129
+ // This is the same class of bug — mixing code and data in a string
130
+ const attackerName = '<b>URGENT</b>';
131
+ const div = document.createElement('div');
132
+ div.innerHTML = `Hello ${attackerName}`;
133
+
134
+ // The attacker's data became structure
135
+ expect(div.querySelector('b')).not.toBeNull();
136
+ expect(div.querySelector('b')!).toHaveTextContent('URGENT');
137
+ });
138
+
139
+ it('react JSX prevents this by design', () => {
140
+ // In contrast, React treats variables as pure data — never as structure
141
+ const attackerName = '<strong>URGENT</strong>';
142
+
143
+ const { container } = render(<span>Hello {attackerName}</span>);
144
+
145
+ // React escapes the string — it renders as visible text, not as a <strong> tag
146
+ expect(container.querySelector('strong')).not.toBeInTheDocument();
147
+ expect(container).toHaveTextContent(/<strong>URGENT<\/strong>/);
148
+ });
149
+
150
+ it('demonstrates why Markup reintroduces the problem React solved', () => {
151
+ // The same attacker string that React handles safely...
152
+ const attackerName = '<strong>URGENT: Call +44 123 456 789 NOW</strong>';
153
+
154
+ // ...becomes dangerous when passed through Markup's string parser
155
+ const { container } = render(<Markup>{`Hello ${attackerName}`}</Markup>);
156
+
157
+ // The attacker's data has been promoted to structure
158
+ const injected = container.querySelector('strong');
159
+ expect(injected).toBeInTheDocument();
160
+ expect(injected).toHaveTextContent('URGENT: Call +44 123 456 789 NOW');
161
+ });
162
+ });
163
+ });
@@ -0,0 +1,29 @@
1
+ .wds-markup-emphasis {
2
+ font-weight: var(--font-weight-semi-bold);
3
+ font-style: normal;
4
+ }
5
+
6
+ .wds-markup-emphasis--important {
7
+ color: var(--color-content-primary);
8
+ }
9
+
10
+ .wds-markup-emphasis--positive {
11
+ color: var(--color-content-positive);
12
+ }
13
+
14
+ .wds-markup-emphasis--negative {
15
+ color: var(--color-content-negative);
16
+ }
17
+
18
+ .wds-markup-strong {
19
+ font-weight: var(--font-weight-semi-bold);
20
+ }
21
+
22
+ /// @see https://www.webaxe.org/strikethrough-html-accessibility/
23
+ .wds-markup-strikethrough {
24
+ text-decoration: line-through;
25
+ }
26
+
27
+ .wds-markup-paragraph {
28
+ margin: 0;
29
+ }