@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
@@ -3,7 +3,6 @@ import { useEffect, useRef } from 'react';
3
3
  import { useIntl } from 'react-intl';
4
4
 
5
5
  import messages from '../Calendar.messages';
6
- import SentimentSurface from '../../SentimentSurface';
7
6
 
8
7
  interface TableLinkProps {
9
8
  item: number;
@@ -11,11 +10,9 @@ interface TableLinkProps {
11
10
  title?: string;
12
11
  longTitle?: string;
13
12
  active: boolean;
14
- outOfRange: boolean;
15
13
  disabled: boolean;
16
14
  today: boolean;
17
15
  autofocus?: boolean;
18
- disabledReasonId?: string;
19
16
  onClick: (item: number) => void;
20
17
  }
21
18
 
@@ -25,11 +22,9 @@ const TableLink = ({
25
22
  title,
26
23
  longTitle,
27
24
  active,
28
- outOfRange,
29
25
  disabled,
30
26
  today,
31
27
  autofocus,
32
- disabledReasonId,
33
28
  onClick,
34
29
  }: TableLinkProps) => {
35
30
  const buttonRef = useRef<HTMLButtonElement>(null);
@@ -43,49 +38,39 @@ const TableLink = ({
43
38
  }
44
39
  }, [autofocus]);
45
40
 
46
- const onCalendarClick = (event: React.MouseEvent) => {
41
+ const onCalendarClick = (event: React.MouseEvent<HTMLButtonElement>) => {
47
42
  event.preventDefault();
48
- onClick(item);
43
+ if (!disabled) {
44
+ onClick(item);
45
+ }
49
46
  };
50
47
 
51
- const isUnavailable = outOfRange || disabled;
52
-
53
48
  const calculateAriaLabel = () => {
54
- let label = longTitle || title || '';
55
49
  if (active) {
56
- label += `, ${intl.formatMessage(messages.selected)} ${intl.formatMessage(messages[type])}`;
57
- }
58
- if (isUnavailable) {
59
- label += `, ${intl.formatMessage(messages.notAvailable)}`;
50
+ return `${longTitle || title || ''}, ${intl.formatMessage(
51
+ messages.selected,
52
+ )} ${intl.formatMessage(messages[type])}`;
60
53
  }
61
- return label;
54
+ return longTitle || title;
62
55
  };
63
56
 
64
57
  return (
65
- <SentimentSurface
66
- // @ts-expect-error - Generic forwardRef limitation in SentimentSurface
58
+ <button
67
59
  ref={buttonRef}
68
- as="button"
69
- data-calendar-cell
70
- aria-describedby={isUnavailable ? disabledReasonId : undefined}
71
- aria-disabled={isUnavailable || undefined}
72
- aria-label={calculateAriaLabel()}
73
- aria-pressed={active}
60
+ type="button"
74
61
  className={clsx(
75
- `wds-Calendar__option--${type}`,
76
62
  `tw-date-lookup-${type}-option np-text-body-default-bold`,
77
63
  { active: !!active },
78
64
  { today: !!today },
79
- { disabled: !!isUnavailable },
80
65
  )}
81
- hasBaseStyles={false}
82
- sentiment={isUnavailable ? 'negative' : 'neutral'}
66
+ disabled={disabled}
83
67
  tabIndex={autofocus ? 0 : -1}
84
- type="button"
68
+ aria-label={calculateAriaLabel()}
69
+ aria-pressed={active}
85
70
  onClick={onCalendarClick}
86
71
  >
87
72
  {title || item}
88
- </SentimentSurface>
73
+ </button>
89
74
  );
90
75
  };
91
76
 
@@ -110,13 +110,24 @@ describe('DateLookup', () => {
110
110
  ['{ArrowRight}', +1],
111
111
  ['{ArrowUp}', -7],
112
112
  ['{ArrowDown}', +7],
113
- ])("handles '%s' to step %d day(s) without changing value", async (text: string) => {
113
+ ])("handles '%s' to step %d day(s)", async (text: string, step: number) => {
114
114
  const handleChange = jest.fn();
115
115
  await setupAndOpenWithMouse({ onChange: handleChange });
116
116
 
117
+ expect(handleChange).not.toHaveBeenCalled();
118
+
117
119
  await user.keyboard(text);
118
120
 
119
- expect(handleChange).not.toHaveBeenCalled();
121
+ const value = new Date(initialValue);
122
+ value.setDate(initialValue.getDate() + step);
123
+ expect(handleChange).toHaveBeenCalledWith(value);
124
+
125
+ await user.keyboard('{Escape}');
126
+ await act(async () => {
127
+ await jest.runOnlyPendingTimersAsync();
128
+ });
129
+
130
+ expect(handleChange).toHaveBeenCalledWith(initialValue);
120
131
  });
121
132
  });
