@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.
- package/build/Calendar/Calendar.js +16 -94
- package/build/Calendar/Calendar.js.map +1 -1
- package/build/Calendar/Calendar.messages.js +0 -12
- package/build/Calendar/Calendar.messages.js.map +1 -1
- package/build/Calendar/Calendar.messages.mjs +0 -12
- package/build/Calendar/Calendar.messages.mjs.map +1 -1
- package/build/Calendar/Calendar.mjs +17 -95
- package/build/Calendar/Calendar.mjs.map +1 -1
- package/build/Calendar/DateHeader/DateHeader.js +1 -5
- package/build/Calendar/DateHeader/DateHeader.js.map +1 -1
- package/build/Calendar/DateHeader/DateHeader.mjs +1 -5
- package/build/Calendar/DateHeader/DateHeader.mjs.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.js +0 -14
- package/build/Calendar/DayCalendar/DayCalendar.js.map +1 -1
- package/build/Calendar/DayCalendar/DayCalendar.mjs +0 -14
- package/build/Calendar/DayCalendar/DayCalendar.mjs.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js +19 -53
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.js.map +1 -1
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs +19 -53
- package/build/Calendar/DayCalendar/Table/DayCalendarTable.mjs.map +1 -1
- package/build/Calendar/MonthCalendar/MonthCalendar.js +0 -6
- package/build/Calendar/MonthCalendar/MonthCalendar.js.map +1 -1
- package/build/Calendar/MonthCalendar/MonthCalendar.mjs +0 -6
- package/build/Calendar/MonthCalendar/MonthCalendar.mjs.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js +3 -8
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.js.map +1 -1
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs +3 -8
- package/build/Calendar/MonthCalendar/Table/MonthCalendarTable.mjs.map +1 -1
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js +3 -8
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.js.map +1 -1
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs +3 -8
- package/build/Calendar/YearCalendar/Table/YearCalendarTable.mjs.map +1 -1
- package/build/Calendar/YearCalendar/YearCalendar.js +0 -7
- package/build/Calendar/YearCalendar/YearCalendar.js.map +1 -1
- package/build/Calendar/YearCalendar/YearCalendar.mjs +0 -7
- package/build/Calendar/YearCalendar/YearCalendar.mjs.map +1 -1
- package/build/Calendar/{TableLink → tableLink}/TableLink.js +11 -26
- package/build/Calendar/tableLink/TableLink.js.map +1 -0
- package/build/Calendar/{TableLink → tableLink}/TableLink.mjs +11 -26
- package/build/Calendar/tableLink/TableLink.mjs.map +1 -0
- package/build/DateLookup/DateLookup.js +9 -20
- package/build/DateLookup/DateLookup.js.map +1 -1
- package/build/DateLookup/DateLookup.mjs +9 -20
- package/build/DateLookup/DateLookup.mjs.map +1 -1
- package/build/Markup/Markup.js +137 -0
- package/build/Markup/Markup.js.map +1 -0
- package/build/Markup/Markup.mjs +132 -0
- package/build/Markup/Markup.mjs.map +1 -0
- package/build/Markup/constants.js +36 -0
- package/build/Markup/constants.js.map +1 -0
- package/build/Markup/constants.mjs +29 -0
- package/build/Markup/constants.mjs.map +1 -0
- package/build/Markup/utils/parseMarkup.js +236 -0
- package/build/Markup/utils/parseMarkup.js.map +1 -0
- package/build/Markup/utils/parseMarkup.mjs +231 -0
- package/build/Markup/utils/parseMarkup.mjs.map +1 -0
- package/build/Markup/utils/sanitise.js +114 -0
- package/build/Markup/utils/sanitise.js.map +1 -0
- package/build/Markup/utils/sanitise.mjs +111 -0
- package/build/Markup/utils/sanitise.mjs.map +1 -0
- package/build/i18n/en.json +0 -4
- package/build/i18n/en.json.js +0 -4
- package/build/i18n/en.json.js.map +1 -1
- package/build/i18n/en.json.mjs +0 -4
- package/build/i18n/en.json.mjs.map +1 -1
- package/build/index.js +4 -2
- package/build/index.js.map +1 -1
- package/build/index.mjs +2 -1
- package/build/index.mjs.map +1 -1
- package/build/main.css +78 -123
- package/build/styles/Calendar/Calendar.css +37 -112
- package/build/styles/Markup/Markup.css +28 -0
- package/build/styles/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
- package/build/styles/main.css +78 -123
- package/build/types/Body/Body.d.ts +2 -2
- package/build/types/Calendar/Calendar.d.ts +3 -16
- package/build/types/Calendar/Calendar.d.ts.map +1 -1
- package/build/types/Calendar/Calendar.messages.d.ts +0 -20
- package/build/types/Calendar/Calendar.messages.d.ts.map +1 -1
- package/build/types/Calendar/DateHeader/DateHeader.d.ts +0 -2
- package/build/types/Calendar/DateHeader/DateHeader.d.ts.map +1 -1
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts +1 -5
- package/build/types/Calendar/DayCalendar/DayCalendar.d.ts.map +1 -1
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts +0 -4
- package/build/types/Calendar/DayCalendar/Table/DayCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts +0 -1
- package/build/types/Calendar/MonthCalendar/MonthCalendar.d.ts.map +1 -1
- package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts +1 -2
- package/build/types/Calendar/MonthCalendar/Table/MonthCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts +1 -2
- package/build/types/Calendar/YearCalendar/Table/YearCalendarTable.d.ts.map +1 -1
- package/build/types/Calendar/YearCalendar/YearCalendar.d.ts +0 -1
- package/build/types/Calendar/YearCalendar/YearCalendar.d.ts.map +1 -1
- package/build/types/Calendar/index.d.ts +0 -1
- package/build/types/Calendar/index.d.ts.map +1 -1
- package/build/types/Calendar/{TableLink → tableLink}/TableLink.d.ts +1 -3
- package/build/types/Calendar/tableLink/TableLink.d.ts.map +1 -0
- package/build/types/Calendar/tableLink/index.d.ts.map +1 -0
- package/build/types/DateLookup/DateLookup.d.ts +5 -24
- package/build/types/DateLookup/DateLookup.d.ts.map +1 -1
- package/build/types/DateLookup/index.d.ts +0 -1
- package/build/types/DateLookup/index.d.ts.map +1 -1
- package/build/types/IconButton/IconButton.d.ts +1 -1
- package/build/types/Markup/Markup.d.ts +18 -0
- package/build/types/Markup/Markup.d.ts.map +1 -0
- package/build/types/Markup/Markup.types.d.ts +77 -0
- package/build/types/Markup/Markup.types.d.ts.map +1 -0
- package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts +12 -0
- package/build/types/Markup/_stories/Vulnerability/Vulnerability.d.ts.map +1 -0
- package/build/types/Markup/constants.d.ts +20 -0
- package/build/types/Markup/constants.d.ts.map +1 -0
- package/build/types/Markup/index.d.ts +3 -0
- package/build/types/Markup/index.d.ts.map +1 -0
- package/build/types/Markup/utils/parseMarkup.d.ts +8 -0
- package/build/types/Markup/utils/parseMarkup.d.ts.map +1 -0
- package/build/types/Markup/utils/sanitise.d.ts +4 -0
- package/build/types/Markup/utils/sanitise.d.ts.map +1 -0
- package/build/types/Title/Title.d.ts +2 -2
- package/build/types/Upload/Steps/UploadImageStep/UploadImageStep.d.ts +1 -1
- package/build/types/UploadInput/UploadItem/UploadItemLink.d.ts +1 -1
- package/build/types/common/DateUtils/index.d.ts +0 -2
- package/build/types/common/DateUtils/index.d.ts.map +1 -1
- package/build/types/index.d.ts +4 -3
- package/build/types/index.d.ts.map +1 -1
- package/build/types/test-utils/index.d.ts +0 -8
- package/build/types/test-utils/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Calendar/Calendar.css +37 -112
- package/src/Calendar/Calendar.less +35 -5
- package/src/Calendar/Calendar.messages.ts +0 -21
- package/src/Calendar/Calendar.test.tsx +27 -288
- package/src/Calendar/Calendar.tsx +19 -136
- package/src/Calendar/DateHeader/DateHeader.tsx +0 -6
- package/src/Calendar/DayCalendar/DayCalendar.tsx +1 -33
- package/src/Calendar/DayCalendar/Table/DayCalendarTable.tsx +28 -62
- package/src/Calendar/MonthCalendar/MonthCalendar.tsx +0 -8
- package/src/Calendar/MonthCalendar/Table/MonthCalendarTable.tsx +3 -15
- package/src/Calendar/YearCalendar/Table/YearCalendarTable.tsx +3 -15
- package/src/Calendar/YearCalendar/YearCalendar.tsx +0 -9
- package/src/Calendar/_stories/Calendar.story.tsx +4 -138
- package/src/Calendar/index.ts +0 -7
- package/src/Calendar/{TableLink → tableLink}/TableLink.tsx +14 -29
- package/src/DateLookup/DateLookup.test.tsx +37 -34
- package/src/DateLookup/DateLookup.tsx +13 -37
- package/src/DateLookup/_stories/DateLookup.story.tsx +2 -124
- package/src/DateLookup/index.ts +0 -6
- package/src/Markup/Markup.css +28 -0
- package/src/Markup/Markup.injection.test.tsx +163 -0
- package/src/Markup/Markup.less +29 -0
- package/src/Markup/Markup.test.tsx +463 -0
- package/src/Markup/Markup.tsx +169 -0
- package/src/Markup/Markup.types.ts +62 -0
- package/src/Markup/_stories/Markup.docs.mdx +75 -0
- package/src/Markup/_stories/Markup.security.docs.mdx +343 -0
- package/src/Markup/_stories/Markup.story.tsx +266 -0
- package/src/Markup/_stories/Vulnerability/Vulnerability.css +69 -0
- package/src/Markup/_stories/Vulnerability/Vulnerability.tsx +30 -0
- package/src/Markup/constants.ts +41 -0
- package/src/Markup/index.ts +2 -0
- package/src/Markup/utils/parseMarkup.test.ts +706 -0
- package/src/Markup/utils/parseMarkup.ts +234 -0
- package/src/Markup/utils/sanitise.test.ts +497 -0
- package/src/Markup/utils/sanitise.ts +133 -0
- package/src/common/DateUtils/index.ts +0 -2
- package/src/i18n/en.json +0 -4
- package/src/index.ts +4 -3
- package/src/main.css +78 -123
- package/src/main.less +1 -0
- package/build/Calendar/TableLink/TableLink.js.map +0 -1
- package/build/Calendar/TableLink/TableLink.mjs.map +0 -1
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js +0 -33
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.js.map +0 -1
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs +0 -31
- package/build/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.mjs.map +0 -1
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.js +0 -40
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.js.map +0 -1
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs +0 -38
- package/build/common/DateUtils/isDateDisabled/isDateDisabled.mjs.map +0 -1
- package/build/styles/Calendar/TableLink/TableLink.css +0 -115
- package/build/types/Calendar/Calendar.types.d.ts +0 -32
- package/build/types/Calendar/Calendar.types.d.ts.map +0 -1
- package/build/types/Calendar/TableLink/TableLink.d.ts.map +0 -1
- package/build/types/Calendar/TableLink/index.d.ts.map +0 -1
- package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts +0 -3
- package/build/types/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.d.ts.map +0 -1
- package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts +0 -3
- package/build/types/common/DateUtils/isDateDisabled/isDateDisabled.d.ts.map +0 -1
- package/src/Calendar/Calendar.disabledDates.test.tsx +0 -518
- package/src/Calendar/Calendar.types.ts +0 -35
- package/src/Calendar/TableLink/TableLink.css +0 -115
- package/src/Calendar/TableLink/TableLink.less +0 -68
- package/src/Calendar/_stories/Calendar.accessibility.docs.mdx +0 -118
- package/src/Calendar/_stories/Calendar.test.story.tsx +0 -74
- package/src/DateLookup/DateLookup.disabledDates.test.tsx +0 -293
- package/src/DateLookup/_stories/DateLookup.accessibility.docs.mdx +0 -108
- package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.test.ts +0 -98
- package/src/common/DateUtils/hasDisabledDatesInMonth/hasDisabledDatesInMonth.ts +0 -45
- package/src/common/DateUtils/isDateDisabled/isDateDisabled.test.ts +0 -169
- package/src/common/DateUtils/isDateDisabled/isDateDisabled.ts +0 -59
- /package/build/types/Calendar/{TableLink → tableLink}/index.d.ts +0 -0
- /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
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
label += `, ${intl.formatMessage(messages.notAvailable)}`;
|
|
50
|
+
return `${longTitle || title || ''}, ${intl.formatMessage(
|
|
51
|
+
messages.selected,
|
|
52
|
+
)} ${intl.formatMessage(messages[type])}`;
|
|
60
53
|
}
|
|
61
|
-
return
|
|
54
|
+
return longTitle || title;
|
|
62
55
|
};
|
|
63
56
|
|
|
64
57
|
return (
|
|
65
|
-
<
|
|
66
|
-
// @ts-expect-error - Generic forwardRef limitation in SentimentSurface
|
|
58
|
+
<button
|
|
67
59
|
ref={buttonRef}
|
|
68
|
-
|
|
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
|
-
|
|
82
|
-
sentiment={isUnavailable ? 'negative' : 'neutral'}
|
|
66
|
+
disabled={disabled}
|
|
83
67
|
tabIndex={autofocus ? 0 : -1}
|
|
84
|
-
|
|
68
|
+
aria-label={calculateAriaLabel()}
|
|
69
|
+
aria-pressed={active}
|
|
85
70
|
onClick={onCalendarClick}
|
|
86
71
|
>
|
|
87
72
|
{title || item}
|
|
88
|
-
</
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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)
|
|
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(
|
|
179
|
-
|
|
180
|
-
expect(handleChange).not.toHaveBeenCalled();
|
|
181
|
-
});
|
|
187
|
+
await user.keyboard(text);
|
|
182
188
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
189
|
+
const value = new Date(initialValue);
|
|
190
|
+
value.setMonth(initialValue.getMonth() + step);
|
|
191
|
+
expect(handleChange).toHaveBeenCalledWith(value);
|
|
186
192
|
|
|
187
|
-
|
|
188
|
-
await
|
|
189
|
-
|
|
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(
|
|
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: '
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
183
|
-
|
|
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
|
|
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
|
-
});
|
package/src/DateLookup/index.ts
CHANGED
|
@@ -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
|
+
});
|