@transferwise/components 0.0.0-experimental-fa3aa98 → 0.0.0-experimental-f4a328f

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 (201) hide show
  1. package/build/Calendar/Calendar.js +16 -94
  2. package/build/Calendar/Calendar.js.map +1 -1
  3. package/build/Calendar/Calendar.messages.js +0 -12
  4. package/build/Calendar/Calendar.messages.js.map +1 -1
  5. package/build/Calendar/Calendar.messages.mjs +0 -12
  6. package/build/Calendar/Calendar.messages.mjs.map +1 -1
  7. package/build/Calendar/Calendar.mjs +17 -95
  8. package/build/Calendar/Calendar.mjs.map +1 -1
  9. package/build/Calendar/DateHeader/DateHeader.js +1 -5
  10. package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
  11. package/build/Calendar/DateHeader/DateHeader.mjs +1 -5
  12. package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
  13. package/build/Calendar/DayCalendar/DayCalendar.js +0 -14
  14. package/build/Calendar/DayCalendar/DayCalendar.js.map +1 -1
  15. package/build/Calendar/DayCalendar/DayCalendar.mjs +0 -14
  16. package/build/Calendar/DayCalendar/DayCalendar.mjs.map +1 -1
  17. package/build/Calendar/DayCalendar/Table/DayCalendarTable.js +19 -53
  18. package/build/Calendar/DayCalendar/Table/DayCalendarTable.js.map +1 -1
  19. package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs +19 -53
  20. package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs.map +1 -1
  21. package/build/Calendar/MonthCalendar/MonthCalendar.js +0 -6
  22. package/build/Calendar/MonthCalendar/MonthCalendar.js.map +1 -1
  23. package/build/Calendar/MonthCalendar/MonthCalendar.mjs +0 -6
  24. package/build/Calendar/MonthCalendar/MonthCalendar.mjs.map +1 -1
  25. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js +3 -8
  26. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js.map +1 -1
  27. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs +3 -8
  28. package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs.map +1 -1
  29. package/build/Calendar/YearCalendar/Table/YearCalendarTable.js +3 -8
  30. package/build/Calendar/YearCalendar/Table/YearCalendarTable.js.map +1 -1
  31. package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs +3 -8
  32. package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs.map +1 -1
  33. package/build/Calendar/YearCalendar/YearCalendar.js +0 -7
  34. package/build/Calendar/YearCalendar/YearCalendar.js.map +1 -1
  35. package/build/Calendar/YearCalendar/YearCalendar.mjs +0 -7
  36. package/build/Calendar/YearCalendar/YearCalendar.mjs.map +1 -1
  37. package/build/Calendar/{TableLink → tableLink}/TableLink.js +11 -26
  38. package/build/Calendar/tableLink/TableLink.js.map +1 -0
  39. package/build/Calendar/{TableLink → tableLink}/TableLink.mjs +11 -26
  40. package/build/Calendar/tableLink/TableLink.mjs.map +1 -0
  41. package/build/DateLookup/DateLookup.js +9 -20
  42. package/build/DateLookup/DateLookup.js.map +1 -1
  43. package/build/DateLookup/DateLookup.mjs +9 -20
  44. package/build/DateLookup/DateLookup.mjs.map +1 -1
  45. package/build/Markup/Markup.js +137 -0
  46. package/build/Markup/Markup.js.map +1 -0
  47. package/build/Markup/Markup.mjs +132 -0
  48. package/build/Markup/Markup.mjs.map +1 -0
  49. package/build/Markup/constants.js +36 -0
  50. package/build/Markup/constants.js.map +1 -0
  51. package/build/Markup/constants.mjs +29 -0
  52. package/build/Markup/constants.mjs.map +1 -0
  53. package/build/Markup/utils/parseMarkup.js +236 -0
  54. package/build/Markup/utils/parseMarkup.js.map +1 -0
  55. package/build/Markup/utils/parseMarkup.mjs +231 -0
  56. package/build/Markup/utils/parseMarkup.mjs.map +1 -0
  57. package/build/Markup/utils/sanitise.js +114 -0
  58. package/build/Markup/utils/sanitise.js.map +1 -0
  59. package/build/Markup/utils/sanitise.mjs +111 -0
  60. package/build/Markup/utils/sanitise.mjs.map +1 -0
  61. package/build/i18n/en.json +0 -4
  62. package/build/i18n/en.json.js +0 -4
  63. package/build/i18n/en.json.js.map +1 -1
  64. package/build/i18n/en.json.mjs +0 -4
  65. package/build/i18n/en.json.mjs.map +1 -1
  66. package/build/index.js +4 -2
  67. package/build/index.js.map +1 -1
  68. package/build/index.mjs +2 -1
  69. package/build/index.mjs.map +1 -1
  70. package/build/main.css +78 -123
  71. package/build/styles/Calendar/Calendar.css +37 -112
  72. package/build/styles/Markup/Markup.css +28 -0
  73. package/build/styles/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
  74. package/build/styles/main.css +78 -123
  75. package/build/types/Body/Body.d.ts +2 -2
  76. package/build/types/Calendar/Calendar.d.ts +3 -16
  77. package/build/types/Calendar/Calendar.d.ts.map +1 -1
  78. package/build/types/Calendar/Calendar.messages.d.ts +0 -20
  79. package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
  80. package/build/types/Calendar/DateHeader/DateHeader.d.ts +0 -2
  81. package/build/types/Calendar/DateHeader/DateHeader.d.ts.map +1 -1
  82. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +1 -5
  83. package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
  84. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +0 -4
  85. package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
  86. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts +0 -1
  87. package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts.map +1 -1
  88. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts +1 -2
  89. package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts.map +1 -1
  90. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts +1 -2
  91. package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts.map +1 -1
  92. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts +0 -1
  93. package/build/types/Calendar/YearCalendar/YearCalendar.d.ts.map +1 -1
  94. package/build/types/Calendar/index.d.ts +0 -1
  95. package/build/types/Calendar/index.d.ts.map +1 -1
  96. package/build/types/Calendar/{TableLink → tableLink}/TableLink.d.ts +1 -3
  97. package/build/types/Calendar/tableLink/TableLink.d.ts.map +1 -0
  98. package/build/types/Calendar/tableLink/index.d.ts.map +1 -0
  99. package/build/types/DateLookup/DateLookup.d.ts +5 -24
  100. package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
  101. package/build/types/DateLookup/index.d.ts +0 -1
  102. package/build/types/DateLookup/index.d.ts.map +1 -1
  103. package/build/types/IconButton/IconButton.d.ts +1 -1
  104. package/build/types/Markup/Markup.d.ts +18 -0
  105. package/build/types/Markup/Markup.d.ts.map +1 -0
  106. package/build/types/Markup/Markup.types.d.ts +77 -0
  107. package/build/types/Markup/Markup.types.d.ts.map +1 -0
  108. package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts +12 -0
  109. package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts.map +1 -0
  110. package/build/types/Markup/constants.d.ts +20 -0
  111. package/build/types/Markup/constants.d.ts.map +1 -0
  112. package/build/types/Markup/index.d.ts +3 -0
  113. package/build/types/Markup/index.d.ts.map +1 -0
  114. package/build/types/Markup/utils/parseMarkup.d.ts +8 -0
  115. package/build/types/Markup/utils/parseMarkup.d.ts.map +1 -0
  116. package/build/types/Markup/utils/sanitise.d.ts +4 -0
  117. package/build/types/Markup/utils/sanitise.d.ts.map +1 -0
  118. package/build/types/Title/Title.d.ts +2 -2
  119. package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
  120. package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
  121. package/build/types/common/DateUtils/index.d.ts +0 -2
  122. package/build/types/common/DateUtils/index.d.ts.map +1 -1
  123. package/build/types/index.d.ts +4 -3
  124. package/build/types/index.d.ts.map +1 -1
  125. package/build/types/test-utils/index.d.ts +0 -8
  126. package/build/types/test-utils/index.d.ts.map +1 -1
  127. package/package.json +1 -1
  128. package/src/Calendar/Calendar.css +37 -112
  129. package/src/Calendar/Calendar.less +35 -5
  130. package/src/Calendar/Calendar.messages.ts +0 -21
  131. package/src/Calendar/Calendar.test.tsx +27 -288
  132. package/src/Calendar/Calendar.tsx +19 -136
  133. package/src/Calendar/DateHeader/DateHeader.tsx +0 -6
  134. package/src/Calendar/DayCalendar/DayCalendar.tsx +1 -33
  135. package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +28 -62
  136. package/src/Calendar/MonthCalendar/MonthCalendar.tsx +0 -8
  137. package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +3 -15
  138. package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +3 -15
  139. package/src/Calendar/YearCalendar/YearCalendar.tsx +0 -9
  140. package/src/Calendar/_stories/Calendar.story.tsx +4 -138
  141. package/src/Calendar/index.ts +0 -7
  142. package/src/Calendar/{TableLink → tableLink}/TableLink.tsx +14 -29
  143. package/src/DateLookup/DateLookup.test.tsx +37 -34
  144. package/src/DateLookup/DateLookup.tsx +13 -37
  145. package/src/DateLookup/_stories/DateLookup.story.tsx +2 -124
  146. package/src/DateLookup/index.ts +0 -6
  147. package/src/Markup/Markup.css +28 -0
  148. package/src/Markup/Markup.injection.test.tsx +163 -0
  149. package/src/Markup/Markup.less +29 -0
  150. package/src/Markup/Markup.test.tsx +463 -0
  151. package/src/Markup/Markup.tsx +169 -0
  152. package/src/Markup/Markup.types.ts +62 -0
  153. package/src/Markup/_stories/Markup.docs.mdx +75 -0
  154. package/src/Markup/_stories/Markup.security.docs.mdx +343 -0
  155. package/src/Markup/_stories/Markup.story.tsx +266 -0
  156. package/src/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
  157. package/src/Markup/_stories/Vulnerability/Vulnerability.tsx +30 -0
  158. package/src/Markup/constants.ts +41 -0
  159. package/src/Markup/index.ts +2 -0
  160. package/src/Markup/utils/parseMarkup.test.ts +706 -0
  161. package/src/Markup/utils/parseMarkup.ts +234 -0
  162. package/src/Markup/utils/sanitise.test.ts +497 -0
  163. package/src/Markup/utils/sanitise.ts +133 -0
  164. package/src/common/DateUtils/index.ts +0 -2
  165. package/src/i18n/en.json +0 -4
  166. package/src/index.ts +4 -3
  167. package/src/main.css +78 -123
  168. package/src/main.less +1 -0
  169. package/build/Calendar/TableLink/TableLink.js.map +0 -1
  170. package/build/Calendar/TableLink/TableLink.mjs.map +0 -1
  171. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js +0 -33
  172. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js.map +0 -1
  173. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs +0 -31
  174. package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs.map +0 -1
  175. package/build/common/DateUtils/isDateDisabled/isDateDisabled.js +0 -40
  176. package/build/common/DateUtils/isDateDisabled/isDateDisabled.js.map +0 -1
  177. package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs +0 -38
  178. package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs.map +0 -1
  179. package/build/styles/Calendar/TableLink/TableLink.css +0 -115
  180. package/build/types/Calendar/Calendar.types.d.ts +0 -32
  181. package/build/types/Calendar/Calendar.types.d.ts.map +0 -1
  182. package/build/types/Calendar/TableLink/TableLink.d.ts.map +0 -1
  183. package/build/types/Calendar/TableLink/index.d.ts.map +0 -1
  184. package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +0 -3
  185. package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +0 -1
  186. package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts +0 -3
  187. package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts.map +0 -1
  188. package/src/Calendar/Calendar.disabledDates.test.tsx +0 -518
  189. package/src/Calendar/Calendar.types.ts +0 -35
  190. package/src/Calendar/TableLink/TableLink.css +0 -115
  191. package/src/Calendar/TableLink/TableLink.less +0 -68
  192. package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +0 -118
  193. package/src/Calendar/_stories/Calendar.test.story.tsx +0 -74
  194. package/src/DateLookup/DateLookup.disabledDates.test.tsx +0 -293
  195. package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +0 -108
  196. package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +0 -98
  197. package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +0 -45
  198. package/src/common/DateUtils/isDateDisabled/isDateDisabled.test.ts +0 -169
  199. package/src/common/DateUtils/isDateDisabled/isDateDisabled.ts +0 -59
  200. /package/build/types/Calendar/{TableLink → tableLink}/index.d.ts +0 -0
  201. /package/src/Calendar/{TableLink → tableLink}/index.ts +0 -0
