@transferwise/components 0.0.0-experimental-c388243 → 0.0.0-experimental-18063d6
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/chips/Chip.js.map +1 -1
- package/build/chips/Chip.mjs.map +1 -1
- package/build/chips/Chips.js.map +1 -1
- package/build/chips/Chips.mjs.map +1 -1
- package/build/index.js +0 -2
- package/build/index.js.map +1 -1
- package/build/index.mjs +0 -1
- package/build/index.mjs.map +1 -1
- package/build/main.css +0 -35
- package/build/styles/main.css +0 -35
- package/build/types/body/Body.d.ts +2 -2
- package/build/types/chips/Chip.d.ts +3 -3
- package/build/types/chips/Chip.d.ts.map +1 -1
- package/build/types/chips/Chips.d.ts +10 -9
- package/build/types/chips/Chips.d.ts.map +1 -1
- package/build/types/iconButton/IconButton.d.ts +1 -1
- package/build/types/index.d.ts +0 -2
- package/build/types/index.d.ts.map +1 -1
- 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/package.json +3 -3
- package/src/chips/Chip.tsx +4 -4
- package/src/chips/Chips.tsx +13 -18
- package/src/index.ts +0 -2
- package/src/main.css +0 -35
- package/src/main.less +0 -1
- package/build/markup/Markup.js +0 -128
- package/build/markup/Markup.js.map +0 -1
- package/build/markup/Markup.mjs +0 -126
- package/build/markup/Markup.mjs.map +0 -1
- package/build/markup/constants.js +0 -36
- package/build/markup/constants.js.map +0 -1
- package/build/markup/constants.mjs +0 -29
- package/build/markup/constants.mjs.map +0 -1
- package/build/markup/utils/parseMarkup.js +0 -226
- package/build/markup/utils/parseMarkup.js.map +0 -1
- package/build/markup/utils/parseMarkup.mjs +0 -223
- package/build/markup/utils/parseMarkup.mjs.map +0 -1
- package/build/markup/utils/sanitise.js +0 -91
- package/build/markup/utils/sanitise.js.map +0 -1
- package/build/markup/utils/sanitise.mjs +0 -89
- package/build/markup/utils/sanitise.mjs.map +0 -1
- package/build/styles/markup/Markup.css +0 -28
- package/build/styles/markup/_stories/Vulnerability/Vulnerability.css +0 -70
- package/build/types/markup/Markup.d.ts +0 -4
- package/build/types/markup/Markup.d.ts.map +0 -1
- package/build/types/markup/Markup.types.d.ts +0 -63
- package/build/types/markup/Markup.types.d.ts.map +0 -1
- package/build/types/markup/_stories/Vulnerability/Vulnerability.d.ts +0 -12
- package/build/types/markup/_stories/Vulnerability/Vulnerability.d.ts.map +0 -1
- package/build/types/markup/constants.d.ts +0 -20
- package/build/types/markup/constants.d.ts.map +0 -1
- package/build/types/markup/index.d.ts +0 -3
- package/build/types/markup/index.d.ts.map +0 -1
- package/build/types/markup/utils/parseMarkup.d.ts +0 -6
- package/build/types/markup/utils/parseMarkup.d.ts.map +0 -1
- package/build/types/markup/utils/sanitise.d.ts +0 -2
- package/build/types/markup/utils/sanitise.d.ts.map +0 -1
- package/src/markup/Markup.css +0 -28
- package/src/markup/Markup.less +0 -29
- package/src/markup/Markup.test.tsx +0 -330
- package/src/markup/Markup.tsx +0 -144
- package/src/markup/Markup.types.ts +0 -48
- package/src/markup/_stories/Markup.docs.mdx +0 -75
- package/src/markup/_stories/Markup.security.docs.mdx +0 -256
- package/src/markup/_stories/Markup.story.tsx +0 -198
- package/src/markup/_stories/Vulnerability/Vulnerability.css +0 -70
- package/src/markup/_stories/Vulnerability/Vulnerability.tsx +0 -30
- package/src/markup/constants.ts +0 -41
- package/src/markup/index.ts +0 -2
- package/src/markup/utils/parseMarkup.test.ts +0 -658
- package/src/markup/utils/parseMarkup.ts +0 -228
- package/src/markup/utils/sanitise.test.ts +0 -375
- package/src/markup/utils/sanitise.ts +0 -107
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
export declare const ALLOWED_TAGS: readonly ["important", "positive", "negative", "strong", "strikethrough", "paragraph", "p", "link", "a"];
|
|
2
|
-
/**
|
|
3
|
-
* Matches opening and closing tags from the ALLOWED_TAGS allow-list.
|
|
4
|
-
*
|
|
5
|
-
* Capture groups:
|
|
6
|
-
* 1. `/` if this is a closing tag, undefined otherwise
|
|
7
|
-
* 2. Tag name
|
|
8
|
-
* 3. Raw attribute string between the tag name and `>` (empty for closing tags)
|
|
9
|
-
*
|
|
10
|
-
* The lookahead `(?=[\s>/])` after the tag name ensures we don't match partial
|
|
11
|
-
* words — e.g. `<stronger>` won't be matched as `<strong>`.
|
|
12
|
-
*
|
|
13
|
-
* Global flag (`g`) allows iterative matching via `exec()`.
|
|
14
|
-
*/
|
|
15
|
-
export declare const TAG_REGEX: RegExp;
|
|
16
|
-
export declare const TAG_ALIASES: Record<string, string>;
|
|
17
|
-
export declare const MAX_NESTING_DEPTH = 32;
|
|
18
|
-
export declare const ALLOWED_TARGETS: string[];
|
|
19
|
-
export declare const ALLOWED_PROTOCOLS: string[];
|
|
20
|
-
//# sourceMappingURL=constants.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"constants.d.ts","sourceRoot":"","sources":["../../../src/markup/constants.ts"],"names":[],"mappings":"AACA,eAAO,MAAM,YAAY,0GAUf,CAAC;AAEX;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,SAAS,QAGrB,CAAC;AAEF,eAAO,MAAM,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAiC,CAAC;AAGjF,eAAO,MAAM,iBAAiB,KAAK,CAAC;AAGpC,eAAO,MAAM,eAAe,UAAsB,CAAC;AAGnD,eAAO,MAAM,iBAAiB,UAAwB,CAAC"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/markup/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAClC,YAAY,EAAE,mBAAmB,EAAE,aAAa,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAC"}
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import { MAX_NESTING_DEPTH } from '../constants';
|
|
2
|
-
import { MarkupNode } from '../Markup.types';
|
|
3
|
-
export { MAX_NESTING_DEPTH };
|
|
4
|
-
/** Entry point: tokenises the input string, then parses the token stream into a MarkupNode tree. */
|
|
5
|
-
export declare function parseMarkup(input: string): MarkupNode[];
|
|
6
|
-
//# sourceMappingURL=parseMarkup.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"parseMarkup.d.ts","sourceRoot":"","sources":["../../../../src/markup/utils/parseMarkup.ts"],"names":[],"mappings":"AAAA,OAAO,EAAmB,iBAAiB,EAA0B,MAAM,cAAc,CAAC;AAC1F,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAE7C,OAAO,EAAE,iBAAiB,EAAE,CAAC;AAuN7B,oGAAoG;AACpG,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,UAAU,EAAE,CAQvD"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"sanitise.d.ts","sourceRoot":"","sources":["../../../../src/markup/utils/sanitise.ts"],"names":[],"mappings":"AAaA,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAmFnD"}
|
package/src/markup/Markup.css
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
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
|
-
}
|
package/src/markup/Markup.less
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
.wds-markup-emphasis {
|
|
2
|
-
font-weight: var(--font-weight-semi-bold);
|
|
3
|
-
font-style: normal;
|
|
4
|
-
}
|
|
5
|
-
|
|
6
|
-
.wds-markup-emphasis--important {
|
|
7
|
-
color: var(--color-content-primary);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
.wds-markup-emphasis--positive {
|
|
11
|
-
color: var(--color-content-positive);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
.wds-markup-emphasis--negative {
|
|
15
|
-
color: var(--color-content-negative);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
.wds-markup-strong {
|
|
19
|
-
font-weight: var(--font-weight-semi-bold);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/// @see https://www.webaxe.org/strikethrough-html-accessibility/
|
|
23
|
-
.wds-markup-strikethrough {
|
|
24
|
-
text-decoration: line-through;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
.wds-markup-paragraph {
|
|
28
|
-
margin: 0;
|
|
29
|
-
}
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
import { render, screen, userEvent } from '../test-utils';
|
|
4
|
-
|
|
5
|
-
import { Markup } from './Markup';
|
|
6
|
-
|
|
7
|
-
describe('Markup', () => {
|
|
8
|
-
it('renders plain text', () => {
|
|
9
|
-
render(<Markup>hello world</Markup>);
|
|
10
|
-
expect(screen.getByText('hello world')).toBeInTheDocument();
|
|
11
|
-
});
|
|
12
|
-
|
|
13
|
-
it('returns null for empty children', () => {
|
|
14
|
-
const { container } = render(<Markup />);
|
|
15
|
-
expect(container).toBeEmptyDOMElement();
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('wrapper element', () => {
|
|
19
|
-
it('renders in a span wrapper for inline content', () => {
|
|
20
|
-
render(<Markup>content</Markup>);
|
|
21
|
-
const element = screen.getByText('content');
|
|
22
|
-
expect(element.tagName).toBe('SPAN');
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
it('renders in a div wrapper when paragraph nodes are present', () => {
|
|
26
|
-
const { container } = render(<Markup>{'<p>block content</p>'}</Markup>);
|
|
27
|
-
const wrapper = container.firstChild as HTMLElement;
|
|
28
|
-
expect(wrapper.tagName).toBe('DIV');
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
it('renders in a div wrapper when nested paragraph nodes are present', () => {
|
|
32
|
-
const { container } = render(
|
|
33
|
-
<Markup>{'<important><p>nested paragraph</p></important>'}</Markup>,
|
|
34
|
-
);
|
|
35
|
-
const wrapper = container.firstChild as HTMLElement;
|
|
36
|
-
expect(wrapper.tagName).toBe('DIV');
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('uses span when as="span" even with paragraphs', () => {
|
|
40
|
-
const { container } = render(<Markup as="span">{'<p>forced span</p>'}</Markup>);
|
|
41
|
-
const wrapper = container.firstChild as HTMLElement;
|
|
42
|
-
expect(wrapper.tagName).toBe('SPAN');
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('uses div when as="div" even without paragraphs', () => {
|
|
46
|
-
const { container } = render(<Markup as="div">inline but div</Markup>);
|
|
47
|
-
const wrapper = container.firstChild as HTMLElement;
|
|
48
|
-
expect(wrapper.tagName).toBe('DIV');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('accepts any HTML element via as prop', () => {
|
|
52
|
-
const { container } = render(<Markup as="section">{'<p>in a section</p>'}</Markup>);
|
|
53
|
-
const wrapper = container.firstChild as HTMLElement;
|
|
54
|
-
expect(wrapper.tagName).toBe('SECTION');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('accepts a React component via as prop', () => {
|
|
58
|
-
const Card = ({
|
|
59
|
-
className,
|
|
60
|
-
children,
|
|
61
|
-
}: {
|
|
62
|
-
className?: string;
|
|
63
|
-
children?: React.ReactNode;
|
|
64
|
-
}) => (
|
|
65
|
-
<article data-testid="card" className={className}>
|
|
66
|
-
{children}
|
|
67
|
-
</article>
|
|
68
|
-
);
|
|
69
|
-
const { container } = render(
|
|
70
|
-
<Markup as={Card} className="custom">
|
|
71
|
-
{'<important>inside component</important>'}
|
|
72
|
-
</Markup>,
|
|
73
|
-
);
|
|
74
|
-
expect(screen.getByTestId('card')).toHaveClass('custom');
|
|
75
|
-
expect(container.querySelector('em')).toHaveTextContent('inside component');
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('passes data-testid to the wrapper element', () => {
|
|
79
|
-
render(<Markup data-testid="markup-wrapper">content</Markup>);
|
|
80
|
-
expect(screen.getByTestId('markup-wrapper')).toBeInTheDocument();
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('applies className to wrapper regardless of element type', () => {
|
|
84
|
-
render(<Markup className="custom">content</Markup>);
|
|
85
|
-
expect(screen.getByText('content')).toHaveClass('custom');
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
it('applies className to div wrapper with paragraphs', () => {
|
|
89
|
-
const { container } = render(<Markup className="block-wrapper">{'<p>paragraph</p>'}</Markup>);
|
|
90
|
-
const wrapper = container.firstChild as HTMLElement;
|
|
91
|
-
expect(wrapper).toHaveClass('block-wrapper');
|
|
92
|
-
expect(wrapper.tagName).toBe('DIV');
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe('emphasis', () => {
|
|
97
|
-
it('renders <important> with correct class', () => {
|
|
98
|
-
render(<Markup>{'<important>critical</important>'}</Markup>);
|
|
99
|
-
const element = screen.getByText('critical');
|
|
100
|
-
expect(element.tagName).toBe('EM');
|
|
101
|
-
expect(element).toHaveClass('wds-markup-emphasis', 'wds-markup-emphasis--important');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('renders <positive> with correct class', () => {
|
|
105
|
-
render(<Markup>{'<positive>good</positive>'}</Markup>);
|
|
106
|
-
expect(screen.getByText('good')).toHaveClass('wds-markup-emphasis--positive');
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
it('renders <negative> with correct class', () => {
|
|
110
|
-
render(<Markup>{'<negative>bad</negative>'}</Markup>);
|
|
111
|
-
expect(screen.getByText('bad')).toHaveClass('wds-markup-emphasis--negative');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('treats <warning> as plain text (unsupported tag)', () => {
|
|
115
|
-
render(<Markup>{'<warning>careful</warning>'}</Markup>);
|
|
116
|
-
expect(screen.getByText(/<warning>/)).toBeInTheDocument();
|
|
117
|
-
});
|
|
118
|
-
});
|
|
119
|
-
|
|
120
|
-
describe('links', () => {
|
|
121
|
-
it('renders a link with href', () => {
|
|
122
|
-
render(<Markup>{'<link href="/settings">settings</link>'}</Markup>);
|
|
123
|
-
const link = screen.getByRole('link', { name: 'settings' });
|
|
124
|
-
expect(link).toHaveAttribute('href', '/settings');
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
it('renders a link with target', () => {
|
|
128
|
-
render(<Markup>{'<link href="https://example.com" target="_blank">visit</link>'}</Markup>);
|
|
129
|
-
const link = screen.getByRole('link', { name: /visit/ });
|
|
130
|
-
expect(link).toHaveAttribute('target', '_blank');
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
it('renders a button-mode link with action', async () => {
|
|
134
|
-
const handleSave = jest.fn();
|
|
135
|
-
render(<Markup actions={{ save: handleSave }}>{'<link action="save">Save</link>'}</Markup>);
|
|
136
|
-
const button = screen.getByRole('button', { name: 'Save' });
|
|
137
|
-
await userEvent.click(button);
|
|
138
|
-
expect(handleSave).toHaveBeenCalledTimes(1);
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
it('renders as plain text when action handler is missing', () => {
|
|
142
|
-
render(<Markup>{'<link action="missing">text</link>'}</Markup>);
|
|
143
|
-
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
|
144
|
-
expect(screen.queryByRole('button')).not.toBeInTheDocument();
|
|
145
|
-
expect(screen.getByText('text')).toBeInTheDocument();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('renders as plain text when href is unsafe', () => {
|
|
149
|
-
render(<Markup>{'<link href="javascript:alert(1)">evil</link>'}</Markup>);
|
|
150
|
-
expect(screen.queryByRole('link')).not.toBeInTheDocument();
|
|
151
|
-
expect(screen.getByText('evil')).toBeInTheDocument();
|
|
152
|
-
});
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
describe('nesting', () => {
|
|
156
|
-
it('renders emphasis inside link', () => {
|
|
157
|
-
render(<Markup>{'<link href="/x"><important>bold link</important></link>'}</Markup>);
|
|
158
|
-
const link = screen.getByRole('link');
|
|
159
|
-
const emphasis = link.querySelector('em');
|
|
160
|
-
expect(emphasis).toHaveClass('wds-markup-emphasis--important');
|
|
161
|
-
expect(emphasis).toHaveTextContent('bold link');
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
it('renders link inside emphasis', () => {
|
|
165
|
-
render(<Markup>{'<important>Read the <link href="/terms">terms</link></important>'}</Markup>);
|
|
166
|
-
const emphasis = screen.getByText(/Read the/);
|
|
167
|
-
expect(emphasis.tagName).toBe('EM');
|
|
168
|
-
const link = screen.getByRole('link', { name: 'terms' });
|
|
169
|
-
expect(emphasis).toContainElement(link);
|
|
170
|
-
});
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
describe('strong', () => {
|
|
174
|
-
it('renders <strong> as a strong element', () => {
|
|
175
|
-
render(<Markup>{'<strong>bold</strong>'}</Markup>);
|
|
176
|
-
const element = screen.getByText('bold');
|
|
177
|
-
expect(element.tagName).toBe('STRONG');
|
|
178
|
-
});
|
|
179
|
-
});
|
|
180
|
-
|
|
181
|
-
describe('strikethrough', () => {
|
|
182
|
-
it('renders <strikethrough> as a del element', () => {
|
|
183
|
-
render(<Markup>{'<strikethrough>removed</strikethrough>'}</Markup>);
|
|
184
|
-
const element = screen.getByText('removed');
|
|
185
|
-
expect(element.tagName).toBe('DEL');
|
|
186
|
-
});
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
describe('paragraph', () => {
|
|
190
|
-
it('renders <paragraph> as a p element', () => {
|
|
191
|
-
render(<Markup>{'<paragraph>A paragraph.</paragraph>'}</Markup>);
|
|
192
|
-
const element = screen.getByText('A paragraph.');
|
|
193
|
-
expect(element.tagName).toBe('P');
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('renders <p> as a p element', () => {
|
|
197
|
-
render(<Markup>{'<p>Short form.</p>'}</Markup>);
|
|
198
|
-
const element = screen.getByText('Short form.');
|
|
199
|
-
expect(element.tagName).toBe('P');
|
|
200
|
-
});
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
describe('newlines', () => {
|
|
204
|
-
it('renders newlines as br elements', () => {
|
|
205
|
-
const { container } = render(<Markup>{'line one\nline two'}</Markup>);
|
|
206
|
-
expect(container.querySelector('br')).toBeInTheDocument();
|
|
207
|
-
});
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
describe('non-string children (pass-through)', () => {
|
|
211
|
-
it('passes through JSX elements unchanged', () => {
|
|
212
|
-
render(
|
|
213
|
-
<Markup>
|
|
214
|
-
<button type="button">Click me</button>
|
|
215
|
-
</Markup>,
|
|
216
|
-
);
|
|
217
|
-
expect(screen.getByRole('button', { name: 'Click me' })).toBeInTheDocument();
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
it('passes through a React component', () => {
|
|
221
|
-
const Custom = () => <span data-testid="custom">custom component</span>;
|
|
222
|
-
render(
|
|
223
|
-
<Markup>
|
|
224
|
-
<Custom />
|
|
225
|
-
</Markup>,
|
|
226
|
-
);
|
|
227
|
-
expect(screen.getByTestId('custom')).toBeInTheDocument();
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
it('applies className when passing through JSX', () => {
|
|
231
|
-
const { container } = render(
|
|
232
|
-
<Markup className="wrapper">
|
|
233
|
-
<em>styled</em>
|
|
234
|
-
</Markup>,
|
|
235
|
-
);
|
|
236
|
-
expect(container.firstChild).toHaveClass('wrapper');
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('returns null for undefined children', () => {
|
|
240
|
-
const { container } = render(<Markup>{undefined}</Markup>);
|
|
241
|
-
expect(container).toBeEmptyDOMElement();
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('returns null for null children', () => {
|
|
245
|
-
const { container } = render(<Markup>{null}</Markup>);
|
|
246
|
-
expect(container).toBeEmptyDOMElement();
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('passes through numbers as-is', () => {
|
|
250
|
-
render(<Markup>{42}</Markup>);
|
|
251
|
-
expect(screen.getByText('42')).toBeInTheDocument();
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
it('passes through fragment with mixed content', () => {
|
|
255
|
-
render(
|
|
256
|
-
<Markup>
|
|
257
|
-
<>
|
|
258
|
-
<strong>bold</strong> and text
|
|
259
|
-
</>
|
|
260
|
-
</Markup>,
|
|
261
|
-
);
|
|
262
|
-
expect(screen.getByText('bold')).toBeInTheDocument();
|
|
263
|
-
expect(screen.getByText(/and text/)).toBeInTheDocument();
|
|
264
|
-
});
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
describe('accessibilityLabel', () => {
|
|
268
|
-
it('applies aria-label to emphasis tags', () => {
|
|
269
|
-
render(
|
|
270
|
-
<Markup>{'<important accessibilityLabel="Important notice">critical</important>'}</Markup>,
|
|
271
|
-
);
|
|
272
|
-
const element = screen.getByText('critical');
|
|
273
|
-
expect(element).toHaveAttribute('aria-label', 'Important notice');
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
it('applies aria-label to strong tags', () => {
|
|
277
|
-
render(<Markup>{'<strong accessibilityLabel="Highlighted">bold</strong>'}</Markup>);
|
|
278
|
-
expect(screen.getByText('bold')).toHaveAttribute('aria-label', 'Highlighted');
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('applies aria-label to strikethrough tags', () => {
|
|
282
|
-
render(
|
|
283
|
-
<Markup>
|
|
284
|
-
{'<strikethrough accessibilityLabel="No longer valid">old price</strikethrough>'}
|
|
285
|
-
</Markup>,
|
|
286
|
-
);
|
|
287
|
-
expect(screen.getByText('old price')).toHaveAttribute('aria-label', 'No longer valid');
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
it('applies aria-label to link tags with href', () => {
|
|
291
|
-
render(
|
|
292
|
-
<Markup>
|
|
293
|
-
{'<link href="/settings" accessibilityLabel="Go to account settings">settings</link>'}
|
|
294
|
-
</Markup>,
|
|
295
|
-
);
|
|
296
|
-
const link = screen.getByRole('link', { name: 'Go to account settings' });
|
|
297
|
-
expect(link).toBeInTheDocument();
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
it('applies aria-label to button-mode link tags', () => {
|
|
301
|
-
const handler = jest.fn();
|
|
302
|
-
render(
|
|
303
|
-
<Markup actions={{ save: handler }}>
|
|
304
|
-
{'<link action="save" accessibilityLabel="Save your changes">Save</link>'}
|
|
305
|
-
</Markup>,
|
|
306
|
-
);
|
|
307
|
-
expect(screen.getByRole('button', { name: 'Save your changes' })).toBeInTheDocument();
|
|
308
|
-
});
|
|
309
|
-
|
|
310
|
-
it('does not apply aria-label to paragraph tags', () => {
|
|
311
|
-
render(<Markup>{'<paragraph accessibilityLabel="ignored">content</paragraph>'}</Markup>);
|
|
312
|
-
const element = screen.getByText('content');
|
|
313
|
-
expect(element.tagName).toBe('P');
|
|
314
|
-
expect(element).not.toHaveAttribute('aria-label');
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('does not render aria-label attribute when not provided', () => {
|
|
318
|
-
render(<Markup>{'<strong>plain bold</strong>'}</Markup>);
|
|
319
|
-
expect(screen.getByText('plain bold')).not.toHaveAttribute('aria-label');
|
|
320
|
-
});
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
describe('security', () => {
|
|
324
|
-
it('does not render script tags', () => {
|
|
325
|
-
const { container } = render(<Markup>{'<script>alert("xss")</script>'}</Markup>);
|
|
326
|
-
expect(container.querySelector('script')).not.toBeInTheDocument();
|
|
327
|
-
expect(container).toHaveTextContent('<script>alert("xss")</script>');
|
|
328
|
-
});
|
|
329
|
-
});
|
|
330
|
-
});
|
package/src/markup/Markup.tsx
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import React, { Fragment, useMemo } from 'react';
|
|
2
|
-
|
|
3
|
-
import Link from '../link';
|
|
4
|
-
|
|
5
|
-
import { parseMarkup } from './utils/parseMarkup';
|
|
6
|
-
import { isAllowedHref } from './utils/sanitise';
|
|
7
|
-
import type { MarkupActions, MarkupNode, MarkupProps } from './Markup.types';
|
|
8
|
-
|
|
9
|
-
function renderNodes(nodes: MarkupNode[], actions?: MarkupActions): React.ReactNode[] {
|
|
10
|
-
return nodes.map((node, index) => renderNode(node, actions, index));
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function renderLinkNode(
|
|
14
|
-
node: Extract<MarkupNode, { type: 'link' }>,
|
|
15
|
-
actions: MarkupActions | undefined,
|
|
16
|
-
key: number,
|
|
17
|
-
): React.ReactNode {
|
|
18
|
-
const children = renderNodes(node.children, actions);
|
|
19
|
-
|
|
20
|
-
if (node.action && actions?.[node.action]) {
|
|
21
|
-
return (
|
|
22
|
-
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
|
23
|
-
<Link key={key} aria-label={node.accessibilityLabel} onClick={actions[node.action]}>
|
|
24
|
-
{children}
|
|
25
|
-
</Link>
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (node.href && isAllowedHref(node.href)) {
|
|
30
|
-
return (
|
|
31
|
-
<Link key={key} href={node.href} target={node.target} aria-label={node.accessibilityLabel}>
|
|
32
|
-
{children}
|
|
33
|
-
</Link>
|
|
34
|
-
);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
return <Fragment key={key}>{children}</Fragment>;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function renderNode(
|
|
41
|
-
node: MarkupNode,
|
|
42
|
-
actions: MarkupActions | undefined,
|
|
43
|
-
key: number,
|
|
44
|
-
): React.ReactNode {
|
|
45
|
-
switch (node.type) {
|
|
46
|
-
case 'text':
|
|
47
|
-
return <Fragment key={key}>{node.content}</Fragment>;
|
|
48
|
-
case 'newline':
|
|
49
|
-
return <br key={key} />;
|
|
50
|
-
case 'important':
|
|
51
|
-
case 'positive':
|
|
52
|
-
case 'negative':
|
|
53
|
-
return (
|
|
54
|
-
<em
|
|
55
|
-
key={key}
|
|
56
|
-
className={`wds-markup-emphasis wds-markup-emphasis--${node.type}`}
|
|
57
|
-
aria-label={node.accessibilityLabel}
|
|
58
|
-
>
|
|
59
|
-
{renderNodes(node.children, actions)}
|
|
60
|
-
</em>
|
|
61
|
-
);
|
|
62
|
-
case 'strong':
|
|
63
|
-
return (
|
|
64
|
-
<strong key={key} className="wds-markup-strong" aria-label={node.accessibilityLabel}>
|
|
65
|
-
{renderNodes(node.children, actions)}
|
|
66
|
-
</strong>
|
|
67
|
-
);
|
|
68
|
-
case 'strikethrough':
|
|
69
|
-
return (
|
|
70
|
-
<del key={key} className="wds-markup-strikethrough" aria-label={node.accessibilityLabel}>
|
|
71
|
-
{renderNodes(node.children, actions)}
|
|
72
|
-
</del>
|
|
73
|
-
);
|
|
74
|
-
case 'paragraph':
|
|
75
|
-
return (
|
|
76
|
-
<p key={key} className="wds-markup-paragraph">
|
|
77
|
-
{renderNodes(node.children, actions)}
|
|
78
|
-
</p>
|
|
79
|
-
);
|
|
80
|
-
case 'link':
|
|
81
|
-
return renderLinkNode(node, actions, key);
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* Parses a markup string and renders it as React elements. Supports `<important>`,
|
|
87
|
-
* `<positive>`, `<negative>`, `<strong>`, `<strikethrough>`, `<paragraph>` (alias `<p>`),
|
|
88
|
-
* and `<link>` (alias `<a>`) with secure href validation.
|
|
89
|
-
*
|
|
90
|
-
* It only processes `string` input, while `ReactNode` is passed-through without processing.
|
|
91
|
-
*
|
|
92
|
-
* > The component supersedes [Emphasis](?path=/docs/typography-emphasis--docs), which will be
|
|
93
|
-
* eventually deprecated.
|
|
94
|
-
*
|
|
95
|
-
* @see http://storybook.wise.design/?path=/docs/typography-markup--docs
|
|
96
|
-
* @link ../emphasis/Emphasis | Emphasis
|
|
97
|
-
*/
|
|
98
|
-
function hasParagraph(nodes: MarkupNode[]): boolean {
|
|
99
|
-
for (const node of nodes) {
|
|
100
|
-
if (node.type === 'paragraph') {
|
|
101
|
-
return true;
|
|
102
|
-
}
|
|
103
|
-
if ('children' in node && hasParagraph(node.children)) {
|
|
104
|
-
return true;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
return false;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
export const Markup: React.FC<MarkupProps> = ({
|
|
111
|
-
children,
|
|
112
|
-
actions,
|
|
113
|
-
as,
|
|
114
|
-
className,
|
|
115
|
-
'data-testid': testId,
|
|
116
|
-
}) => {
|
|
117
|
-
const tree = useMemo(
|
|
118
|
-
() => (typeof children === 'string' ? parseMarkup(children) : []),
|
|
119
|
-
[children],
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
if (typeof children !== 'string') {
|
|
123
|
-
if (children == null) {
|
|
124
|
-
return null;
|
|
125
|
-
}
|
|
126
|
-
return (
|
|
127
|
-
<span className={className} data-testid={testId}>
|
|
128
|
-
{children}
|
|
129
|
-
</span>
|
|
130
|
-
);
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (tree.length === 0) {
|
|
134
|
-
return null;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
const Element = as ?? (hasParagraph(tree) ? 'div' : 'span');
|
|
138
|
-
|
|
139
|
-
return (
|
|
140
|
-
<Element className={className} data-testid={testId}>
|
|
141
|
-
{renderNodes(tree, actions)}
|
|
142
|
-
</Element>
|
|
143
|
-
);
|
|
144
|
-
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import React from 'react';
|
|
2
|
-
|
|
3
|
-
import type { LinkProps } from '../link';
|
|
4
|
-
|
|
5
|
-
export type MarkupNode =
|
|
6
|
-
| { type: 'text'; content: string }
|
|
7
|
-
| { type: 'newline' }
|
|
8
|
-
| { type: 'important'; accessibilityLabel?: string; children: MarkupNode[] }
|
|
9
|
-
| { type: 'positive'; accessibilityLabel?: string; children: MarkupNode[] }
|
|
10
|
-
| { type: 'negative'; accessibilityLabel?: string; children: MarkupNode[] }
|
|
11
|
-
| { type: 'strong'; accessibilityLabel?: string; children: MarkupNode[] }
|
|
12
|
-
| { type: 'strikethrough'; accessibilityLabel?: string; children: MarkupNode[] }
|
|
13
|
-
| { type: 'paragraph'; children: MarkupNode[] }
|
|
14
|
-
| {
|
|
15
|
-
type: 'link';
|
|
16
|
-
href?: string;
|
|
17
|
-
action?: string;
|
|
18
|
-
target?: string;
|
|
19
|
-
accessibilityLabel?: string;
|
|
20
|
-
children: MarkupNode[];
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type MarkupActionHandler = NonNullable<LinkProps['onClick']>;
|
|
24
|
-
|
|
25
|
-
export type MarkupActions = Record<string, MarkupActionHandler>;
|
|
26
|
-
|
|
27
|
-
export interface MarkupProps {
|
|
28
|
-
/**
|
|
29
|
-
* Markup string to parse and render, or arbitrary JSX to pass through unchanged.
|
|
30
|
-
* Parsing only occurs when children is a string.
|
|
31
|
-
* */
|
|
32
|
-
children?: React.ReactNode;
|
|
33
|
-
/**
|
|
34
|
-
* Map of action names to click handlers for `<link action="name">` button-mode links.
|
|
35
|
-
* */
|
|
36
|
-
actions?: MarkupActions;
|
|
37
|
-
/**
|
|
38
|
-
* Wrapper element. Defaults to `div` when the parsed tree contains `<paragraph>` nodes,
|
|
39
|
-
* `span` otherwise. Set explicitly to override auto-detection.
|
|
40
|
-
* When passing a custom React component, it must accept a `className` prop.
|
|
41
|
-
* */
|
|
42
|
-
as?: React.ElementType;
|
|
43
|
-
className?: string;
|
|
44
|
-
/**
|
|
45
|
-
* When using a custom React component via `as`, it must also accept a `data-testid` prop.
|
|
46
|
-
* */
|
|
47
|
-
'data-testid'?: string;
|
|
48
|
-
}
|