@transferwise/components 46.156.0 → 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 (215) 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/Typeahead/Typeahead.js +53 -27
  68. package/build/Typeahead/Typeahead.js.map +1 -1
  69. package/build/Typeahead/Typeahead.messages.js +3 -0
  70. package/build/Typeahead/Typeahead.messages.js.map +1 -1
  71. package/build/Typeahead/Typeahead.messages.mjs +3 -0
  72. package/build/Typeahead/Typeahead.messages.mjs.map +1 -1
  73. package/build/Typeahead/Typeahead.mjs +53 -27
  74. package/build/Typeahead/Typeahead.mjs.map +1 -1
  75. package/build/Typeahead/TypeaheadInput/TypeaheadInput.js +2 -2
  76. package/build/Typeahead/TypeaheadInput/TypeaheadInput.js.map +1 -1
  77. package/build/Typeahead/TypeaheadInput/TypeaheadInput.mjs +2 -2
  78. package/build/Typeahead/TypeaheadInput/TypeaheadInput.mjs.map +1 -1
  79. package/build/Typeahead/TypeaheadOption/TypeaheadOption.js +3 -1
  80. package/build/Typeahead/TypeaheadOption/TypeaheadOption.js.map +1 -1
  81. package/build/Typeahead/TypeaheadOption/TypeaheadOption.mjs +3 -1
  82. package/build/Typeahead/TypeaheadOption/TypeaheadOption.mjs.map +1 -1
  83. package/build/i18n/en.json +5 -0
  84. package/build/i18n/en.json.js +5 -0
  85. package/build/i18n/en.json.js.map +1 -1
  86. package/build/i18n/en.json.mjs +5 -0
  87. package/build/i18n/en.json.mjs.map +1 -1
  88. package/build/index.js +2 -2
  89. package/build/index.mjs +1 -1
  90. package/build/main.css +133 -43
  91. package/build/styles/Calendar/Calendar.css +112 -37
  92. package/build/styles/Calendar/TableLink/TableLink.css +115 -0
  93. package/build/styles/Typeahead/Typeahead.css +4 -0
  94. package/build/styles/Typeahead/TypeaheadOption/TypeaheadOption.css +5 -0
  95. package/build/styles/main.css +133 -43
  96. package/build/types/Body/Body.d.ts +2 -2
  97. package/build/types/Calendar/Calendar.constants.d.ts +2 -0
  98. package/build/types/Calendar/Calendar.constants.d.ts.map +1 -0
  99. package/build/types/Calendar/Calendar.d.ts +16 -3
  100. package/build/types/Calendar/Calendar.d.ts.map +1 -1
  101. package/build/types/Calendar/Calendar.messages.d.ts +20 -0
  102. package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
  103. package/build/types/Calendar/Calendar.types.d.ts +32 -0
  104. package/build/types/Calendar/Calendar.types.d.ts.map +1 -0
  105. package/build/types/Calendar/DateHeader/DateHeader.d.ts +2 -0
  106. package/build/types/Calendar/DateHeader/DateHeader.d.ts.map +1 -1
  107. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +7 -1
  108. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
  109. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +6 -0
  110. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
  111. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts +1 -0
  112. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts.map +1 -1
  113. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts +2 -1
  114. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts.map +1 -1
  115. package/build/types/Calendar/{tableLink → TableLink}/TableLink.d.ts +5 -1
  116. package/build/types/Calendar/TableLink/TableLink.d.ts.map +1 -0
  117. package/build/types/Calendar/TableLink/index.d.ts.map +1 -0
  118. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts +2 -1
  119. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts.map +1 -1
  120. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts +1 -0
  121. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts.map +1 -1
  122. package/build/types/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.d.ts +8 -0
  123. package/build/types/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.d.ts.map +1 -0
  124. package/build/types/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +3 -0
  125. package/build/types/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +1 -0
  126. package/build/types/Calendar/_helpers/index.d.ts +4 -0
  127. package/build/types/Calendar/_helpers/index.d.ts.map +1 -0
  128. package/build/types/Calendar/_helpers/isDateDisabled/isDateDisabled.d.ts +3 -0
  129. package/build/types/Calendar/_helpers/isDateDisabled/isDateDisabled.d.ts.map +1 -0
  130. package/build/types/Calendar/index.d.ts +1 -0
  131. package/build/types/Calendar/index.d.ts.map +1 -1
  132. package/build/types/DateLookup/DateLookup.d.ts +24 -5
  133. package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
  134. package/build/types/DateLookup/index.d.ts +1 -0
  135. package/build/types/DateLookup/index.d.ts.map +1 -1
  136. package/build/types/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.d.ts +6 -4
  137. package/build/types/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.d.ts.map +1 -1
  138. package/build/types/IconButton/IconButton.d.ts +1 -1
  139. package/build/types/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts +1 -1
  140. package/build/types/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.d.ts.map +1 -1
  141. package/build/types/MoneyInput/MoneyInput.d.ts +1 -1
  142. package/build/types/MoneyInput/MoneyInput.d.ts.map +1 -1
  143. package/build/types/Title/Title.d.ts +2 -2
  144. package/build/types/Typeahead/Typeahead.d.ts.map +1 -1
  145. package/build/types/Typeahead/Typeahead.messages.d.ts +5 -0
  146. package/build/types/Typeahead/Typeahead.messages.d.ts.map +1 -1
  147. package/build/types/Typeahead/TypeaheadInput/TypeaheadInput.d.ts.map +1 -1
  148. package/build/types/Typeahead/TypeaheadOption/TypeaheadOption.d.ts +1 -0
  149. package/build/types/Typeahead/TypeaheadOption/TypeaheadOption.d.ts.map +1 -1
  150. package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
  151. package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
  152. package/build/types/index.d.ts +3 -2
  153. package/build/types/index.d.ts.map +1 -1
  154. package/build/types/test-utils/index.d.ts +10 -0
  155. package/build/types/test-utils/index.d.ts.map +1 -1
  156. package/package.json +1 -1
  157. package/src/Calendar/Calendar.constants.ts +5 -0
  158. package/src/Calendar/Calendar.css +112 -37
  159. package/src/Calendar/Calendar.disabledDates.test.tsx +610 -0
  160. package/src/Calendar/Calendar.less +5 -35
  161. package/src/Calendar/Calendar.messages.ts +21 -0
  162. package/src/Calendar/Calendar.test.tsx +342 -32
  163. package/src/Calendar/Calendar.tsx +169 -19
  164. package/src/Calendar/Calendar.types.ts +35 -0
  165. package/src/Calendar/DateHeader/DateHeader.tsx +6 -0
  166. package/src/Calendar/DayCalendar/DayCalendar.tsx +39 -1
  167. package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +67 -27
  168. package/src/Calendar/MonthCalendar/MonthCalendar.tsx +8 -0
  169. package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +15 -3
  170. package/src/Calendar/TableLink/TableLink.css +115 -0
  171. package/src/Calendar/TableLink/TableLink.less +68 -0
  172. package/src/Calendar/TableLink/TableLink.tsx +117 -0
  173. package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +15 -3
  174. package/src/Calendar/YearCalendar/YearCalendar.tsx +9 -0
  175. package/src/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.test.ts +90 -0
  176. package/src/Calendar/_helpers/clampToDayViewBoundary/clampToDayViewBoundary.ts +25 -0
  177. package/src/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +98 -0
  178. package/src/Calendar/_helpers/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +45 -0
  179. package/src/Calendar/_helpers/index.ts +3 -0
  180. package/src/Calendar/_helpers/isDateDisabled/isDateDisabled.test.ts +169 -0
  181. package/src/Calendar/_helpers/isDateDisabled/isDateDisabled.ts +59 -0
  182. package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +118 -0
  183. package/src/Calendar/_stories/Calendar.story.tsx +144 -4
  184. package/src/Calendar/_stories/Calendar.test.story.tsx +114 -0
  185. package/src/Calendar/index.ts +7 -0
  186. package/src/DateLookup/DateLookup.disabledDates.test.tsx +312 -0
  187. package/src/DateLookup/DateLookup.test.tsx +34 -37
  188. package/src/DateLookup/DateLookup.tsx +37 -13
  189. package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +108 -0
  190. package/src/DateLookup/_stories/DateLookup.story.tsx +126 -2
  191. package/src/DateLookup/index.ts +6 -0
  192. package/src/ExpressiveMoneyInput/CurrencySelector/CurrencySelector.tsx +12 -13
  193. package/src/Inputs/SelectInput/SelectInput.test.tsx +37 -0
  194. package/src/Inputs/SelectInput/TriggerButton/SelectInputTriggerButton.tsx +1 -1
  195. package/src/Typeahead/Typeahead.css +4 -0
  196. package/src/Typeahead/Typeahead.less +2 -0
  197. package/src/Typeahead/Typeahead.messages.ts +5 -0
  198. package/src/Typeahead/Typeahead.test.tsx +80 -3
  199. package/src/Typeahead/Typeahead.tsx +50 -27
  200. package/src/Typeahead/TypeaheadInput/TypeaheadInput.tsx +2 -6
  201. package/src/Typeahead/TypeaheadOption/TypeaheadOption.css +5 -0
  202. package/src/Typeahead/TypeaheadOption/TypeaheadOption.less +9 -0
  203. package/src/Typeahead/TypeaheadOption/TypeaheadOption.tsx +4 -2
  204. package/src/i18n/en.json +4 -0
  205. package/src/index.ts +3 -2
  206. package/src/main.css +133 -43
  207. package/build/Calendar/tableLink/TableLink.js +0 -61
  208. package/build/Calendar/tableLink/TableLink.js.map +0 -1
  209. package/build/Calendar/tableLink/TableLink.mjs +0 -57
  210. package/build/Calendar/tableLink/TableLink.mjs.map +0 -1
  211. package/build/types/Calendar/tableLink/TableLink.d.ts.map +0 -1
  212. package/build/types/Calendar/tableLink/index.d.ts.map +0 -1
  213. package/src/Calendar/tableLink/TableLink.tsx +0 -77
  214. /package/build/types/Calendar/{tableLink → TableLink}/index.d.ts +0 -0
  215. /package/src/Calendar/{tableLink → TableLink}/index.ts +0 -0
