@squiz/formatted-text-editor 1.33.1-alpha.2 → 1.33.1-alpha.4
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/demo/App.tsx +4 -24
- package/demo/AppContext.tsx +28 -0
- package/demo/index.scss +0 -2
- package/demo/main.tsx +2 -0
- package/demo/resources.json +28 -0
- package/demo/sources.json +23 -0
- package/lib/Editor/EditorContext.d.ts +0 -7
- package/lib/Editor/EditorContext.js +0 -2
- package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +16 -32
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -2
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +18 -58
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +3 -2
- package/lib/EditorToolbar/Tools/Link/RemoveLinkButton.js +1 -1
- package/lib/Extensions/Extensions.js +0 -2
- package/lib/Extensions/ImageExtension/AssetImageExtension.d.ts +0 -1
- package/lib/Extensions/ImageExtension/AssetImageExtension.js +1 -2
- package/lib/Extensions/LinkExtension/AssetLinkExtension.d.ts +0 -1
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +2 -3
- package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
- package/lib/index.css +84 -4
- package/lib/types.d.ts +3 -3
- package/lib/ui/Fields/Checkbox/Checkbox.d.ts +8 -0
- package/lib/ui/Fields/Checkbox/Checkbox.js +47 -0
- package/lib/ui/Fields/Input/Input.d.ts +2 -4
- package/lib/ui/Fields/Input/Input.js +3 -9
- package/lib/ui/Fields/InputContainer/InputContainer.d.ts +9 -0
- package/lib/ui/Fields/InputContainer/InputContainer.js +16 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.d.ts +17 -0
- package/lib/ui/Fields/MatrixAsset/MatrixAsset.js +29 -0
- package/lib/ui/Modal/Modal.d.ts +1 -0
- package/lib/ui/Modal/Modal.js +3 -2
- package/lib/ui/Tabs/Tabs.d.ts +10 -0
- package/lib/ui/Tabs/Tabs.js +46 -0
- package/lib/utils/validation.d.ts +2 -1
- package/lib/utils/validation.js +8 -2
- package/package.json +4 -3
- package/src/Editor/Editor.spec.tsx +1 -1
- package/src/Editor/EditorContext.spec.tsx +11 -13
- package/src/Editor/EditorContext.ts +0 -11
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +29 -12
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +37 -53
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +76 -49
- package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +1 -0
- package/src/EditorToolbar/Tools/Image/ImageModal.tsx +3 -2
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +22 -13
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +35 -57
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +52 -36
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +3 -2
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.spec.tsx +47 -4
- package/src/EditorToolbar/Tools/Link/RemoveLinkButton.tsx +3 -2
- package/src/Extensions/Extensions.ts +0 -2
- package/src/Extensions/ImageExtension/AssetImageExtension.ts +1 -3
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +2 -4
- package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
- package/src/index.scss +1 -0
- package/src/types.ts +7 -5
- package/src/ui/Fields/Checkbox/Checkbox.spec.tsx +50 -0
- package/src/ui/Fields/Checkbox/Checkbox.tsx +49 -0
- package/src/ui/Fields/Checkbox/_checkbox.scss +26 -0
- package/src/ui/Fields/Input/Input.tsx +4 -18
- package/src/ui/Fields/InputContainer/InputContainer.spec.tsx +18 -0
- package/src/ui/Fields/InputContainer/InputContainer.tsx +29 -0
- package/src/ui/Fields/MatrixAsset/MatrixAsset.spec.tsx +103 -0
- package/src/ui/Fields/MatrixAsset/MatrixAsset.tsx +55 -0
- package/src/ui/Modal/FormModal.spec.tsx +2 -1
- package/src/ui/Modal/Modal.spec.tsx +15 -7
- package/src/ui/Modal/Modal.tsx +4 -2
- package/src/ui/Tabs/Tabs.spec.tsx +44 -0
- package/src/ui/Tabs/Tabs.tsx +41 -0
- package/src/ui/_forms.scss +4 -2
- package/src/utils/validation.spec.ts +22 -0
- package/src/utils/validation.ts +9 -1
- package/tests/index.ts +2 -0
- package/tests/mockResourceBrowserContext.tsx +63 -0
- package/tests/renderWithContext.tsx +18 -0
- package/tests/renderWithEditor.tsx +18 -21
- package/vite.config.ts +8 -0
- package/lib/ui/Fields/Select/Select.d.ts +0 -12
- package/lib/ui/Fields/Select/Select.js +0 -53
@@ -1,13 +1,14 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
2
|
import { act, screen, fireEvent, waitForElementToBeRemoved } from '@testing-library/react';
|
3
3
|
import React from 'react';
|
4
|
-
import { renderWithEditor,
|
4
|
+
import { renderWithEditor, mockResourceBrowserContext } from '../../../../tests';
|
5
5
|
import LinkButton from './LinkButton';
|
6
6
|
|
7
7
|
describe('LinkButton', () => {
|
8
8
|
const openModal = async () => {
|
9
9
|
fireEvent.click(screen.getByRole('button', { name: 'Link (cmd+K)' }));
|
10
10
|
await screen.findByRole('button', { name: 'Apply' });
|
11
|
+
fireEvent.click(screen.getByRole('button', { name: 'From URL' }));
|
11
12
|
};
|
12
13
|
|
13
14
|
it('Adds a new link', async () => {
|
@@ -21,7 +22,7 @@ describe('LinkButton', () => {
|
|
21
22
|
fireEvent.change(screen.getByLabelText('URL'), { target: { value: 'https://www.squiz.net/link-button' } });
|
22
23
|
fireEvent.change(screen.getByLabelText('Text'), { target: { value: 'Link text' } });
|
23
24
|
fireEvent.change(screen.getByLabelText('Title'), { target: { value: 'Link title' } });
|
24
|
-
|
25
|
+
fireEvent.click(document.querySelector('div.squiz-fte-checkbox button') as HTMLButtonElement);
|
25
26
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
26
27
|
|
27
28
|
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
@@ -235,19 +236,29 @@ describe('LinkButton', () => {
|
|
235
236
|
it('Add a new asset link', async () => {
|
236
237
|
const matrixIdentifier = 'matrix-api-identifier';
|
237
238
|
const matrixDomain = 'https://my-matrix.squiz.net';
|
238
|
-
const {
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
239
|
+
const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
|
240
|
+
sources: [{ id: matrixIdentifier }],
|
241
|
+
resources: [{ id: 'my-resource-id', name: 'My resource' }],
|
242
|
+
});
|
243
|
+
|
244
|
+
const { getJsonContent } = await renderWithEditor(
|
245
|
+
<MockResourceBrowserContext>
|
246
|
+
<LinkButton />
|
247
|
+
</MockResourceBrowserContext>,
|
248
|
+
{
|
249
|
+
context: {
|
250
|
+
editor: {
|
251
|
+
matrix: {
|
252
|
+
matrixDomain,
|
253
|
+
},
|
254
|
+
},
|
244
255
|
},
|
245
256
|
},
|
246
|
-
|
257
|
+
);
|
247
258
|
|
248
259
|
await openModal();
|
249
|
-
|
250
|
-
|
260
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
261
|
+
await selectResource(screen.getByRole('button', { name: 'Choose asset' }), 'My resource');
|
251
262
|
fireEvent.change(screen.getByLabelText('Text'), { target: { value: 'Link text' } });
|
252
263
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
253
264
|
|
@@ -263,7 +274,7 @@ describe('LinkButton', () => {
|
|
263
274
|
marks: [
|
264
275
|
{
|
265
276
|
type: 'assetLink',
|
266
|
-
attrs: { matrixAssetId: '
|
277
|
+
attrs: { matrixAssetId: 'my-resource-id', target: '_self', matrixDomain, matrixIdentifier },
|
267
278
|
},
|
268
279
|
],
|
269
280
|
},
|
@@ -274,25 +285,35 @@ describe('LinkButton', () => {
|
|
274
285
|
it('Updates an existing link to be an asset link', async () => {
|
275
286
|
const matrixIdentifier = 'matrix-api-identifier';
|
276
287
|
const matrixDomain = 'https://my-matrix.squiz.net';
|
277
|
-
const {
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
288
|
+
const { MockResourceBrowserContext, selectResource } = mockResourceBrowserContext({
|
289
|
+
sources: [{ id: matrixIdentifier }],
|
290
|
+
resources: [{ id: 'my-resource-id', name: 'My resource' }],
|
291
|
+
});
|
292
|
+
|
293
|
+
const { editor, getJsonContent } = await renderWithEditor(
|
294
|
+
<MockResourceBrowserContext>
|
295
|
+
<LinkButton />
|
296
|
+
</MockResourceBrowserContext>,
|
297
|
+
{
|
298
|
+
content:
|
299
|
+
'<a href="https://www.example.org/my-link">Sample link</a> with ' +
|
300
|
+
'<a href="https://www.example.org/another-link">another link</a>',
|
301
|
+
context: {
|
302
|
+
editor: {
|
303
|
+
matrix: {
|
304
|
+
matrixDomain,
|
305
|
+
},
|
306
|
+
},
|
286
307
|
},
|
287
308
|
},
|
288
|
-
|
309
|
+
);
|
289
310
|
|
290
311
|
await act(() => editor.selectText(5));
|
291
312
|
|
292
313
|
await openModal();
|
293
|
-
|
294
|
-
|
295
|
-
|
314
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
315
|
+
await selectResource(screen.getByRole('button', { name: 'Choose asset' }), 'My resource');
|
316
|
+
fireEvent.click(document.querySelector('div.squiz-fte-checkbox button') as HTMLButtonElement);
|
296
317
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
297
318
|
|
298
319
|
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
@@ -307,7 +328,7 @@ describe('LinkButton', () => {
|
|
307
328
|
marks: [
|
308
329
|
{
|
309
330
|
type: 'assetLink',
|
310
|
-
attrs: { matrixAssetId: '
|
331
|
+
attrs: { matrixAssetId: 'my-resource-id', target: '_blank', matrixDomain, matrixIdentifier },
|
311
332
|
},
|
312
333
|
],
|
313
334
|
},
|
@@ -326,18 +347,14 @@ describe('LinkButton', () => {
|
|
326
347
|
});
|
327
348
|
});
|
328
349
|
|
329
|
-
it('Shows an error if
|
330
|
-
|
331
|
-
|
332
|
-
await renderWithEditor(<LinkButton />, { context: { matrix: { resolveMatrixAsset } } });
|
350
|
+
it('Shows an error if no asset is selected', async () => {
|
351
|
+
await renderWithEditor(<LinkButton />);
|
333
352
|
|
334
353
|
await openModal();
|
335
|
-
|
336
|
-
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: 'invalid-asset-id' } });
|
354
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
337
355
|
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
338
356
|
|
339
|
-
expect(screen.getByText('
|
340
|
-
expect(resolveMatrixAsset).toHaveBeenCalledWith('invalid-asset-id');
|
357
|
+
expect(screen.getByText('An asset must be selected')).toBeInTheDocument();
|
341
358
|
});
|
342
359
|
|
343
360
|
it('Shows an error if a required field is not provided', async () => {
|
@@ -351,7 +368,6 @@ describe('LinkButton', () => {
|
|
351
368
|
|
352
369
|
expect(screen.getByText('URL is required')).toBeInTheDocument();
|
353
370
|
expect(screen.getByText('Text is required')).toBeInTheDocument();
|
354
|
-
expect(screen.getByText('Title is required')).toBeInTheDocument();
|
355
371
|
});
|
356
372
|
|
357
373
|
it('Shows an error if the field value is just an empty space', async () => {
|
@@ -363,6 +379,6 @@ describe('LinkButton', () => {
|
|
363
379
|
fireEvent.change(screen.getByLabelText('Title'), { target: { value: ' ' } });
|
364
380
|
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
365
381
|
|
366
|
-
expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(
|
382
|
+
expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(2);
|
367
383
|
});
|
368
384
|
});
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import { LinkForm, LinkFormData } from './Form/LinkForm';
|
2
2
|
import React from 'react';
|
3
3
|
import { useRemirrorContext } from '@remirror/react';
|
4
|
+
import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
|
4
5
|
import FormModal from '../../../ui/Modal/FormModal';
|
5
6
|
import { SubmitHandler } from 'react-hook-form';
|
6
7
|
import { useExpandedSelection } from '../../../hooks';
|
@@ -19,7 +20,7 @@ const LinkModal = ({ onCancel, onSubmit }: LinkModalProps) => {
|
|
19
20
|
const { selection, marks } = useExpandedSelection([MarkName.Link, MarkName.AssetLink]);
|
20
21
|
const selectedText = helpers.getTextBetween(selection.from, selection.to, state.doc);
|
21
22
|
const data = {
|
22
|
-
linkType: marks[0]?.type?.name === MarkName.
|
23
|
+
linkType: marks[0]?.type?.name === MarkName.Link ? MarkName.Link : MarkName.AssetLink,
|
23
24
|
text: selectedText,
|
24
25
|
link: { ...marks.find((mark) => mark.type.name === 'link')?.attrs },
|
25
26
|
assetLink: { ...marks.find((mark) => mark.type.name === MarkName.AssetLink)?.attrs },
|
@@ -27,7 +28,7 @@ const LinkModal = ({ onCancel, onSubmit }: LinkModalProps) => {
|
|
27
28
|
};
|
28
29
|
|
29
30
|
return (
|
30
|
-
<FormModal title="Link" onCancel={onCancel}>
|
31
|
+
<FormModal title="Link" icon={<InsertLinkRoundedIcon />} onCancel={onCancel}>
|
31
32
|
<LinkForm data={data} onSubmit={onSubmit} />
|
32
33
|
</FormModal>
|
33
34
|
);
|
@@ -7,7 +7,7 @@ import RemoveLinkButton from './RemoveLinkButton';
|
|
7
7
|
describe('RemoveLinkButton', () => {
|
8
8
|
it('Removes a link', async () => {
|
9
9
|
const { editor, getJsonContent } = await renderWithEditor(<RemoveLinkButton />, {
|
10
|
-
context: { matrix: { matrixDomain: 'my-matrix.squiz.net' } },
|
10
|
+
context: { editor: { matrix: { matrixDomain: 'my-matrix.squiz.net' } } },
|
11
11
|
content: {
|
12
12
|
type: 'doc',
|
13
13
|
content: [
|
@@ -17,7 +17,12 @@ describe('RemoveLinkButton', () => {
|
|
17
17
|
{
|
18
18
|
type: 'text',
|
19
19
|
text: 'Sample link',
|
20
|
-
marks: [
|
20
|
+
marks: [
|
21
|
+
{
|
22
|
+
type: 'assetLink',
|
23
|
+
attrs: { matrixAssetId: '123', matrixIdentifier: 'matrix-identifier', target: '_blank' },
|
24
|
+
},
|
25
|
+
],
|
21
26
|
},
|
22
27
|
{ type: 'text', text: ' with ' },
|
23
28
|
{
|
@@ -47,7 +52,7 @@ describe('RemoveLinkButton', () => {
|
|
47
52
|
|
48
53
|
it('Removes the link when clicking the keyboard shortcut', async () => {
|
49
54
|
const { elements, editor, getJsonContent } = await renderWithEditor(<RemoveLinkButton />, {
|
50
|
-
context: { matrix: { matrixDomain: 'my-matrix.squiz.net' } },
|
55
|
+
context: { editor: { matrix: { matrixDomain: 'my-matrix.squiz.net' } } },
|
51
56
|
content: {
|
52
57
|
type: 'doc',
|
53
58
|
content: [
|
@@ -57,7 +62,12 @@ describe('RemoveLinkButton', () => {
|
|
57
62
|
{
|
58
63
|
type: 'text',
|
59
64
|
text: 'Sample link',
|
60
|
-
marks: [
|
65
|
+
marks: [
|
66
|
+
{
|
67
|
+
type: 'assetLink',
|
68
|
+
attrs: { matrixAssetId: '123', matrixIdentifier: 'matrix-identifier', target: '_blank' },
|
69
|
+
},
|
70
|
+
],
|
61
71
|
},
|
62
72
|
{ type: 'text', text: ' with ' },
|
63
73
|
{
|
@@ -97,4 +107,37 @@ describe('RemoveLinkButton', () => {
|
|
97
107
|
// expect remove button to be enabled
|
98
108
|
expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).not.toBeDisabled();
|
99
109
|
});
|
110
|
+
|
111
|
+
it('Enables the Remove link button when asset link text is selected', async () => {
|
112
|
+
const { editor } = await renderWithEditor(<RemoveLinkButton />, {
|
113
|
+
context: { editor: { matrix: { matrixDomain: 'my-matrix.squiz.net' } } },
|
114
|
+
content: {
|
115
|
+
type: 'doc',
|
116
|
+
content: [
|
117
|
+
{
|
118
|
+
type: 'paragraph',
|
119
|
+
content: [
|
120
|
+
{
|
121
|
+
type: 'text',
|
122
|
+
text: 'Sample link',
|
123
|
+
marks: [
|
124
|
+
{
|
125
|
+
type: 'assetLink',
|
126
|
+
attrs: { matrixAssetId: '123', matrixIdentifier: 'matrix-identifier', target: '_blank' },
|
127
|
+
},
|
128
|
+
],
|
129
|
+
},
|
130
|
+
],
|
131
|
+
},
|
132
|
+
],
|
133
|
+
},
|
134
|
+
});
|
135
|
+
|
136
|
+
// expect remove button to be disabled
|
137
|
+
expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).toBeDisabled();
|
138
|
+
// jump to the middle of the link.
|
139
|
+
await act(() => editor.selectText(3));
|
140
|
+
// expect remove button to be enabled
|
141
|
+
expect(screen.getByRole('button', { name: 'Remove link (shift+cmd+K)' })).not.toBeDisabled();
|
142
|
+
});
|
100
143
|
});
|
@@ -2,13 +2,14 @@ import React, { useCallback } from 'react';
|
|
2
2
|
import { useChainedCommands, useActive, useKeymap } from '@remirror/react';
|
3
3
|
import Button from '../../../ui/Button/Button';
|
4
4
|
import LinkOffIcon from '@mui/icons-material/LinkOff';
|
5
|
+
import { AssetLinkExtension } from '../../../Extensions/LinkExtension/AssetLinkExtension';
|
5
6
|
import { LinkExtension } from '../../../Extensions/LinkExtension/LinkExtension';
|
6
7
|
import { LinkButtonProps } from './LinkButton';
|
7
8
|
|
8
9
|
const RemoveLinkButton = ({ inPopover = false }: LinkButtonProps) => {
|
9
10
|
const chain = useChainedCommands();
|
10
|
-
const active = useActive<LinkExtension>();
|
11
|
-
const disabled = !active.link();
|
11
|
+
const active = useActive<LinkExtension | AssetLinkExtension>();
|
12
|
+
const disabled = !active.link() && !active.assetLink();
|
12
13
|
|
13
14
|
const handleClick = () => {
|
14
15
|
chain.removeLink().removeAssetLink().focus().run();
|
@@ -42,12 +42,10 @@ export const createExtensions = (context: EditorContextOptions) => {
|
|
42
42
|
new ImageExtension(),
|
43
43
|
new ImageExtension({ preferPastedTextContent: false }),
|
44
44
|
new AssetImageExtension({
|
45
|
-
matrixIdentifier: context.matrix.matrixIdentifier,
|
46
45
|
matrixDomain: context.matrix.matrixDomain,
|
47
46
|
}),
|
48
47
|
new LinkExtension(),
|
49
48
|
new AssetLinkExtension({
|
50
|
-
matrixIdentifier: context.matrix.matrixIdentifier,
|
51
49
|
matrixDomain: context.matrix.matrixDomain,
|
52
50
|
}),
|
53
51
|
];
|
@@ -16,7 +16,6 @@ import { resolveMatrixAssetUrl } from '../../utils/resolveMatrixAssetUrl';
|
|
16
16
|
import { NodeName } from '../Extensions';
|
17
17
|
|
18
18
|
export type AssetImageOptions = {
|
19
|
-
matrixIdentifier?: string;
|
20
19
|
matrixDomain?: string;
|
21
20
|
};
|
22
21
|
|
@@ -28,7 +27,6 @@ export type AssetImageAttributes = {
|
|
28
27
|
|
29
28
|
@extension<AssetImageOptions>({
|
30
29
|
defaultOptions: {
|
31
|
-
matrixIdentifier: '',
|
32
30
|
matrixDomain: '',
|
33
31
|
},
|
34
32
|
defaultPriority: ExtensionPriority.High,
|
@@ -51,7 +49,7 @@ export class AssetImageExtension extends NodeExtension<AssetImageOptions> {
|
|
51
49
|
attrs: {
|
52
50
|
...extra.defaults(),
|
53
51
|
matrixAssetId: {},
|
54
|
-
matrixIdentifier: {
|
52
|
+
matrixIdentifier: {},
|
55
53
|
matrixDomain: { default: this.options.matrixDomain },
|
56
54
|
},
|
57
55
|
parseDOM: [
|
@@ -20,7 +20,6 @@ export type AssetLinkAttributes = {
|
|
20
20
|
};
|
21
21
|
|
22
22
|
export type AssetLinkOptions = {
|
23
|
-
matrixIdentifier?: string;
|
24
23
|
matrixDomain?: string;
|
25
24
|
defaultTarget?: LinkTarget;
|
26
25
|
supportedTargets?: LinkTarget[];
|
@@ -34,7 +33,6 @@ export type UpdateAssetLinkProps = {
|
|
34
33
|
|
35
34
|
@extension<AssetLinkOptions>({
|
36
35
|
defaultOptions: {
|
37
|
-
matrixIdentifier: '',
|
38
36
|
matrixDomain: '',
|
39
37
|
defaultTarget: LinkTarget.Self,
|
40
38
|
supportedTargets: [LinkTarget.Self, LinkTarget.Blank],
|
@@ -49,12 +47,12 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
|
|
49
47
|
createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
|
50
48
|
return {
|
51
49
|
inclusive: false,
|
52
|
-
excludes: MarkName.Link,
|
50
|
+
excludes: [this.name, MarkName.Link].join(' '),
|
53
51
|
...override,
|
54
52
|
attrs: {
|
55
53
|
...extra.defaults(),
|
56
54
|
matrixAssetId: {},
|
57
|
-
matrixIdentifier: {
|
55
|
+
matrixIdentifier: {},
|
58
56
|
matrixDomain: { default: this.options.matrixDomain },
|
59
57
|
target: { default: this.options.defaultTarget },
|
60
58
|
},
|
@@ -43,7 +43,7 @@ export class LinkExtension extends MarkExtension<LinkOptions> {
|
|
43
43
|
createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
|
44
44
|
return {
|
45
45
|
inclusive: false,
|
46
|
-
excludes: MarkName.AssetLink,
|
46
|
+
excludes: [this.name, MarkName.AssetLink].join(' '),
|
47
47
|
...override,
|
48
48
|
attrs: {
|
49
49
|
...extra.defaults(),
|
package/src/index.scss
CHANGED
package/src/types.ts
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
export type DeepPartial<T> =
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
1
|
+
export type DeepPartial<T> = {
|
2
|
+
[P in keyof T]?: T[P] extends Array<infer U>
|
3
|
+
? Array<DeepPartial<U>>
|
4
|
+
: T[P] extends ReadonlyArray<infer U>
|
5
|
+
? ReadonlyArray<DeepPartial<U>>
|
6
|
+
: DeepPartial<T[P]>;
|
7
|
+
};
|
@@ -0,0 +1,50 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
3
|
+
import React from 'react';
|
4
|
+
import { Checkbox } from './Checkbox';
|
5
|
+
import { LinkTarget } from '../../../Extensions/LinkExtension/common';
|
6
|
+
|
7
|
+
describe('Input', () => {
|
8
|
+
const mockOnChange = jest.fn();
|
9
|
+
|
10
|
+
const CheckboxComponent = ({ defaultChecked = false }: { defaultChecked?: boolean }) => {
|
11
|
+
return (
|
12
|
+
<Checkbox
|
13
|
+
label="This is a test checkbox"
|
14
|
+
onChange={mockOnChange}
|
15
|
+
defaultChecked={defaultChecked}
|
16
|
+
unchecked={'self' as LinkTarget}
|
17
|
+
checked={'_blank' as LinkTarget}
|
18
|
+
/>
|
19
|
+
);
|
20
|
+
};
|
21
|
+
|
22
|
+
it('Renders the checkbox label', () => {
|
23
|
+
render(<CheckboxComponent />);
|
24
|
+
// Check that the supplied label renders
|
25
|
+
const checkboxLabel = screen.getByText('This is a test checkbox');
|
26
|
+
expect(checkboxLabel).toBeInTheDocument();
|
27
|
+
});
|
28
|
+
|
29
|
+
it('Renders the default checkmark', () => {
|
30
|
+
render(<CheckboxComponent defaultChecked={true} />);
|
31
|
+
// Check that default value supplied renders
|
32
|
+
expect(screen.getByTestId('CheckRoundedIcon')).toBeInTheDocument();
|
33
|
+
});
|
34
|
+
|
35
|
+
it('Does not render the default checkmark', () => {
|
36
|
+
render(<CheckboxComponent defaultChecked={false} />);
|
37
|
+
expect(screen.queryByTestId('CheckRoundedIcon')).toBeFalsy();
|
38
|
+
});
|
39
|
+
|
40
|
+
it('Toggles checkbox when it is clicked', () => {
|
41
|
+
render(<CheckboxComponent />);
|
42
|
+
const checkbox = screen.getAllByRole('button')[0];
|
43
|
+
|
44
|
+
expect(checkbox).toBeTruthy();
|
45
|
+
fireEvent.click(checkbox);
|
46
|
+
|
47
|
+
expect(mockOnChange).toHaveBeenCalled();
|
48
|
+
expect(screen.getByTestId('CheckRoundedIcon')).toBeInTheDocument();
|
49
|
+
});
|
50
|
+
});
|
@@ -0,0 +1,49 @@
|
|
1
|
+
import React, { useEffect, useState } from 'react';
|
2
|
+
import CheckRoundedIcon from '@mui/icons-material/CheckRounded';
|
3
|
+
|
4
|
+
export type CheckboxProps<TChecked, TUnchecked> = {
|
5
|
+
label: string;
|
6
|
+
onChange: (value: TChecked | TUnchecked) => void;
|
7
|
+
defaultChecked?: boolean;
|
8
|
+
unchecked: TUnchecked;
|
9
|
+
checked: TChecked;
|
10
|
+
};
|
11
|
+
|
12
|
+
export const Checkbox = <TChecked, TUnchecked>({
|
13
|
+
label,
|
14
|
+
onChange,
|
15
|
+
defaultChecked = false,
|
16
|
+
unchecked,
|
17
|
+
checked,
|
18
|
+
}: CheckboxProps<TChecked, TUnchecked>) => {
|
19
|
+
const [toggled, setToggled] = useState<boolean>(defaultChecked);
|
20
|
+
|
21
|
+
useEffect(() => {
|
22
|
+
if (toggled) {
|
23
|
+
onChange(checked);
|
24
|
+
} else {
|
25
|
+
onChange(unchecked);
|
26
|
+
}
|
27
|
+
}, [toggled]);
|
28
|
+
|
29
|
+
const toggleCheckbox = () => setToggled(!toggled);
|
30
|
+
|
31
|
+
return (
|
32
|
+
<div className="squiz-fte-checkbox">
|
33
|
+
<button
|
34
|
+
type="button"
|
35
|
+
role="checkbox"
|
36
|
+
aria-label={label}
|
37
|
+
aria-checked={toggled}
|
38
|
+
className="checkbox"
|
39
|
+
onClick={toggleCheckbox}
|
40
|
+
>
|
41
|
+
{toggled && <CheckRoundedIcon />}
|
42
|
+
</button>
|
43
|
+
{/* Checkbox label as a button, acts as a secondary way to toggle */}
|
44
|
+
<button type="button" className="label" onClick={toggleCheckbox} tabIndex={-1}>
|
45
|
+
{label}
|
46
|
+
</button>
|
47
|
+
</div>
|
48
|
+
);
|
49
|
+
};
|
@@ -0,0 +1,26 @@
|
|
1
|
+
.squiz-fte-checkbox {
|
2
|
+
@apply text-gray-800;
|
3
|
+
font-size: 14px;
|
4
|
+
|
5
|
+
display: flex;
|
6
|
+
align-items: center;
|
7
|
+
margin-top: 0.75rem;
|
8
|
+
gap: 0.75rem;
|
9
|
+
|
10
|
+
.checkbox {
|
11
|
+
display: flex;
|
12
|
+
justify-content: center;
|
13
|
+
align-items: center;
|
14
|
+
|
15
|
+
width: 1.25rem;
|
16
|
+
height: 1.25rem;
|
17
|
+
background-color: #fff;
|
18
|
+
|
19
|
+
border: 2px solid #e0e0e0;
|
20
|
+
border-radius: 4px;
|
21
|
+
|
22
|
+
svg {
|
23
|
+
width: 100%;
|
24
|
+
}
|
25
|
+
}
|
26
|
+
}
|
@@ -1,27 +1,14 @@
|
|
1
1
|
import React, { ForwardedRef, forwardRef, InputHTMLAttributes } from 'react';
|
2
|
-
import
|
2
|
+
import { InputContainer, InputContainerProps } from '../InputContainer/InputContainer';
|
3
3
|
|
4
|
-
type InputProps = InputHTMLAttributes<HTMLInputElement> &
|
5
|
-
label?: string;
|
6
|
-
error?: string;
|
7
|
-
};
|
4
|
+
type InputProps = InputHTMLAttributes<HTMLInputElement> & Omit<InputContainerProps, 'children'>;
|
8
5
|
|
9
6
|
const InputInternal = (
|
10
7
|
{ name, label, type = 'text', error, required, ...rest }: InputProps,
|
11
8
|
ref: ForwardedRef<HTMLInputElement>,
|
12
9
|
) => {
|
13
10
|
return (
|
14
|
-
<
|
15
|
-
{label && (
|
16
|
-
<label htmlFor={name} className="squiz-fte-form-label">
|
17
|
-
{label}
|
18
|
-
</label>
|
19
|
-
)}
|
20
|
-
{required && (
|
21
|
-
<span className="text-gray-600" aria-label="Required field">
|
22
|
-
*
|
23
|
-
</span>
|
24
|
-
)}
|
11
|
+
<InputContainer name={name} label={label} error={error} required={required}>
|
25
12
|
<input
|
26
13
|
ref={ref}
|
27
14
|
id={name}
|
@@ -31,8 +18,7 @@ const InputInternal = (
|
|
31
18
|
className="squiz-fte-form-control"
|
32
19
|
{...rest}
|
33
20
|
/>
|
34
|
-
|
35
|
-
</div>
|
21
|
+
</InputContainer>
|
36
22
|
);
|
37
23
|
};
|
38
24
|
|
@@ -0,0 +1,18 @@
|
|
1
|
+
import '@testing-library/jest-dom';
|
2
|
+
import React from 'react';
|
3
|
+
import { render, screen } from '@testing-library/react';
|
4
|
+
import { InputContainer } from './InputContainer';
|
5
|
+
|
6
|
+
describe('InputContainer', () => {
|
7
|
+
it('Renders with expected content', () => {
|
8
|
+
render(
|
9
|
+
<InputContainer name="my-input" label="My input" error="Input is invalid" required={true}>
|
10
|
+
input element
|
11
|
+
</InputContainer>,
|
12
|
+
);
|
13
|
+
|
14
|
+
expect(screen.getByText('My input')).toHaveClass('squiz-fte-form-label');
|
15
|
+
expect(screen.getByText('input element')).toBeInTheDocument();
|
16
|
+
expect(screen.getByText('Input is invalid')).toHaveClass('squiz-fte-form-error');
|
17
|
+
});
|
18
|
+
});
|
@@ -0,0 +1,29 @@
|
|
1
|
+
import React, { ReactNode } from 'react';
|
2
|
+
import clsx from 'clsx';
|
3
|
+
|
4
|
+
export type InputContainerProps = {
|
5
|
+
name?: string;
|
6
|
+
label?: string;
|
7
|
+
error?: string;
|
8
|
+
required?: boolean;
|
9
|
+
children: ReactNode;
|
10
|
+
};
|
11
|
+
|
12
|
+
export const InputContainer = ({ name, label, error, required, children }: InputContainerProps) => {
|
13
|
+
return (
|
14
|
+
<div className={clsx(error && 'squiz-fte-invalid-form-field')}>
|
15
|
+
{label && (
|
16
|
+
<label htmlFor={name} className="squiz-fte-form-label">
|
17
|
+
{label}
|
18
|
+
</label>
|
19
|
+
)}
|
20
|
+
{label && required && (
|
21
|
+
<span className="text-gray-600" aria-label="Required field">
|
22
|
+
*
|
23
|
+
</span>
|
24
|
+
)}
|
25
|
+
{children}
|
26
|
+
{error && <div className="squiz-fte-form-error">{error}</div>}
|
27
|
+
</div>
|
28
|
+
);
|
29
|
+
};
|