@@ -1,6 +1,5 @@
1
1
  import { mockMatchMedia, render, screen, userEvent } from '../test-utils';
2
2
  import Calendar from './Calendar';
3
- import type { DisabledRules } from './Calendar.types';
4
3
 
5
4
  mockMatchMedia();
6
5
 
@@ -16,159 +15,35 @@ describe('Calendar', () => {
16
15
  ['{ArrowUp}', -7],
17
16
  ['{ArrowDown}', +7],
18
17
  ])("handles '%s' to step %d day(s)", async (text: string, step: number) => {
19
- const handleDateFocus = jest.fn();
20
- render(<Calendar value={initialValue} onDateFocus={handleDateFocus} />);
18
+ const handleChange = jest.fn();
19
+ render(<Calendar value={initialValue} onChange={handleChange} />);
21
20
 
22
21
  await user.click(screen.getByRole('button', { name: /selected day/iu }));
23
22
  await user.keyboard(text);
24
23
 
25
24
  const expected = new Date(initialValue);
26
25
  expected.setDate(initialValue.getDate() + step);
27
- expect(handleDateFocus).toHaveBeenCalledWith(expected, { isEnabled: true });
26
+ expect(handleChange).toHaveBeenCalledWith(expected);
28
27
  });
29
28
 
30
29
  it('selects a day on click', async () => {
31
- const handleDateFocus = jest.fn();
32
- render(<Calendar value={initialValue} onDateFocus={handleDateFocus} />);
33
-
34
- await user.click(screen.getByText('15'));
35
-
36
- expect(handleDateFocus).toHaveBeenCalledWith(new Date(2000, 0, 15), { isEnabled: true });
37
- });
38
-
39
- it('fires both onDateFocus and onChange when clicking a day', async () => {
40
- const handleDateFocus = jest.fn();
41
30
  const handleChange = jest.fn();
42
- render(
43
- <Calendar value={initialValue} onDateFocus={handleDateFocus} onChange={handleChange} />,
44
- );
31
+ render(<Calendar value={initialValue} onChange={handleChange} />);
45
32
 
46
33
  await user.click(screen.getByText('15'));
47
34
 
48
- expect(handleDateFocus).toHaveBeenCalledWith(new Date(2000, 0, 15), { isEnabled: true });
49
35
  expect(handleChange).toHaveBeenCalledWith(new Date(2000, 0, 15));
50
36
  });