122
133
 
@@ -126,7 +137,7 @@ describe('DateLookup', () => {
126
137
  ['{ArrowRight}', +1],
127
138
  ['{ArrowUp}', -4],
128
139
  ['{ArrowDown}', +4],
129
- ])("handles '%s' to step %d year(s) without changing value", async (text: string) => {
140
+ ])("handles '%s' to step %d year(s)", async (text: string, step: number) => {
130
141
  const handleChange = jest.fn();
131
142
  await setupAndOpenWithMouse({ onChange: handleChange });
132
143
 
@@ -135,9 +146,20 @@ describe('DateLookup', () => {
135
146
  await jest.runOnlyPendingTimersAsync();
136
147
  });
137
148
 
149
+ expect(handleChange).not.toHaveBeenCalled();
150
+
138
151
  await user.keyboard(text);
139
152
 
140
- expect(handleChange).not.toHaveBeenCalled();
153
+ const value = new Date(initialValue);
154
+ value.setFullYear(initialValue.getFullYear() + step);
155
+ expect(handleChange).toHaveBeenCalledWith(value);
156
+
157
+ await user.keyboard('{Escape}');
158
+ await act(async () => {
159
+ await jest.runOnlyPendingTimersAsync();
160
+ });
161
+
162
+ expect(handleChange).toHaveBeenCalledWith(initialValue);
141
163
  });
142
164
  });
143
165
 
@@ -147,7 +169,7 @@ describe('DateLookup', () => {
147
169
  ['{ArrowRight}', +1],
148
170
  ['{ArrowUp}', -4],
149
171
  ['{ArrowDown}', +4],
150
- ])("handles '%s' to step %d month(s) without changing value", async (text: string) => {
172
+ ])("handles '%s' to step %d month(s)", async (text: string, step: number) => {
151
173
  const handleChange = jest.fn();
152
174
  await setupAndOpenWithMouse({ onChange: handleChange });
153
175
 
@@ -160,39 +182,20 @@ describe('DateLookup', () => {
160
182
  await jest.runOnlyPendingTimersAsync();
161
183
  });
162
184
 
163
- await user.keyboard(text);
164
-
165
- expect(handleChange).not.toHaveBeenCalled();
166
- });
167
- });
168
-
169
- describe('focus vs selection separation', () => {
170
- it('does not call onChange during arrow key navigation', async () => {
171
- const handleChange = jest.fn();
172
- render(<DateLookup value={new Date(2020, 0, 15)} onChange={handleChange} />);
173
-
174
- // Open the panel
175
- await user.click(screen.getByRole('button'));
176
185
  expect(handleChange).not.toHaveBeenCalled();
177
186
 
178
- await user.keyboard('{ArrowRight}');
179
-
180
- expect(handleChange).not.toHaveBeenCalled();
181
- });
187
+ await user.keyboard(text);
182
188
 
183
- it('calls onChange when Enter is pressed on a focused date', async () => {
184
- const handleChange = jest.fn();
185
- render(<DateLookup value={new Date(2020, 0, 15)} onChange={handleChange} />);
189
+ const value = new Date(initialValue);
190
+ value.setMonth(initialValue.getMonth() + step);
191
+ expect(handleChange).toHaveBeenCalledWith(value);
186
192
 
187
- // Open the panel
188
- await user.click(screen.getByRole('button'));
189
- expect(handleChange).not.toHaveBeenCalled();
190
-
191
- // Arrow to next date then confirm with Enter
192
- await user.keyboard('{ArrowRight}');
193
- await user.keyboard('{Enter}');
193
+ await user.keyboard('{Escape}');
194
+ await act(async () => {
195
+ await jest.runOnlyPendingTimersAsync();
196
+ });
194
197
 
195
- expect(handleChange).toHaveBeenCalledWith(new Date(2020, 0, 16));
198
+ expect(handleChange).toHaveBeenCalledWith(initialValue);
196
199
  });
197
200
  });