@@ -0,0 +1,610 @@
1
+ import { fireEvent, mockMatchMedia, render, screen, userEvent } from '../test-utils';
2
+ import Calendar from './Calendar';
3
+ import type { DisabledRules } from './Calendar.types';
4
+
5
+ mockMatchMedia();
6
+
7
+ const user = userEvent.setup();
8
+
9
+ // 2024-07-01 is a Monday
10
+ const initialValue = new Date(2024, 6, 1);
11
+
12
+ const weekendRules: DisabledRules = {
13
+ weekdays: ['sat', 'sun'],
14
+ reason: 'We are closed on weekends.',
15
+ };
16
+
17
+ const bankHolidayRules: DisabledRules = {
18
+ dates: [new Date(2024, 6, 4), new Date(2024, 6, 11)],
19
+ reason: 'The service is unavailable on bank holidays.',
20
+ };
21
+
22
+ const rangeRules: DisabledRules = {
23
+ ranges: [{ start: new Date(2024, 6, 15), end: new Date(2024, 6, 19) }],
24
+ reason: 'Scheduled maintenance.',
25
+ };
26
+
27
+ describe('Calendar - disabled dates', () => {
28
+ describe('rendering disabled state', () => {
29
+ it('renders disabled weekday cells with aria-disabled', () => {
30
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
31
+
32
+ // 2024-07-06 is Saturday
33
+ const saturday = screen.getByLabelText(/^6 July 2024/u);
34
+ expect(saturday).toHaveAttribute('aria-disabled', 'true');
35
+ });
36
+
37
+ it('renders enabled weekday cells without aria-disabled', () => {
38
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
39
+
40
+ // 2024-07-01 is Monday
41
+ const monday = screen.getByLabelText(/^1 July 2024.*selected/iu);
42
+ expect(monday).not.toHaveAttribute('aria-disabled');
43
+ });
44
+
45
+ it('renders disabled specific date cells with aria-disabled', () => {
46
+ render(<Calendar value={initialValue} disabledRules={bankHolidayRules} />);
47
+
48
+ const holiday = screen.getByLabelText(/^4 July 2024/u);
49
+ expect(holiday).toHaveAttribute('aria-disabled', 'true');
50
+ });
51
+
52
+ it('renders disabled range cells with aria-disabled', () => {
53
+ render(<Calendar value={initialValue} disabledRules={rangeRules} />);
54
+
55
+ const rangeDate = screen.getByLabelText(/16 July 2024/u);
56
+ expect(rangeDate).toHaveAttribute('aria-disabled', 'true');
57
+ });
58
+
59
+ it('disabled cells are not natively disabled (remain interactive)', () => {
60
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
61
+
62
+ const saturday = screen.getByLabelText(/^6 July 2024/u);
63
+ expect(saturday).toBeEnabled();
64
+ });
65
+ });
66
+
67
+ describe('click interaction', () => {
68
+ it('fires onDateFocus exactly once when an enabled date cell is clicked', async () => {
69
+ const handleDateFocus = jest.fn();
70
+ render(
71
+ <Calendar
72
+ value={initialValue}
73
+ disabledRules={bankHolidayRules}
74
+ onDateFocus={handleDateFocus}
75
+ />,
76
+ );
77
+ handleDateFocus.mockClear();
78
+
79
+ await user.click(screen.getByLabelText(/^3 July 2024/u));
80
+
81
+ expect(handleDateFocus).toHaveBeenCalledTimes(1);
82
+ expect(handleDateFocus).toHaveBeenCalledWith(new Date(2024, 6, 3), { isEnabled: true });
83
+ });
84
+
85
+ it('fires onDateFocus with isEnabled: false when clicking a rule-disabled date', async () => {
86
+ const handleDateFocus = jest.fn();
87
+ const handleChange = jest.fn();
88
+ render(
89
+ <Calendar
90
+ value={initialValue}
91
+ disabledRules={bankHolidayRules}
92
+ onDateFocus={handleDateFocus}
93
+ onChange={handleChange}
94
+ />,
95
+ );
96
+
97
+ const holiday = screen.getByLabelText(/^4 July 2024/u);
98
+ await user.click(holiday);
99
+
100
+ expect(handleDateFocus).toHaveBeenCalledWith(new Date(2024, 6, 4), { isEnabled: false });
101
+ expect(handleChange).not.toHaveBeenCalled();
102
+ });
103
+
104
+ it('fires onDateFocus and onChange when clicking an enabled date', async () => {
105
+ const handleDateFocus = jest.fn();
106
+ const handleChange = jest.fn();
107
+ render(
108
+ <Calendar
109
+ value={initialValue}
110
+ disabledRules={bankHolidayRules}
111
+ onDateFocus={handleDateFocus}
112
+ onChange={handleChange}
113
+ />,
114
+ );
115
+
116
+ const enabledDate = screen.getByLabelText(/^3 July 2024/u);
117
+ await user.click(enabledDate);
118
+
119
+ expect(handleDateFocus).toHaveBeenCalledWith(new Date(2024, 6, 3), { isEnabled: true });
120
+ expect(handleChange).toHaveBeenCalledWith(new Date(2024, 6, 3));
121
+ });
122
+ });
123
+
124
+ describe('keyboard navigation', () => {
125
+ it('does NOT skip disabled dates and fires onDateFocus with isEnabled: false', async () => {
126
+ const handleDateFocus = jest.fn();
127
+ render(
128
+ <Calendar
129
+ value={new Date(2024, 6, 5)}
130
+ disabledRules={weekendRules}
131
+ onDateFocus={handleDateFocus}
132
+ />,
133
+ );
134
+
135
+ await user.click(screen.getByRole('button', { name: /selected day/iu }));
136
+ // ArrowRight from July 5 (Friday) should go to July 6 (Saturday = disabled)
137
+ await user.keyboard('{ArrowRight}');
138
+
139
+ expect(handleDateFocus).toHaveBeenCalledWith(new Date(2024, 6, 6), { isEnabled: false });
140
+ });
141
+
142
+ it('fires onDateFocus with isEnabled: true when navigating to an enabled date', async () => {
143
+ const handleDateFocus = jest.fn();
144
+ render(
145
+ <Calendar
146
+ value={new Date(2024, 6, 1)}
147
+ disabledRules={weekendRules}
148
+ onDateFocus={handleDateFocus}
149
+ />,
150
+ );
151
+
152
+ await user.click(screen.getByRole('button', { name: /selected day/iu }));
153
+ // ArrowRight from July 1 (Monday) → July 2 (Tuesday = enabled)
154
+ await user.keyboard('{ArrowRight}');
155
+
156
+ expect(handleDateFocus).toHaveBeenCalledWith(new Date(2024, 6, 2), { isEnabled: true });
157
+ });
158
+
159
+ it('does not fire onChange when Enter is pressed on a rule-disabled focused date', async () => {
160
+ const handleChange = jest.fn();
161
+ render(
162
+ <Calendar
163
+ value={new Date(2024, 6, 5)}
164
+ disabledRules={weekendRules}
165
+ onChange={handleChange}
166
+ />,
167
+ );
168
+
169
+ await user.click(screen.getByRole('button', { name: /selected day/iu }));
170
+ // Click on selected day (Friday) fires onChange once
171
+ expect(handleChange).toHaveBeenCalledTimes(1);
172
+
173
+ // ArrowRight from July 5 (Friday) → July 6 (Saturday = disabled)
174
+ await user.keyboard('{ArrowRight}');
175
+ await user.keyboard('{Enter}');
176
+
177
+ expect(handleChange).toHaveBeenCalledTimes(1);
178
+ });
179
+
180
+ it('fires onChange when Enter is pressed on an enabled focused date', async () => {
181
+ const handleChange = jest.fn();
182
+ render(
183
+ <Calendar
184
+ value={new Date(2024, 6, 1)}
185
+ disabledRules={weekendRules}
186
+ onChange={handleChange}
187
+ />,
188
+ );
189
+
190
+ await user.click(screen.getByRole('button', { name: /selected day/iu }));
191
+ // ArrowRight from July 1 (Monday) → July 2 (Tuesday = enabled)
192
+ await user.keyboard('{ArrowRight}');
193
+ await user.keyboard('{Enter}');
194
+
195
+ expect(handleChange).toHaveBeenCalledWith(new Date(2024, 6, 2));
196
+ });
197
+ });
198
+
199
+ describe('InlinePrompt for rule-disabled dates', () => {
200
+ it('renders reason text when month has disabled dates', () => {
201
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
202
+
203
+ expect(screen.getByText('We are closed on weekends.')).toBeInTheDocument();
204
+ });
205
+
206
+ it('does not render when no disabledRules provided and no out-of-range date is focused', () => {
207
+ render(<Calendar value={initialValue} />);
208
+
209
+ expect(screen.queryByRole('status')).not.toBeInTheDocument();
210
+ expect(document.querySelector('.wds-inline-prompt')).not.toBeInTheDocument();
211
+ });
212
+
213
+ it('renders with neutral sentiment when focused date is enabled', () => {
214
+ const { container } = render(<Calendar value={initialValue} disabledRules={weekendRules} />);
215
+
216
+ const prompt = container.querySelector('.wds-inline-prompt');
217
+ expect(prompt).toBeInTheDocument();
218
+ expect(prompt).toHaveClass('wds-prompt--neutral');
219
+ });
220
+
221
+ it('does not render in month view', async () => {
222
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
223
+
224
+ await user.click(screen.getByRole('button', { name: /year view/iu }));
225
+ await user.keyboard(' ');
226
+
227
+ expect(screen.queryByText('We are closed on weekends.')).not.toBeInTheDocument();
228
+ });
229
+
230
+ it('does not render in year view', async () => {
231
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
232
+
233
+ await user.click(screen.getByRole('button', { name: /year view/iu }));
234
+
235
+ expect(screen.queryByText('We are closed on weekends.')).not.toBeInTheDocument();
236
+ });
237
+ });
238
+
239
+ describe('accessibility for rule-disabled dates', () => {
240
+ it('disabled cells announce "not available" in aria-label', () => {
241
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
242
+
243
+ const saturday = screen.getByLabelText(/6 July 2024.*not available/iu);
244
+ expect(saturday).toBeInTheDocument();
245
+ });
246
+
247
+ it('disabled cells have accessible description with the reason text', () => {
248
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
249
+
250
+ const saturday = screen.getByLabelText(/6 July 2024.*not available/iu);
251
+ expect(saturday).toHaveAccessibleDescription('We are closed on weekends.');
252
+ });
253
+
254
+ it('initially focused cell has accessible description for first-entry announcement', () => {
255
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
256
+
257
+ const selected = screen.getByLabelText(/^1 July 2024.*selected/iu);
258
+ expect(selected).toHaveAccessibleDescription('We are closed on weekends.');
259
+ });
260
+
261
+ it('enabled non-focused cells have no accessible description', () => {
262
+ render(<Calendar value={initialValue} disabledRules={weekendRules} />);
263
+
264
+ const tuesday = screen.getByLabelText(/^2 July 2024/u);
265
+ expect(tuesday).toHaveAccessibleDescription('');
266
+ });
267
+ });
268
+
269
+ describe('InlinePrompt sentiment for rule-disabled dates', () => {
270
+ it('shows negative sentiment when focused date is rule-disabled', () => {
271
+ const { container } = render(
272
+ <Calendar value={new Date(2024, 6, 6)} disabledRules={weekendRules} />,
273
+ );
274
+
275
+ const prompt = container.querySelector('.wds-inline-prompt')!;
276
+ expect(prompt).toHaveClass('wds-prompt--negative');
277
+ });
278
+
279
+ it('shows neutral sentiment when focused date is enabled', () => {
280
+ const { container } = render(
281
+ <Calendar value={new Date(2024, 6, 8)} disabledRules={weekendRules} />,
282
+ );
283
+
284
+ const prompt = container.querySelector('.wds-inline-prompt')!;
285
+ expect(prompt).toHaveClass('wds-prompt--neutral');
286
+ });
287
+
288
+ it('updates sentiment when value changes from disabled to enabled', () => {
289
+ const { container, rerender } = render(
290
+ <Calendar value={new Date(2024, 6, 6)} disabledRules={weekendRules} />,
291
+ );
292
+
293
+ const prompt = container.querySelector('.wds-inline-prompt')!;
294
+ expect(prompt).toHaveClass('wds-prompt--negative');
295
+
296
+ rerender(<Calendar value={new Date(2024, 6, 5)} disabledRules={weekendRules} />);
297
+
298
+ expect(prompt).toHaveClass('wds-prompt--neutral');
299
+ expect(prompt).not.toHaveClass('wds-prompt--negative');
300
+ });
301
+ });
302
+
303
+ describe('dynamic disabledRules prop changes', () => {
304
+ it('clears aria-disabled and removes InlinePrompt when disabledRules is removed', () => {
305
+ const onDateFocus = jest.fn();
306
+ const { rerender } = render(
307
+ <Calendar value={initialValue} disabledRules={weekendRules} onDateFocus={onDateFocus} />,
308
+ );
309
+
310
+ expect(screen.getByLabelText(/^6 July 2024/u)).toHaveAttribute('aria-disabled', 'true');
311
+ expect(screen.getByText('We are closed on weekends.')).toBeInTheDocument();
312
+
313
+ rerender(<Calendar value={initialValue} onDateFocus={onDateFocus} />);
314
+
315
+ expect(screen.getByLabelText(/^6 July 2024/u)).not.toHaveAttribute('aria-disabled');
316
+ expect(screen.queryByText('We are closed on weekends.')).not.toBeInTheDocument();
317
+ });
318
+
319
+ it('shows neutral sentiment when disabledRules is added and focused date is enabled', async () => {
320
+ const onDateFocus = jest.fn();
321
+ const { rerender } = render(<Calendar value={initialValue} onDateFocus={onDateFocus} />);
322
+
323
+ expect(screen.queryByText('We are closed on weekends.')).not.toBeInTheDocument();
324
+
325
+ rerender(
326
+ <Calendar value={initialValue} disabledRules={weekendRules} onDateFocus={onDateFocus} />,
327
+ );
328
+
329
+ const prompt = await screen.findByText('We are closed on weekends.');
330
+ const promptElement = prompt.closest('.wds-inline-prompt')!;
331
+ expect(promptElement).toHaveClass('wds-prompt--neutral');
332
+ });
333
+ });
334
+
335
+ describe('outOfRange behaviour', () => {
336
+ const minDate = new Date(2024, 6, 3);
337
+ const maxDate = new Date(2024, 6, 25);
338
+
339
+ describe('rendering', () => {
340
+ it('out-of-range dates use aria-disabled, NOT native disabled', () => {
341
+ render(<Calendar value={new Date(2024, 6, 5)} min={minDate} max={maxDate} />);
342
+
343
+ const outOfRangeDate = screen.getByRole('button', { name: /^1 July 2024/u });
344
+ expect(outOfRangeDate).toHaveAttribute('aria-disabled', 'true');
345
+ expect(outOfRangeDate).toBeEnabled();
346
+ });
347
+
348
+ it('out-of-range dates announce "not available" in aria-label', () => {
349
+ render(<Calendar value={new Date(2024, 6, 5)} min={minDate} max={maxDate} />);
350
+
351
+ expect(
352
+ screen.getByRole('button', { name: /^1 July 2024.*not available/iu }),
353
+ ).toBeInTheDocument();
354
+ });
355
+
356
+ it('when a date is both out-of-range and rule-disabled, it uses aria-disabled', () => {
357
+ render(
358
+ <Calendar
359
+ value={new Date(2024, 6, 5)}
360
+ min={minDate}
361
+ max={maxDate}
362
+ disabledRules={weekendRules}
363
+ />,
364
+ );
365
+
366
+ // July 27 is a Saturday (weekendRules) AND beyond max
367
+ const saturdayBeyondMax = screen.getByRole('button', { name: /^27 July 2024/u });
368
+ expect(saturdayBeyondMax).toHaveAttribute('aria-disabled', 'true');
369
+ expect(saturdayBeyondMax).toBeEnabled();
370
+ });
371
+ });
372
+
373
+ describe('click interaction', () => {
374
+ it('fires onDateFocus with isEnabled: false when clicking an out-of-range date', async () => {
375
+ const handleDateFocus = jest.fn();
376
+ const handleChange = jest.fn();
377
+ render(
378
+ <Calendar
379
+ value={new Date(2024, 6, 5)}
380
+ min={minDate}
381
+ max={maxDate}
382
+ onDateFocus={handleDateFocus}
383
+ onChange={handleChange}
384
+ />,
385
+ );
386
+
387
+ const outOfRangeDate = screen.getByRole('button', { name: /^1 July 2024/u });
388
+ await user.click(outOfRangeDate);
389
+
390
+ expect(handleDateFocus).toHaveBeenCalledWith(new Date(2024, 6, 1), { isEnabled: false });
391
+ expect(handleChange).not.toHaveBeenCalled();
392
+ });
393
+
394
+ it('does not fire onChange when Enter is pressed on a focused out-of-range date', async () => {
395
+ const handleChange = jest.fn();
396
+ // value is July 3 (at min boundary) — ArrowLeft would clamp, so navigate from July 4
397
+ render(
398
+ <Calendar
399
+ value={new Date(2024, 6, 4)}
400
+ min={minDate}
401
+ max={maxDate}
402
+ onChange={handleChange}
403
+ />,
404
+ );
405
+
406
+ const outOfRangeDate = screen.getByRole('button', { name: /^1 July 2024/u });
407
+ await user.click(outOfRangeDate);
408
+ // Click already fires onDateFocus but not onChange; now press Enter
409
+ await user.keyboard('{Enter}');
410
+
411
+ expect(handleChange).not.toHaveBeenCalled();
412
+ });
413
+ });
414
+
415
+ describe('InlinePrompt', () => {
416
+ it('shows negative prompt with boundary dates when out-of-range date is focused', async () => {
417
+ const { container } = render(
418
+ <Calendar value={new Date(2024, 6, 5)} min={minDate} max={maxDate} />,
419
+ );
420
+
421
+ await user.click(screen.getByRole('button', { name: /^1 July 2024/u }));
422
+
423
+ const prompt = container.querySelector('.wds-inline-prompt');
424
+ expect(prompt).toBeInTheDocument();
425
+ expect(prompt).toHaveClass('wds-prompt--negative');
426
+ expect(prompt).toHaveTextContent(
427
+ 'Please select a date between 3 July 2024 and 25 July 2024.',
428
+ );
429
+ });
430
+
431
+ it('shows boundary message (not disabledRules reason) when out-of-range date is focused', async () => {
432
+ const { container } = render(
433
+ <Calendar
434
+ value={new Date(2024, 6, 5)}
435
+ min={minDate}
436
+ max={maxDate}
437
+ disabledRules={weekendRules}
438
+ />,
439
+ );
440
+
441
+ // July 27 is beyond max
442
+ await user.click(screen.getByRole('button', { name: /^27 July 2024/u }));
443
+
444
+ const prompt = container.querySelector('.wds-inline-prompt');
445
+ expect(prompt).toHaveTextContent(
446
+ 'Please select a date between 3 July 2024 and 25 July 2024.',
447
+ );
448
+ expect(prompt).not.toHaveTextContent('We are closed on weekends.');
449
+ });
450
+
451
+ it('shows only-min message when only min is set', async () => {
452
+ const { container } = render(<Calendar value={new Date(2024, 6, 5)} min={minDate} />);
453
+
454
+ await user.click(screen.getByRole('button', { name: /^1 July 2024/u }));
455
+
456
+ expect(container.querySelector('.wds-inline-prompt')).toHaveTextContent(
457
+ 'Please enter 3 July 2024 or a later date.',
458
+ );
459
+ });
460
+
461
+ it('shows only-max message when only max is set', async () => {
462
+ const { container } = render(<Calendar value={new Date(2024, 6, 5)} max={maxDate} />);
463
+
464
+ await user.click(screen.getByRole('button', { name: /^27 July 2024/u }));
465
+
466
+ expect(container.querySelector('.wds-inline-prompt')).toHaveTextContent(
467
+ 'Please enter 25 July 2024 or an earlier date.',
468
+ );
469
+ });
470
+
471
+ it('hides out-of-range prompt (sr-only) when focused date returns to within range', async () => {
472
+ const { container } = render(
473
+ <Calendar value={new Date(2024, 6, 5)} min={minDate} max={maxDate} />,
474
+ );
475
+
476
+ // Focus an out-of-range date to show the prompt visually
477
+ await user.click(screen.getByRole('button', { name: /^1 July 2024/u }));
478
+ expect(container.querySelector('.wds-inline-prompt')).not.toHaveClass('sr-only');
479
+
480
+ // Focus an in-range date — prompt goes back to sr-only
481
+ await user.click(screen.getByRole('button', { name: /^5 July 2024/u }));
482
+ expect(container.querySelector('.wds-inline-prompt')).toHaveClass('sr-only');
483
+ });
484
+
485
+ it('does not show out-of-range prompt in month view', async () => {
486
+ const { container } = render(
487
+ <Calendar value={new Date(2024, 6, 5)} min={minDate} max={maxDate} />,
488
+ );
489
+
490
+ await user.click(screen.getByRole('button', { name: /year view/iu }));
491
+ await user.keyboard(' ');
492
+
493
+ expect(container.querySelector('.wds-inline-prompt')).not.toBeInTheDocument();
494
+ });
495
+
496
+ it('does not show out-of-range prompt in year view', async () => {
497
+ const { container } = render(
498
+ <Calendar value={new Date(2024, 6, 5)} min={minDate} max={maxDate} />,
499
+ );
500
+
501
+ await user.click(screen.getByRole('button', { name: /year view/iu }));
502
+
503
+ expect(container.querySelector('.wds-inline-prompt')).not.toBeInTheDocument();
504
+ });
505
+ });
506
+
507
+ describe('accessibility', () => {
508
+ it('out-of-range dates have accessible description with boundary message', () => {
509
+ render(<Calendar value={new Date(2024, 6, 5)} min={minDate} max={maxDate} />);
510
+
511
+ const outOfRangeDate = screen.getByRole('button', { name: /^1 July 2024/u });
512
+ expect(outOfRangeDate).toHaveAccessibleDescription(
513
+ 'Please select a date between 3 July 2024 and 25 July 2024.',
514
+ );
515
+ });
516
+
517
+ it('out-of-range prompt is visually hidden until an out-of-range date is focused', async () => {
518
+ const { container } = render(
519
+ <Calendar value={new Date(2024, 6, 5)} min={minDate} max={maxDate} />,
520
+ );
521
+
522
+ const promptBefore = container.querySelector('.wds-inline-prompt');
523
+ expect(promptBefore).toBeInTheDocument();
524
+ expect(promptBefore).toHaveClass('sr-only');
525
+
526
+ await user.click(screen.getByRole('button', { name: /^1 July 2024/u }));
527
+
528
+ const promptAfter = container.querySelector('.wds-inline-prompt');
529
+ expect(promptAfter).not.toHaveClass('sr-only');
530
+ });
531
+ });
532
+ });
533
+
534
+ describe('screen reader focus navigation', () => {
535
+ it('fires onDateFocus with isEnabled: false when a disabled cell receives DOM focus', () => {
536
+ const handleDateFocus = jest.fn();
537
+ render(
538
+ <Calendar
539
+ value={initialValue}
540
+ disabledRules={weekendRules}
541
+ onDateFocus={handleDateFocus}
542
+ />,
543
+ );
544
+
545
+ const saturday = screen.getByLabelText(/^6 July 2024/u);
546
+ fireEvent.focus(saturday);
547
+
548
+ expect(handleDateFocus).toHaveBeenCalledWith(new Date(2024, 6, 6), { isEnabled: false });
549
+ });
550
+
551
+ it('fires onDateFocus with isEnabled: true when an enabled cell receives DOM focus', () => {
552
+ const handleDateFocus = jest.fn();
553
+ render(
554
+ <Calendar
555
+ value={initialValue}
556
+ disabledRules={weekendRules}
557
+ onDateFocus={handleDateFocus}
558
+ />,
559
+ );
560
+
561
+ const wednesday = screen.getByLabelText(/^3 July 2024/u);
562
+ fireEvent.focus(wednesday);
563
+
564
+ expect(handleDateFocus).toHaveBeenCalledWith(new Date(2024, 6, 3), { isEnabled: true });
565
+ });
566
+
567
+ it('updates prompt sentiment when screen reader moves focus to a disabled cell', () => {
568
+ const { container } = render(<Calendar value={initialValue} disabledRules={weekendRules} />);
569
+
570
+ const prompt = container.querySelector('.wds-inline-prompt')!;
571
+ expect(prompt).toHaveClass('wds-prompt--neutral');
572
+
573
+ const saturday = screen.getByLabelText(/^6 July 2024/u);
574
+ fireEvent.focus(saturday);
575
+
576
+ expect(prompt).toHaveClass('wds-prompt--negative');
577
+ });
578
+
579
+ it('updates prompt sentiment back to neutral when focus moves to an enabled cell', () => {
580
+ const { container } = render(
581
+ <Calendar value={new Date(2024, 6, 6)} disabledRules={weekendRules} />,
582
+ );
583
+
584
+ const prompt = container.querySelector('.wds-inline-prompt')!;
585
+ expect(prompt).toHaveClass('wds-prompt--negative');
586
+
587
+ const monday = screen.getByLabelText(/^8 July 2024/u);
588
+ fireEvent.focus(monday);
589
+
590
+ expect(prompt).toHaveClass('wds-prompt--neutral');
591
+ });
592
+
593
+ it('fires onDateFocus with isEnabled: false when an out-of-range cell receives DOM focus', () => {
594
+ const handleDateFocus = jest.fn();
595
+ render(
596
+ <Calendar
597
+ value={new Date(2024, 6, 5)}
598
+ min={new Date(2024, 6, 3)}
599
+ max={new Date(2024, 6, 25)}
600
+ onDateFocus={handleDateFocus}
601
+ />,
602
+ );
603
+
604
+ const outOfRange = screen.getByRole('button', { name: /^1 July 2024/u });
605
+ fireEvent.focus(outOfRange);
606
+
607
+ expect(handleDateFocus).toHaveBeenCalledWith(new Date(2024, 6, 1), { isEnabled: false });
608
+ });
609
+ });
610
+ });
@@ -1,4 +1,5 @@
1
1
  @import (reference) "./../styles/variables/neptune-tokens.less";