51
37
 
52
- it('does not fire onChange during arrow key navigation', async () => {
38
+ it('calls onSelect instead of onChange when clicking a day', async () => {
53
39
  const handleChange = jest.fn();
54
- render(<Calendar value={initialValue} onChange={handleChange} />);
55
-
56
- await user.click(screen.getByRole('button', { name: /selected day/iu }));
57
- expect(handleChange).toHaveBeenCalledTimes(1);
58
-
59
- await user.keyboard('{ArrowRight}');
60
-
61
- expect(handleChange).toHaveBeenCalledTimes(1);
62
- });
63
-
64
- it('fires onChange when Enter is pressed on focused date', async () => {
65
- const handleChange = jest.fn();
66
- render(<Calendar value={initialValue} onChange={handleChange} />);
67
-
68
- await user.click(screen.getByRole('button', { name: /selected day/iu }));
69
- await user.keyboard('{ArrowRight}');
70
- await user.keyboard('{Enter}');
71
-
72
- const expected = new Date(initialValue);
73
- expected.setDate(initialValue.getDate() + 1);
74
- expect(handleChange).toHaveBeenCalledWith(expected);
75
- });
76
-
77
- it('does not fire onChange when Enter is pressed on a disabled focused date', async () => {
78
- const handleChange = jest.fn();
79
- const disabledRules: DisabledRules = {
80
- weekdays: ['sat', 'sun'],
81
- reason: 'Closed on weekends.',
82
- };
83
- render(
84
- <Calendar
85
- value={new Date(2000, 0, 7)}
86
- disabledRules={disabledRules}
87
- onChange={handleChange}
88
- />,
89
- );
90
-
91
- await user.click(screen.getByRole('button', { name: /selected day/iu }));
92
- // Click on selected day (Friday) fires onChange once
93
- expect(handleChange).toHaveBeenCalledTimes(1);
94
-
95
- // ArrowRight from Jan 7 (Friday) → Jan 8 (Saturday = disabled)
96
- await user.keyboard('{ArrowRight}');
97
- await user.keyboard('{Enter}');
98
-
99
- expect(handleChange).toHaveBeenCalledTimes(1);
100
- });
101
-
102
- it('fires onDateFocus with isEnabled: false when Enter is pressed on a disabled focused date', async () => {
103
- const handleDateFocus = jest.fn();
104
- const disabledRules: DisabledRules = {
105
- weekdays: ['sat', 'sun'],
106
- reason: 'Closed on weekends.',
107
- };
108
- render(
109
- <Calendar
110
- value={new Date(2000, 0, 7)}
111
- disabledRules={disabledRules}
112
- onDateFocus={handleDateFocus}
113
- />,
114
- );
115
-
116
- await user.click(screen.getByRole('button', { name: /selected day/iu }));
117
- handleDateFocus.mockClear();
118
-
119
- // ArrowRight from Jan 7 (Friday) → Jan 8 (Saturday = disabled)
120
- await user.keyboard('{ArrowRight}');
121
- handleDateFocus.mockClear();
122
-
123
- await user.keyboard('{Enter}');
124
-
125
- expect(handleDateFocus).toHaveBeenCalledWith(new Date(2000, 0, 8), { isEnabled: false });
126
- });
127
-
128
- it('fires onChange when Space is pressed on a focused enabled date', async () => {
129
- const handleChange = jest.fn();
130
- render(<Calendar value={initialValue} onChange={handleChange} />);
131
-
132
- await user.click(screen.getByRole('button', { name: /selected day/iu }));
133
- await user.keyboard('{ArrowRight}');
134
- await user.keyboard(' ');
135
-
136
- const expected = new Date(initialValue);
137
- expected.setDate(initialValue.getDate() + 1);
138
- expect(handleChange).toHaveBeenCalledWith(expected);
139
- });
140
-
141
- it('does not fire onChange when Space is pressed on a disabled focused date', async () => {
142
- const handleChange = jest.fn();
143
- const disabledRules: DisabledRules = {
144
- weekdays: ['sat', 'sun'],
145
- reason: 'Closed on weekends.',
146
- };
147
- render(
148
- <Calendar
149
- value={new Date(2000, 0, 7)}
150
- disabledRules={disabledRules}
151
- onChange={handleChange}
152
- />,
153
- );
154
-
155
- await user.click(screen.getByRole('button', { name: /selected day/iu }));
156
- expect(handleChange).toHaveBeenCalledTimes(1);
157
-
158
- // ArrowRight from Jan 7 (Friday) → Jan 8 (Saturday = disabled)
159
- await user.keyboard('{ArrowRight}');
160
- await user.keyboard(' ');
161
-
162
- expect(handleChange).toHaveBeenCalledTimes(1);
163
- });
164
-
165
- it('renders and remains interactive without onDateFocus', async () => {
166
- const handleChange = jest.fn();
167
- render(<Calendar value={initialValue} onChange={handleChange} />);
40
+ const handleSelect = jest.fn();
41
+ render(<Calendar value={initialValue} onChange={handleChange} onSelect={handleSelect} />);
168
42
 
169
43
  await user.click(screen.getByText('15'));
170
44
 
171
- expect(handleChange).toHaveBeenCalledWith(new Date(2000, 0, 15));
45
+ expect(handleSelect).toHaveBeenCalledWith(new Date(2000, 0, 15));
46
+ expect(handleChange).not.toHaveBeenCalled();
172
47
  });
173
48
  });