198
201
  });
@@ -226,7 +229,7 @@ describe('DateLookup (events)', () => {
226
229
  const today = new Date();
227
230
  const todayDateLabel = today.toLocaleDateString('en-GB', {
228
231
  day: 'numeric',
229
- month: 'long',
232
+ month: 'numeric',
230
233
  year: 'numeric',
231
234
  });
232
235
  const expectedHeaderText = today.toLocaleDateString('en-GB', {
@@ -1,7 +1,6 @@
1
1
  import { clsx } from 'clsx';
2
2
  import { createRef, PureComponent, KeyboardEvent } from 'react';
3
3
  import Calendar from '../Calendar';
4
- import type { DateChangeMeta, DisabledRules, OnDateSelectParams } from '../Calendar/Calendar.types';
5
4
  import {
6
5
  Size,
7
6
  MonthFormat,
@@ -16,37 +15,19 @@ import { OverlayIdProvider } from '../Provider/overlay/OverlayIdProvider';
16
15
  import DateTrigger from './DateTrigger';
17
16
 
18
17
  export interface DateLookupProps {
19
- /** The currently selected date, or `null` for no selection. */
20
- value: Date | null;
21
- /** Called when an enabled date is confirmed. Receives `null` when the value is cleared. */
22
- onChange: (date: Date | null) => void;
23
- /** HTML `id` applied to the trigger button, used for label association. */
24
18
  id?: string;
25
- /** Earliest selectable date (inclusive). Dates before this are interactive but not selectable, and show a boundary prompt. */
19
+ value: Date | null;
26
20
  min?: Date | null;
27
- /** Latest selectable date (inclusive). Dates after this are interactive but not selectable, and show a boundary prompt. */
28
21
  max?: Date | null;
29
- /** Size of the trigger input. Defaults to `'md'`. */
30
22
  size?: SizeSmall | SizeMedium | SizeLarge;
31
- /** Label text rendered above the trigger input. */
23
+ placeholder?: string;
32
24
  label?: string;
33
- /** ID of an external element that labels the trigger, for use inside custom label contexts. */
34
25
  'aria-labelledby'?: string;
35
- /** Placeholder text shown in the trigger when no date is selected. */
36
- placeholder?: string;
37
- /** Month name format in the calendar header. Defaults to `'long'` (e.g. "January"). */
38
26
  monthFormat?: `${MonthFormat}`;
39
- /** When `true`, the trigger and calendar are non-interactive. */
40
27
  disabled?: boolean;
41
- /** When `true`, a clear button is shown in the trigger to reset the value to `null`. */
42
28
  clearable?: boolean;
43
- /** 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. */
44
- disabledRules?: DisabledRules;
45
- /** Called whenever any date is focused or clicked, including disabled and out-of-range dates. `isEnabled` is `false` when the date is not selectable. */
46
- onDateSelect?: (params: OnDateSelectParams) => void;
47
- /** Called when the calendar panel opens. */
29
+ onChange: (date: Date | null) => void;
48
30
  onFocus?: () => void;
49
- /** Called when the calendar panel closes. */
50
31
  onBlur?: () => void;
51
32
  }
52
33
 
@@ -147,15 +128,7 @@ class DateLookup extends PureComponent<DateLookupPropsWithInputAttributes, DateL
147
128
  }
148
129
  };
149
130
 
150
- handleDateChange = (date: Date | null, meta: DateChangeMeta) => {
151
- const { onDateSelect } = this.props;
152
- if (date && onDateSelect) {
153
- onDateSelect({ date, isEnabled: meta.isEnabled });
154
- }
155
- };
156
-
157
- handleDateSelect = (date: Date | null) => {
158
- if (!date) return;
131
+ handleDateChange = (date: Date | null) => {
159
132
  this.setState(
160
133
  (state) => {
161
134
  const originalDate = state.originalDate ?? this.props.value;
@@ -163,14 +136,18 @@ class DateLookup extends PureComponent<DateLookupPropsWithInputAttributes, DateL
163
136
  },
164
137
  () => {
165
138
  this.props.onChange(date);
166
- this.close();
167
- this.focusOn('.btn');
168
139
  },
169
140
  );
170
141
  };
171
142
 
143
+ handleDateSelect = (date: Date) => {
144
+ this.props.onChange(date);
145
+ this.close();
146
+ this.focusOn('.btn');
147
+ };
148
+
172
149
  getCalendar = () => {
173
- const { value, min, max, monthFormat, placeholder, disabledRules } = this.props;
150
+ const { value, min, max, monthFormat, placeholder } = this.props;
174
151
  return (
175
152
  <div className="p-a-1">
176
153
  <Calendar
@@ -179,9 +156,8 @@ class DateLookup extends PureComponent<DateLookupPropsWithInputAttributes, DateL
179
156
  max={max}
180
157
  monthFormat={monthFormat}
181
158
  placeholder={placeholder}
182
- disabledRules={disabledRules}
183
- onDateFocus={this.handleDateChange}
184
- onChange={this.handleDateSelect}
159
+ onChange={this.handleDateChange}
160
+ onSelect={this.handleDateSelect}
185
161
  />
186
162
  </div>
187
163
  );
@@ -1,23 +1,16 @@
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';
5
3
  import {
6
4
  createSandboxStory,
7
5
  globalScope,
8
6
  } from '../../../.storybook/components/sandbox/SandboxEditor';
9
- import { storySourceWithoutNoise } from '../../../.storybook/helpers';
10
7
  import { Size } from '../../common';
11
- import Field from '../../Field';
12
8
  import DateLookup, { type DateLookupProps } from '..';
9
+ import Field from '../../Field';
13
10
 
14
11
  export default {
15
12
  component: DateLookup,
16
13
  title: 'Forms/DateLookup',
17
- args: {
18
- onFocus: fn(),
19
- onBlur: fn(),
20
- },
21
14
  argTypes: {
22
15
  size: { control: 'select', options: [Size.SMALL, Size.MEDIUM, Size.LARGE] },
23
16
  disabled: { control: 'boolean' },
@@ -41,7 +34,7 @@ thePast.setUTCDate(epoch.getUTCDate() - 10);
41
34
 
42
35
  export const Sandbox = createSandboxStory({
43
36
  code: `const App = () => {
44
- const [value, setValue] = React.useState(new Date(2024, 5, 14));
37
+ const [value, setValue] = React.useState(new Date('2024-06-15'));
45
38
 
46
39
  return (
47
40
  <DateLookup
@@ -51,11 +44,6 @@ export const Sandbox = createSandboxStory({
51
44
  placeholder="Choose date"
52
45
  clearable
53
46
  value={value}
54
- disabledRules={{
55
- weekdays: ['sat', 'sun'],
56
- dates: [new Date(2024, 5, 19)],
57
- reason: 'Weekends and holidays are unavailable.',
58
- }}
59
47
  onChange={(date) => setValue(date)}
60
48
  />
61
49
  );
@@ -132,113 +120,3 @@ export const RightAligned: Story = {
132
120
  );
133
121
  },
134
122
  };
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({
150
- args: {
151
- size: Size.MEDIUM,
152
- label: 'Select a date',
153
- placeholder: 'Choose date',
154
- monthFormat: 'long',
155
- },
156
- render: function Render(args: DateLookupProps) {
157
- const min = new Date(2024, 11, 9);
158
- const max = new Date(2025, 0, 22);
159
- const bankHolidays = [new Date(2024, 11, 25), new Date(2024, 11, 26), new Date(2025, 0, 1)];
160
- const [value, setValue] = useState<Date | null>(null);
161
-
162
- return (
163
- <DateLookup
164
- {...args}
165
- min={min}
166
- max={max}
167
- value={value}
168
- disabledRules={{
169
- weekdays: ['sat', 'sun'],
170
- dates: bankHolidays,
171
- ranges: [{ start: new Date(2024, 11, 23), end: new Date(2024, 11, 24) }],
172
- reason: 'Some dates are unavailable for this service.',
173
- }}
174
- onChange={(date) => {
175
- action('onChange')(date);
176
- setValue(date);
177
- }}
178
- onDateSelect={({ date, isEnabled }) => {
179
- action('onDateSelect')({ date, isEnabled });
180
- }}
181
- />
182
- );
183
- },
184
- });
185
-
186
- /**
187
- * When different disabled dates warrant different explanations, drive the `reason`
188
- * dynamically from `onDateSelect`. The `isEnabled` flag tells you whether the focused
189
- * date is selectable; from there you can look up the specific reason and update state.
190
- *
191
- * `min`/`max` out-of-range dates are handled automatically — the component shows a
192
- * built-in prompt with the boundary dates, so `determineReason` only needs to cover
193
- * rule-disabled dates.
194
- */
195
- export const DisabledDatesWithDynamicReason: Story = storySourceWithoutNoise({
196
- args: {
197
- size: Size.MEDIUM,
198
- label: 'Select a date',
199
- placeholder: 'Choose date',
200
- monthFormat: 'long',
201
- },
202
- render: function Render(args: DateLookupProps) {
203
- const DEFAULT_REASON = 'Some dates are unavailable for this service.';
204
- const min = new Date(2024, 11, 9);
205
- const max = new Date(2025, 0, 22);
206
- const bankHolidays = [new Date(2024, 11, 25), new Date(2024, 11, 26), new Date(2025, 0, 1)];
207
- const [value, setValue] = useState<Date | null>(null);
208
- const [reason, setReason] = useState(DEFAULT_REASON);
209
-
210
- function determineReason(date: Date, isEnabled: boolean): string {
211
- if (isEnabled) return DEFAULT_REASON;
212
- if (bankHolidays.some((holiday) => holiday.toDateString() === date.toDateString())) {
213
- return 'This date is a bank holiday.';
214
- }
215
- if (date.getDay() === 0 || date.getDay() === 6) {
216
- return 'We do not operate on weekends.';
217
- }
218
- return 'This date is unavailable due to scheduled maintenance.';
219
- }
220
-
221
- return (
222
- <DateLookup
223
- {...args}
224
- min={min}
225
- max={max}
226
- value={value}
227
- disabledRules={{
228
- weekdays: ['sat', 'sun'],
229
- dates: bankHolidays,
230
- ranges: [{ start: new Date(2024, 11, 23), end: new Date(2024, 11, 24) }],
231
- reason,
232
- }}
233
- onChange={(date) => {
234
- action('onChange')(date);
235
- setValue(date);
236
- }}
237
- onDateSelect={({ date, isEnabled }) => {
238
- action('onDateSelect')({ date, isEnabled });
239
- setReason(determineReason(date, isEnabled));
240
- }}
241
- />
242
- );
243
- },
244
- });
@@ -1,8 +1,2 @@
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';
@@ -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
+ });