2
+ @import "./TableLink/TableLink.less";
2
3
 
3
4
  .tw-date-lookup-calendar {
4
5
  min-width: 300px;
@@ -21,47 +22,16 @@
21
22
  padding: 0;
22
23
  }
23
24
 
24
- &:has(.tw-date-lookup-day-option) {
25
+ &:has(.wds-Calendar__option--day) {
25
26
  padding: 1px;
26
27
  }
27
28
 
28
- button {
29
- width: 100%;
30
- padding: var(--size-4) 0;
31
- @media (--screen-400-zoom) {
32
- padding: 0;
33
- }
34
- color: var(--color-content-accent);
35
- border: transparent;
36
- border-radius: var(--radius-small);
37
- background-color: transparent;
38
- font-weight: var(--font-weight-semi-bold);
39
- color: var(--color-content-primary);
40
-
41
- &.tw-date-lookup-day-option {
42
- height: var(--size-40);
43
- border-radius: var(--radius-full);
44
- line-height: var(--size-40);
45
- width: var(--size-40);
46
- padding: 0;
47
- display: inline-flex;
48
- align-items: center;
49
- justify-content: center;
50
-
51
- &.today {
52
- text-decoration: underline;
53
- text-decoration-thickness: 2px;
54
- text-underline-offset: 4px;
55
- }
56
- }
57
- }
58
-
59
- &:not(.disabled, :disabled) button.show-focus,
60
- &:hover button:not(.disabled, :disabled) {
29
+ &:not(.disabled, :disabled) button.show-focus:not(.disabled, [aria-disabled="true"]),
30
+ &:hover button:not(.disabled, :disabled, [aria-disabled="true"]) {
61
31
  background-color: var(--color-background-screen-hover);
62
32
  }
63
33
 
64
- &:not(.disabled, :disabled) button.active {
34
+ &:not(.disabled, :disabled) button.active:not([aria-disabled="true"]) {
65
35
  background-color: var(--color-interactive-primary);
66
36
  color: var(--color-interactive-contrast);
67
37
  }
@@ -36,9 +36,30 @@ export default defineMessages({
36
36
  defaultMessage: 'selected',
37
37
  description: 'Description of the selected day/month/year',
38
38
  },
39
+ notAvailable: {
40
+ id: 'neptune.DateLookup.notAvailable',
41
+ defaultMessage: 'not available',
42
+ description: 'Announces that a date is disabled/unavailable',
43
+ },
39
44
  goTo20YearView: {
40
45
  id: 'neptune.DateLookup.goTo20YearView',
41
46
  defaultMessage: 'Go to 20 year view',
42
47
  description: 'Description of the button to go to 20 year view',
43
48
  },
49
+ outOfRangeBetween: {
50
+ id: 'neptune.DateLookup.outOfRangeBetween',
51
+ defaultMessage: 'Please select a date between {min} and {max}.',
52
+ description:
53
+ 'Inline prompt when a date outside min/max boundaries is focused and both min and max are set',
54
+ },
55
+ outOfRangeAfter: {
56
+ id: 'neptune.DateLookup.outOfRangeAfter',
57
+ defaultMessage: 'Please enter {min} or a later date.',
58
+ description: 'Inline prompt when a date before min is focused and only min is set',
59
+ },
60
+ outOfRangeBefore: {
61
+ id: 'neptune.DateLookup.outOfRangeBefore',
62
+ defaultMessage: 'Please enter {max} or an earlier date.',
63
+ description: 'Inline prompt when a date after max is focused and only max is set',
64
+ },
44
65
  });