174
49
 
@@ -179,15 +54,15 @@ describe('Calendar', () => {
179
54
  ['{ArrowUp}', -4],
180
55
  ['{ArrowDown}', +4],
181
56
  ])("handles '%s' to step %d year(s)", async (text: string, step: number) => {
182
- const handleDateFocus = jest.fn();
183
- render(<Calendar value={initialValue} onDateFocus={handleDateFocus} />);
57
+ const handleChange = jest.fn();
58
+ render(<Calendar value={initialValue} onChange={handleChange} />);
184
59
 
185
60
  await user.click(screen.getByRole('button', { name: /year view/iu }));
186
61
  await user.keyboard(text);
187
62
 
188
63
  const expected = new Date(initialValue);
189
64
  expected.setFullYear(initialValue.getFullYear() + step);
190
- expect(handleDateFocus).toHaveBeenCalledWith(expected, { isEnabled: true });
65
+ expect(handleChange).toHaveBeenCalledWith(expected);
191
66
  });
192
67
  });
193
68
 
@@ -198,8 +73,8 @@ describe('Calendar', () => {
198
73
  ['{ArrowUp}', -4],
199
74
  ['{ArrowDown}', +4],
200
75
  ])("handles '%s' to step %d month(s)", async (text: string, step: number) => {
201
- const handleDateFocus = jest.fn();
202
- render(<Calendar value={initialValue} onDateFocus={handleDateFocus} />);
76
+ const handleChange = jest.fn();
77
+ render(<Calendar value={initialValue} onChange={handleChange} />);
203
78
 
204
79
  await user.click(screen.getByRole('button', { name: /year view/iu }));
205
80
  await user.keyboard(' ');
@@ -207,166 +82,48 @@ describe('Calendar', () => {
207
82
 
208
83
  const expected = new Date(initialValue);
209
84
  expected.setMonth(initialValue.getMonth() + step);
210
- expect(handleDateFocus).toHaveBeenCalledWith(expected, { isEnabled: true });
85
+ expect(handleChange).toHaveBeenCalledWith(expected);
211
86
  });
212
87
  });
213
88
 
214
89
  describe('min/max clamping', () => {
215
- it('limits keyboard navigation to min value', async () => {
90
+ it('limits navigation to min value', async () => {
216
91
  const min = new Date(initialValue);
217
92
  min.setDate(min.getDate() - 1);
218
- const handleDateFocus = jest.fn();
219
- render(<Calendar value={initialValue} min={min} onDateFocus={handleDateFocus} />);
93
+ const handleChange = jest.fn();
94
+ render(<Calendar value={initialValue} min={min} onChange={handleChange} />);
220
95
 
221
96
  await user.click(screen.getByRole('button', { name: /selected day/iu }));
222
97
  await user.keyboard('{ArrowLeft}{ArrowLeft}');
223
98
 
224
- expect(handleDateFocus).toHaveBeenLastCalledWith(min, { isEnabled: true });
99
+ expect(handleChange).toHaveBeenLastCalledWith(min);
225
100
  });
226
101
 
227
- it('limits keyboard navigation to max value', async () => {
102
+ it('limits navigation to max value', async () => {
228
103
  const max = new Date(initialValue);
229
104
  max.setDate(max.getDate() + 1);
230
- const handleDateFocus = jest.fn();
231
- render(<Calendar value={initialValue} max={max} onDateFocus={handleDateFocus} />);
105
+ const handleChange = jest.fn();
106
+ render(<Calendar value={initialValue} max={max} onChange={handleChange} />);
232
107
 
233
108
  await user.click(screen.getByRole('button', { name: /selected day/iu }));
234
109
  await user.keyboard('{ArrowRight}{ArrowRight}');
235
110
 
236
- expect(handleDateFocus).toHaveBeenLastCalledWith(max, { isEnabled: true });
111
+ expect(handleChange).toHaveBeenLastCalledWith(max);
237
112
  });
238
113
 
239
114
  it('clamps value to range when value is outside min/max', () => {
240
115
  const min = new Date(2000, 0, 5);
241
116
  const max = new Date(2000, 0, 20);
242
- const handleDateFocus = jest.fn();
243
- render(<Calendar value={initialValue} min={min} max={max} onDateFocus={handleDateFocus} />);
244
-
245
- expect(handleDateFocus).toHaveBeenCalledWith(min, { isEnabled: true });
246
- });
247
- });
248
-
249
- describe('navigation button boundary disabling', () => {
250
- describe('day view — previous/next month', () => {
251
- it('disables previous month button when min is in the current view month', () => {
252
- render(<Calendar value={new Date(2000, 3, 15)} min={new Date(2000, 3, 1)} />);
253
- expect(screen.getByRole('button', { name: /previous month/iu })).toBeDisabled();
254
- });
255
-
256
- it('disables next month button when max is in the current view month', () => {
257
- render(<Calendar value={new Date(2000, 3, 15)} max={new Date(2000, 3, 30)} />);
258
- expect(screen.getByRole('button', { name: /next month/iu })).toBeDisabled();
259
- });
260
-
261
- it('does not disable month buttons when min/max are in adjacent months', () => {
262
- render(
263
- <Calendar
264
- value={new Date(2000, 3, 15)}
265
- min={new Date(2000, 2, 1)}
266
- max={new Date(2000, 4, 30)}
267
- />,
268
- );
269
- expect(screen.getByRole('button', { name: /previous month/iu })).toBeEnabled();
270
- expect(screen.getByRole('button', { name: /next month/iu })).toBeEnabled();
271
- });
272
-
273
- it('does not disable month buttons when no min/max is set', () => {
274
- render(<Calendar value={new Date(2000, 3, 15)} />);
275
- expect(screen.getByRole('button', { name: /previous month/iu })).toBeEnabled();
276
- expect(screen.getByRole('button', { name: /next month/iu })).toBeEnabled();
277
- });
278
-
279
- it('disables previous month button when min is in January and view is January', () => {
280
- // Edge case: month wraps to previous year
281
- render(<Calendar value={new Date(2000, 0, 15)} min={new Date(2000, 0, 1)} />);
282
- expect(screen.getByRole('button', { name: /previous month/iu })).toBeDisabled();
283
- });
284
-
285
- it('disables next month button when max is in December and view is December', () => {
286
- // Edge case: month wraps to next year
287
- render(<Calendar value={new Date(2000, 11, 15)} max={new Date(2000, 11, 31)} />);
288
- expect(screen.getByRole('button', { name: /next month/iu })).toBeDisabled();
289
- });
290
- });
291
-
292
- describe('month view — previous/next year', () => {
293
- it('disables previous year button when min year equals view year', async () => {
294
- render(<Calendar value={new Date(2000, 3, 15)} min={new Date(2000, 0, 1)} />);
295
- await user.click(screen.getByRole('button', { name: /year view/iu }));
296
- await user.click(screen.getByRole('button', { name: /2000, selected year/iu }));
297
- expect(screen.getByRole('button', { name: /previous year/iu })).toBeDisabled();
298
- });
299
-
300
- it('disables next year button when max year equals view year', async () => {
301
- render(<Calendar value={new Date(2000, 3, 15)} max={new Date(2000, 11, 31)} />);
302
- await user.click(screen.getByRole('button', { name: /year view/iu }));
303
- await user.click(screen.getByRole('button', { name: /2000, selected year/iu }));
304
- expect(screen.getByRole('button', { name: /next year/iu })).toBeDisabled();
305
- });
306
-
307
- it('does not disable year buttons when min/max are in adjacent years', async () => {
308
- render(
309
- <Calendar
310
- value={new Date(2000, 3, 15)}
311
- min={new Date(1999, 0, 1)}
312
- max={new Date(2001, 11, 31)}
313
- />,
314
- );
315
- await user.click(screen.getByRole('button', { name: /year view/iu }));
316
- await user.click(screen.getByRole('button', { name: /2000, selected year/iu }));
317
- expect(screen.getByRole('button', { name: /previous year/iu })).toBeEnabled();
318
- expect(screen.getByRole('button', { name: /next year/iu })).toBeEnabled();
319
- });
320
-
321
- it('does not disable year buttons when no min/max is set', async () => {
322
- render(<Calendar value={new Date(2000, 3, 15)} />);
323
- await user.click(screen.getByRole('button', { name: /year view/iu }));
324
- await user.click(screen.getByRole('button', { name: /2000, selected year/iu }));
325
- expect(screen.getByRole('button', { name: /previous year/iu })).toBeEnabled();
326
- expect(screen.getByRole('button', { name: /next year/iu })).toBeEnabled();
327
- });
328
- });
329
-
330
- describe('year view — previous/next 20 years', () => {
331
- it('disables previous 20-year button when min falls in the current block', async () => {
332
- // value is 2000, block is 2000–2019, min is 2005 → previous block (1980–1999) is entirely before min
333
- render(<Calendar value={new Date(2005, 0, 1)} min={new Date(2005, 0, 1)} />);
334
- await user.click(screen.getByRole('button', { name: /year view/iu }));
335
- expect(screen.getByRole('button', { name: /previous 20 years/iu })).toBeDisabled();
336
- });
337
-
338
- it('disables next 20-year button when max falls in the current block', async () => {
339
- // value is 2000, block is 2000–2019, max is 2015 → next block (2020–2039) is entirely after max
340
- render(<Calendar value={new Date(2000, 0, 1)} max={new Date(2015, 11, 31)} />);
341
- await user.click(screen.getByRole('button', { name: /year view/iu }));
342
- expect(screen.getByRole('button', { name: /next 20 years/iu })).toBeDisabled();
343
- });
344
-
345
- it('does not disable 20-year buttons when min/max span multiple blocks', async () => {
346
- render(
347
- <Calendar
348
- value={new Date(2000, 0, 1)}
349
- min={new Date(1985, 0, 1)}
350
- max={new Date(2025, 11, 31)}
351
- />,
352
- );
353
- await user.click(screen.getByRole('button', { name: /year view/iu }));
354
- expect(screen.getByRole('button', { name: /previous 20 years/iu })).toBeEnabled();
355
- expect(screen.getByRole('button', { name: /next 20 years/iu })).toBeEnabled();
356
- });
117
+ const handleChange = jest.fn();
118
+ render(<Calendar value={initialValue} min={min} max={max} onChange={handleChange} />);
357
119
 
358
- it('does not disable 20-year buttons when no min/max is set', async () => {
359
- render(<Calendar value={new Date(2000, 0, 1)} />);
360
- await user.click(screen.getByRole('button', { name: /year view/iu }));
361
- expect(screen.getByRole('button', { name: /previous 20 years/iu })).toBeEnabled();
362
- expect(screen.getByRole('button', { name: /next 20 years/iu })).toBeEnabled();
363
- });
120
+ expect(handleChange).toHaveBeenCalledWith(min);
364
121
  });
365
122
  });
366
123
 
367
124
  describe('view switching', () => {
368
125
  it('switches to year view and back to day view', async () => {
369
- render(<Calendar value={initialValue} />);
126
+ render(<Calendar value={initialValue} onChange={jest.fn()} />);
370
127
 
371
128
  await user.click(screen.getByRole('button', { name: /year view/iu }));
372
129
  expect(screen.getByRole('button', { name: /selected year/iu })).toBeInTheDocument();
@@ -377,23 +134,5 @@ describe('Calendar', () => {
377
134
  await user.click(screen.getByRole('button', { name: /selected month/iu }));
378
135
  expect(screen.getByRole('button', { name: /next month/iu })).toBeInTheDocument();
379
136
  });
380
-
381
- it('switches to year view when header button is activated with keyboard', async () => {
382
- const handleChange = jest.fn();
383
- render(<Calendar value={initialValue} onChange={handleChange} />);
384
-
385
- // Tab through to the year view header button
386
- const yearViewButton = screen.getByRole('button', { name: /year view/iu });
387
- await user.tab();
388
- while (document.activeElement !== yearViewButton) {
389
- await user.tab();
390
- }
391
- expect(yearViewButton).toHaveFocus();
392
-
393
- await user.keyboard('{Enter}');
394
-
395
- expect(screen.getByRole('button', { name: /selected year/iu })).toBeInTheDocument();
396
- expect(handleChange).not.toHaveBeenCalled();
397
- });
398
137
  });
399
138
  });
@@ -1,88 +1,40 @@
1
- import { useState, useCallback, useEffect, useId, useRef, KeyboardEvent } from 'react';
2
- import { useIntl } from 'react-intl';
3
- import { formatDate } from '@transferwise/formatting';
1
+ import { useState, useCallback, useEffect, KeyboardEvent } from 'react';
4
2
 
5
3
  import { MonthFormat } from '../common';
6
- import {
7
- isWithinRange,
8
- moveToWithinRange,
9
- returnDateView,
10
- isDateDisabled,
11
- hasDisabledDatesInMonth,
12
- } from '../common/DateUtils';
13
- import { InlinePrompt } from '../Prompt/InlinePrompt';
14
- import type { DateChangeMeta, DisabledRules } from './Calendar.types';
4
+ import { isWithinRange, moveToWithinRange, returnDateView } from '../common/DateUtils';
15
5
  import DayCalendar from './DayCalendar';
16
6
  import { getStartOfDay } from './GetStartOfDay';
17
- import messages from './Calendar.messages';
18
7
  import MonthCalendar from './MonthCalendar';
19
8
  import YearCalendar from './YearCalendar';
20
9
 
21
10
  export interface CalendarProps {
22
- /** The currently selected date, or `null` for no selection. */
23
11
  value: Date | null;
24
- /** Earliest selectable date (inclusive). Dates before this are interactive but not selectable, and show a boundary prompt. */
25
12
  min?: Date | null;
26
- /** Latest selectable date (inclusive). Dates after this are interactive but not selectable, and show a boundary prompt. */
27
13
  max?: Date | null;
28
- /** Month name format in the header. Defaults to `'long'` (e.g. "January"). */
29
14
  monthFormat?: `${MonthFormat}`;
30
- /** Placeholder text shown in the month/year cells when no date is selected. */
31
15
  placeholder?: string;
32
- /** Rules for disabling specific weekdays, individual dates, or date ranges. The required `reason` is shown in an inline prompt and linked to disabled cells via `aria-describedby` for screen reader accessibility. */
33
- disabledRules?: DisabledRules;
34
- /** Called whenever the focused date changes — including disabled and out-of-range dates. `meta.isEnabled` is `false` when the date is not selectable. */
35
- onDateFocus?: (date: Date | null, meta: DateChangeMeta) => void;
36
- /** Called when a date is confirmed (click or Enter). Only fires for enabled dates. */
37
- onChange?: (date: Date | null) => void;
16
+ onChange: (date: Date | null) => void;
17
+ onSelect?: (date: Date) => void;
38
18
  }
39
19
 
40
- /**
41
- * Inline calendar for selecting a single date. Powers [DateLookup](?path=/docs/forms-datelookup--docs).
42
- */
43
20
  const Calendar = ({
44
21
  value,
45
22
  min: minProp = null,
46
23
  max: maxProp = null,
47
24
  monthFormat = MonthFormat.LONG,
48
25
  placeholder,
49
- disabledRules,
50
- onDateFocus,
51
26
  onChange,
27
+ onSelect,
52
28
  }: CalendarProps) => {
53
- const intl = useIntl();
54
29
  const selectedDate = getStartOfDay(value);
55
30
  const min = getStartOfDay(minProp);
56
31
  const max = getStartOfDay(maxProp);
57
- const disabledReasonId = useId();
58
- const disabledRulesRef = useRef(disabledRules);
59
- useEffect(() => {
60
- disabledRulesRef.current = disabledRules;
61
- });
62
-
63
- const [focusedDate, setFocusedDate] = useState<Date | null>(selectedDate);
64
-
65
- useEffect(() => {
66
- // eslint-disable-next-line react-hooks/set-state-in-effect
67
- setFocusedDate(selectedDate);
68
- }, [selectedDate?.getTime()]); // eslint-disable-line react-hooks/exhaustive-deps
69
-
70
- const checkIsEnabled = (date: Date | null) => {
71
- if (!date) return true;
72
- return isWithinRange(date, min, max) && !isDateDisabled(date, disabledRulesRef.current);
73
- };
74
-
75
- const focusedDateDisabled = focusedDate
76
- ? !isWithinRange(focusedDate, min, max) || isDateDisabled(focusedDate, disabledRules)
77
- : false;
78
32
 
79
33
  useEffect(() => {
80
34
  if (selectedDate && !isWithinRange(selectedDate, min, max)) {
81
- const clamped = moveToWithinRange(selectedDate, min, max);
82
- setFocusedDate(clamped); // eslint-disable-line react-hooks/set-state-in-effect
83
- onDateFocus?.(clamped, { isEnabled: checkIsEnabled(clamped) });
35
+ onChange(moveToWithinRange(selectedDate, min, max));
84
36
  }
85
- }, [selectedDate?.getTime(), min?.getTime(), max?.getTime(), onDateFocus]); // eslint-disable-line react-hooks/exhaustive-deps
37
+ }, [selectedDate?.getTime(), min?.getTime(), max?.getTime(), onChange]); // eslint-disable-line react-hooks/exhaustive-deps
86
38
 
87
39
  const dateView = returnDateView(selectedDate, min, max);
88
40
  const [viewMonth, setViewMonth] = useState(dateView.getMonth());
@@ -97,14 +49,6 @@ const Calendar = ({
97
49
  setViewYear(view.getFullYear());
98
50
  }, [selectedDate?.getTime(), min?.getTime(), max?.getTime()]); // eslint-disable-line react-hooks/exhaustive-deps
99
51
 
100
- useEffect(() => {
101
- if (focusedDate) {
102
- // eslint-disable-next-line react-hooks/set-state-in-effect
103
- setViewMonth(focusedDate.getMonth());
104
- setViewYear(focusedDate.getFullYear());
105
- }
106
- }, [focusedDate?.getTime()]); // eslint-disable-line react-hooks/exhaustive-deps
107
-
108
52
  const handleViewDateUpdate = useCallback(
109
53
  // eslint-disable-next-line react-hooks/preserve-manual-memoization
110
54
  ({ month, year }: { month?: number; year?: number }) => {
@@ -120,36 +64,33 @@ const Calendar = ({
120
64
 
121
65
  const handleSelectedDateUpdate = useCallback(
122
66
  (date: Date) => {
123
- const isEnabled = checkIsEnabled(date);
124
- setFocusedDate(date);
125
- onDateFocus?.(date, { isEnabled });
126
- if (isEnabled && onChange) {
67
+ if (onSelect) {
68
+ onSelect(date);
69
+ } else {
127
70
  onChange(date);
128
71
  }
129
72
  },
130
- [onDateFocus, onChange], // eslint-disable-line react-hooks/exhaustive-deps
73
+ [onChange, onSelect],
131
74
  );
132
75
 
133
76
  const adjustDate = useCallback(
134
77
  (daysToAdd: number, monthsToAdd: number, yearsToAdd: number) => {
135
- const base = focusedDate ?? selectedDate;
136
78
  let date: Date | null;
137
- if (base) {
79
+ if (selectedDate) {
138
80
  date = new Date(
139
- mode === 'year' ? base.getFullYear() + yearsToAdd : base.getFullYear(),
140
- mode === 'month' ? base.getMonth() + monthsToAdd : base.getMonth(),
141
- mode === 'day' ? base.getDate() + daysToAdd : base.getDate(),
81
+ mode === 'year' ? selectedDate.getFullYear() + yearsToAdd : selectedDate.getFullYear(),
82
+ mode === 'month' ? selectedDate.getMonth() + monthsToAdd : selectedDate.getMonth(),
83
+ mode === 'day' ? selectedDate.getDate() + daysToAdd : selectedDate.getDate(),
142
84
  );
143
85
  } else {
144
86
  date = getStartOfDay(new Date());
145
87
  }
146
88
  date &&= moveToWithinRange(date, min, max);
147
- if (date?.getTime() !== focusedDate?.getTime()) {
148
- setFocusedDate(date);
149
- onDateFocus?.(date, { isEnabled: checkIsEnabled(date) });
89
+ if (date?.getTime() !== selectedDate?.getTime()) {
90
+ onChange(date);
150
91
  }
151
92
  },
152
- [focusedDate, selectedDate, min, max, mode, onDateFocus], // eslint-disable-line react-hooks/exhaustive-deps
93
+ [selectedDate, min, max, mode, onChange],
153
94
  );
154
95
 
155
96
  const handleKeyDown = useCallback(
@@ -171,70 +112,24 @@ const Calendar = ({
171
112
  adjustDate(7, 4, 4);
172
113
  event.preventDefault();
173
114
  break;
174
- case 'Enter':
175
- case ' ':
176
- if (
177
- mode === 'day' &&
178
- focusedDate &&
179
- (event.target as HTMLElement).hasAttribute('data-calendar-cell')
180
- ) {
181
- handleSelectedDateUpdate(focusedDate);
182
- event.preventDefault();
183
- }
184
- break;
185
115
  default:
186
116
  break;
187
117
  }
188
118
  },
189
- [adjustDate, focusedDate, mode, handleSelectedDateUpdate],
119
+ [adjustDate],
190
120
  );
191
121
 
192
- const focusedDateOutOfRange = focusedDate ? !isWithinRange(focusedDate, min, max) : false;
193
-
194
- const formatDateForPrompt = (date: Date) =>
195
- formatDate(date, intl.locale, { day: 'numeric', month: 'long', year: 'numeric' });
196
-
197
- const outOfRangePromptText =
198
- min && max
199
- ? intl.formatMessage(messages.outOfRangeBetween, {
200
- min: formatDateForPrompt(min),
201
- max: formatDateForPrompt(max),
202
- })
203
- : min
204
- ? intl.formatMessage(messages.outOfRangeAfter, { min: formatDateForPrompt(min) })
205
- : max
206
- ? intl.formatMessage(messages.outOfRangeBefore, { max: formatDateForPrompt(max) })
207
- : '';
208
-
209
- const hasOutOfRangeDates = mode === 'day' && !!(min || max);
210
- const showRulesPrompt =
211
- mode === 'day' &&
212
- !!disabledRules &&
213
- hasDisabledDatesInMonth(viewMonth, viewYear, disabledRules, min, max);
214
- const showOutOfRangePrompt = mode === 'day' && focusedDateOutOfRange;
215
- const showPrompt = hasOutOfRangeDates || showRulesPrompt;
216
-
217
- const promptText = showOutOfRangePrompt
218
- ? outOfRangePromptText
219
- : (disabledRules?.reason ?? outOfRangePromptText);
220
- const promptSentiment = showOutOfRangePrompt || focusedDateDisabled ? 'negative' : 'neutral';
221
- const promptClassName =
222
- showOutOfRangePrompt || (showRulesPrompt && !showOutOfRangePrompt) ? 'm-t-1' : 'sr-only';
223
-
224
122
  return (
225
123
  // eslint-disable-next-line jsx-a11y/no-static-element-interactions
226
124
  <div onKeyDown={handleKeyDown}>
227
125
  {mode === 'day' && (
228
126
  <DayCalendar
229
127
  selectedDate={selectedDate}
230
- focusedDate={focusedDate}
231
128
  min={min}
232
129
  max={max}
233
130
  viewMonth={viewMonth}
234
131
  viewYear={viewYear}
235
132
  monthFormat={monthFormat}
236
- disabledRules={disabledRules}
237
- disabledReasonId={showPrompt ? disabledReasonId : undefined}
238
133
  onSelect={handleSelectedDateUpdate}
239
134
  onLabelClick={switchToYears}
240
135
  onViewDateUpdate={handleViewDateUpdate}
@@ -243,7 +138,6 @@ const Calendar = ({
243
138
  {mode === 'month' && (
244
139
  <MonthCalendar
245
140
  selectedDate={selectedDate}
246
- focusedDate={focusedDate}
247
141
  min={min}
248
142
  max={max}
249
143
  viewYear={viewYear}
@@ -256,7 +150,6 @@ const Calendar = ({
256
150
  {mode === 'year' && (
257
151
  <YearCalendar
258
152
  selectedDate={selectedDate}
259
- focusedDate={focusedDate}
260
153
  min={min}
261
154
  max={max}
262
155
  viewYear={viewYear}
@@ -265,16 +158,6 @@ const Calendar = ({
265
158
  onViewDateUpdate={handleViewDateUpdate}
266
159
  />
267
160
  )}
268
- {showPrompt && (
269
- <InlinePrompt
270
- sentiment={promptSentiment}
271
- width="full"
272
- id={disabledReasonId}
273
- className={promptClassName}
274
- >
275
- {promptText}
276
- </InlinePrompt>
277
- )}
278
161
  </div>
279
162
  );
280
163
  };