@squiz/formatted-text-editor 1.33.1-alpha.1 → 1.33.1-alpha.3
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/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +9 -14
- package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -2
- package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +12 -20
- package/lib/EditorToolbar/Tools/Link/LinkModal.js +3 -2
- package/lib/Extensions/LinkExtension/AssetLinkExtension.js +1 -1
- package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
- package/lib/index.css +77 -2
- package/lib/ui/Fields/Checkbox/Checkbox.d.ts +8 -0
- package/lib/ui/Fields/Checkbox/Checkbox.js +47 -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/package.json +2 -2
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +2 -2
- package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +12 -24
- package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +11 -10
- 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 +10 -11
- package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +21 -37
- package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +8 -8
- package/src/EditorToolbar/Tools/Link/LinkModal.tsx +3 -2
- package/src/Extensions/LinkExtension/AssetLinkExtension.ts +1 -1
- package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
- package/src/index.scss +1 -0
- 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/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/lib/ui/Fields/Select/Select.d.ts +0 -12
- package/lib/ui/Fields/Select/Select.js +0 -53
@@ -2,7 +2,7 @@ import '@testing-library/jest-dom';
|
|
2
2
|
import { screen, fireEvent, waitForElementToBeRemoved, act, waitFor } from '@testing-library/react';
|
3
3
|
import { NodeSelection } from 'prosemirror-state';
|
4
4
|
import React from 'react';
|
5
|
-
import { renderWithEditor
|
5
|
+
import { renderWithEditor } from '../../../../tests';
|
6
6
|
import ImageButton from './ImageButton';
|
7
7
|
import { getImageSize } from 'react-image-size';
|
8
8
|
|
@@ -12,6 +12,7 @@ describe('ImageButton', () => {
|
|
12
12
|
const openModal = async () => {
|
13
13
|
fireEvent.click(screen.getByRole('button', { name: 'Image (cmd+L)' }));
|
14
14
|
await screen.findByRole('button', { name: 'Apply' });
|
15
|
+
fireEvent.click(screen.getByRole('button', { name: 'From URL' }));
|
15
16
|
};
|
16
17
|
|
17
18
|
beforeEach(() => {
|
@@ -35,7 +36,7 @@ describe('ImageButton', () => {
|
|
35
36
|
|
36
37
|
// verify the modal opens
|
37
38
|
await act(() => editor.selectText(2));
|
38
|
-
expect(await screen.findByLabelText('
|
39
|
+
expect(await screen.findByLabelText('Asset ID')).toHaveValue('');
|
39
40
|
});
|
40
41
|
|
41
42
|
it('Adds a new image', async () => {
|
@@ -215,7 +216,10 @@ describe('ImageButton', () => {
|
|
215
216
|
fireEvent.change(screen.getByLabelText('Source'), { target: { value: 'https://httpcats.com/529.jpg' } });
|
216
217
|
fireEvent.change(screen.getByLabelText('Alternative description'), { target: { value: '' } });
|
217
218
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
218
|
-
|
219
|
+
|
220
|
+
await waitFor(() => {
|
221
|
+
expect(document.querySelector('img[src="https://httpcats.com/529.jpg"]')).toBeInTheDocument();
|
222
|
+
});
|
219
223
|
});
|
220
224
|
|
221
225
|
it('Adds a new image with no width or height text', async () => {
|
@@ -246,7 +250,7 @@ describe('ImageButton', () => {
|
|
246
250
|
|
247
251
|
// open the modal and add an image.
|
248
252
|
await openModal();
|
249
|
-
|
253
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
250
254
|
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
|
251
255
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
252
256
|
|
@@ -282,7 +286,7 @@ describe('ImageButton', () => {
|
|
282
286
|
await act(() => editor.selectText(new NodeSelection(editor.state.doc.resolve(6))));
|
283
287
|
|
284
288
|
await openModal();
|
285
|
-
|
289
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
286
290
|
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '100' } });
|
287
291
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
288
292
|
|
@@ -317,7 +321,7 @@ describe('ImageButton', () => {
|
|
317
321
|
await renderWithEditor(<ImageButton />, { context: { matrix: { resolveMatrixAsset } } });
|
318
322
|
|
319
323
|
await openModal();
|
320
|
-
|
324
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
321
325
|
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: assetId } });
|
322
326
|
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
323
327
|
|
@@ -332,11 +336,8 @@ describe('ImageButton', () => {
|
|
332
336
|
await act(async () => {
|
333
337
|
fireEvent.change(screen.getByLabelText('Source'), { target: { value: ' ' } });
|
334
338
|
});
|
335
|
-
await act(async () => {
|
336
|
-
fireEvent.change(screen.getByLabelText('Alternative description'), { target: { value: ' ' } });
|
337
|
-
});
|
338
339
|
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
339
340
|
|
340
|
-
expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(
|
341
|
+
expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(1);
|
341
342
|
});
|
342
343
|
});
|
@@ -13,6 +13,7 @@ const mockSubmitFunction = jest.fn();
|
|
13
13
|
const mockCancelFunction = jest.fn();
|
14
14
|
const setup = async () => {
|
15
15
|
const utils = await renderWithEditor(<ImageModal onCancel={mockCancelFunction} onSubmit={mockSubmitFunction} />);
|
16
|
+
fireEvent.click(screen.getByRole('button', { name: 'From URL' }));
|
16
17
|
const sourceInput = screen.getByRole('textbox', { name: /source/i }) as HTMLInputElement;
|
17
18
|
const altInput = screen.getByRole('textbox', { name: /alt/i }) as HTMLInputElement;
|
18
19
|
const widthInput = screen.getByRole('spinbutton', { name: /Width/i }) as HTMLInputElement;
|
@@ -1,6 +1,7 @@
|
|
1
1
|
import ImageForm, { ImageFormData } from './Form/ImageForm';
|
2
2
|
import React from 'react';
|
3
3
|
import { useCurrentSelection } from '@remirror/react';
|
4
|
+
import ImageRoundedIcon from '@mui/icons-material/ImageRounded';
|
4
5
|
import FormModal from '../../../ui/Modal/FormModal';
|
5
6
|
import { SubmitHandler } from 'react-hook-form';
|
6
7
|
import { NodeSelection } from 'prosemirror-state';
|
@@ -16,13 +17,13 @@ const ImageModal = ({ onCancel, onSubmit }: ImageModalProps) => {
|
|
16
17
|
const currentImage = selection?.node;
|
17
18
|
const currentImageAttrs = { ...currentImage?.attrs };
|
18
19
|
const formData = {
|
19
|
-
imageType: currentImage?.type.name === NodeName.
|
20
|
+
imageType: currentImage?.type.name === NodeName.Image ? NodeName.Image : NodeName.AssetImage,
|
20
21
|
image: currentImage?.type?.name === NodeName.Image ? currentImageAttrs : {},
|
21
22
|
assetImage: currentImage?.type?.name === NodeName.AssetImage ? currentImageAttrs : {},
|
22
23
|
};
|
23
24
|
|
24
25
|
return (
|
25
|
-
<FormModal title="Image" onCancel={onCancel}>
|
26
|
+
<FormModal title="Image" icon={<ImageRoundedIcon />} onCancel={onCancel}>
|
26
27
|
<ImageForm data={formData} onSubmit={onSubmit} />
|
27
28
|
</FormModal>
|
28
29
|
);
|
@@ -27,32 +27,31 @@ describe('Link Form', () => {
|
|
27
27
|
it('Renders the form with expected default values when no data is provided', () => {
|
28
28
|
render(<LinkForm onSubmit={handleSubmit} />);
|
29
29
|
|
30
|
-
expect(
|
31
|
-
expect(screen.getByLabelText('
|
30
|
+
expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
|
31
|
+
expect(screen.getByLabelText('Asset ID')).toHaveValue('');
|
32
32
|
expect(screen.getByLabelText('Text')).toHaveValue('');
|
33
|
-
expect(
|
34
|
-
expect(
|
35
|
-
expect(document.querySelectorAll('label')).toHaveLength(5);
|
33
|
+
expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
|
34
|
+
expect(document.querySelectorAll('label')).toHaveLength(2);
|
36
35
|
});
|
37
36
|
|
38
37
|
it('Renders the form with the expected fields for arbitrary links', () => {
|
39
38
|
render(<LinkForm data={data} onSubmit={handleSubmit} />);
|
40
39
|
|
41
|
-
expect(
|
40
|
+
expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From URL');
|
42
41
|
expect(screen.getByLabelText('URL')).toHaveValue('https://www.squiz.net/link-form');
|
43
42
|
expect(screen.getByLabelText('Text')).toHaveValue('Link text');
|
44
43
|
expect(screen.getByLabelText('Title')).toHaveValue('Link title');
|
45
|
-
expect(
|
46
|
-
expect(document.querySelectorAll('label')).toHaveLength(
|
44
|
+
expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
|
45
|
+
expect(document.querySelectorAll('label')).toHaveLength(3);
|
47
46
|
});
|
48
47
|
|
49
48
|
it('Renders the form with the expected fields for asset links', () => {
|
50
49
|
render(<LinkForm data={{ ...data, linkType: MarkName.AssetLink }} onSubmit={handleSubmit} />);
|
51
50
|
|
52
|
-
expect(
|
51
|
+
expect(document.querySelector('div[data-headlessui-state="selected"]')).toHaveTextContent('From source');
|
53
52
|
expect(screen.getByLabelText('Asset ID')).toHaveValue('100');
|
54
53
|
expect(screen.getByLabelText('Text')).toHaveValue('Link text');
|
55
|
-
expect(
|
56
|
-
expect(document.querySelectorAll('label')).toHaveLength(
|
54
|
+
expect(document.querySelector('div.squiz-fte-checkbox')).toHaveTextContent('Open link in new window');
|
55
|
+
expect(document.querySelectorAll('label')).toHaveLength(2);
|
57
56
|
});
|
58
57
|
});
|
@@ -3,7 +3,7 @@ import clsx from 'clsx';
|
|
3
3
|
import { SubmitHandler, useForm } from 'react-hook-form';
|
4
4
|
import { FromToProps } from 'remirror';
|
5
5
|
import { Input } from '../../../../ui/Fields/Input/Input';
|
6
|
-
import {
|
6
|
+
import { Checkbox } from '../../../../ui/Fields/Checkbox/Checkbox';
|
7
7
|
import { UpdateLinkProps } from '../../../../Extensions/LinkExtension/LinkExtension';
|
8
8
|
import { UpdateAssetLinkProps } from '../../../../Extensions/LinkExtension/AssetLinkExtension';
|
9
9
|
import { LinkTarget } from '../../../../Extensions/LinkExtension/common';
|
@@ -11,6 +11,7 @@ import { EditorContext } from '../../../../Editor/EditorContext';
|
|
11
11
|
import { MarkName } from '../../../../Extensions/Extensions';
|
12
12
|
import { DeepPartial } from '../../../../types';
|
13
13
|
import { noEmptySpacesValidation } from '../../../../utils/validation';
|
14
|
+
import { TabOptions, Tabs } from '../../../../ui/Tabs/Tabs';
|
14
15
|
|
15
16
|
export type LinkFormData = {
|
16
17
|
linkType: MarkName;
|
@@ -25,14 +26,9 @@ export type FormProps = {
|
|
25
26
|
onSubmit: SubmitHandler<LinkFormData>;
|
26
27
|
};
|
27
28
|
|
28
|
-
const linkTypeOptions:
|
29
|
-
[MarkName.
|
30
|
-
[MarkName.
|
31
|
-
};
|
32
|
-
|
33
|
-
const targetOptions: SelectOptions = {
|
34
|
-
[LinkTarget.Self]: { label: 'Current window' },
|
35
|
-
[LinkTarget.Blank]: { label: 'New window' },
|
29
|
+
const linkTypeOptions: TabOptions = {
|
30
|
+
[MarkName.AssetLink]: { label: 'From source' },
|
31
|
+
[MarkName.Link]: { label: 'From URL' },
|
36
32
|
};
|
37
33
|
|
38
34
|
export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
@@ -46,14 +42,12 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
46
42
|
} = useForm<LinkFormData>({
|
47
43
|
defaultValues: data,
|
48
44
|
});
|
49
|
-
const linkType = watch('linkType') || MarkName.
|
45
|
+
const linkType = watch('linkType') || MarkName.AssetLink;
|
50
46
|
|
51
47
|
return (
|
52
48
|
<form className="squiz-fte-form" onSubmit={handleSubmit(onSubmit)}>
|
53
|
-
<div className="squiz-fte-form-group mb-
|
54
|
-
<
|
55
|
-
name="linkType"
|
56
|
-
label="Type"
|
49
|
+
<div className="squiz-fte-form-group mb-4">
|
50
|
+
<Tabs
|
57
51
|
value={linkType}
|
58
52
|
options={linkTypeOptions}
|
59
53
|
onChange={(value) => setValue('linkType', value as MarkName)}
|
@@ -90,25 +84,15 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
90
84
|
/>
|
91
85
|
</div>
|
92
86
|
<div className={clsx('squiz-fte-form-group mb-2')}>
|
93
|
-
<Input
|
94
|
-
label="Title"
|
95
|
-
required
|
96
|
-
error={errors?.link?.title?.message}
|
97
|
-
{...register('link.title', {
|
98
|
-
required: 'Title is required',
|
99
|
-
validate: {
|
100
|
-
noEmptySpaces: noEmptySpacesValidation,
|
101
|
-
},
|
102
|
-
})}
|
103
|
-
/>
|
87
|
+
<Input label="Title" error={errors?.link?.title?.message} {...register('link.title')} />
|
104
88
|
</div>
|
105
|
-
<div className={clsx('squiz-fte-form-group mb-
|
106
|
-
<
|
107
|
-
|
108
|
-
label="Target"
|
109
|
-
value={data?.link?.target || '_self'}
|
110
|
-
options={targetOptions}
|
89
|
+
<div className={clsx('squiz-fte-form-group mb-2')}>
|
90
|
+
<Checkbox
|
91
|
+
label="Open link in new window"
|
111
92
|
onChange={(value) => setValue('link.target', value as LinkTarget)}
|
93
|
+
defaultChecked={data?.link?.target === LinkTarget.Blank}
|
94
|
+
unchecked={LinkTarget.Self}
|
95
|
+
checked={LinkTarget.Blank}
|
112
96
|
/>
|
113
97
|
</div>
|
114
98
|
</>
|
@@ -147,13 +131,13 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
|
|
147
131
|
})}
|
148
132
|
/>
|
149
133
|
</div>
|
150
|
-
<div className={clsx('squiz-fte-form-group mb-
|
151
|
-
<
|
152
|
-
|
153
|
-
label="Target"
|
154
|
-
value={data?.assetLink?.target || '_self'}
|
155
|
-
options={targetOptions}
|
134
|
+
<div className={clsx('squiz-fte-form-group mb-2')}>
|
135
|
+
<Checkbox
|
136
|
+
label="Open link in new window"
|
156
137
|
onChange={(value) => setValue('assetLink.target', value as LinkTarget)}
|
138
|
+
defaultChecked={data?.assetLink?.target === '_blank'}
|
139
|
+
unchecked={'_self' as LinkTarget}
|
140
|
+
checked={'_blank' as LinkTarget}
|
157
141
|
/>
|
158
142
|
</div>
|
159
143
|
</>
|
@@ -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 } 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' }));
|
@@ -246,7 +247,7 @@ describe('LinkButton', () => {
|
|
246
247
|
});
|
247
248
|
|
248
249
|
await openModal();
|
249
|
-
|
250
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
250
251
|
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '123' } });
|
251
252
|
fireEvent.change(screen.getByLabelText('Text'), { target: { value: 'Link text' } });
|
252
253
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
@@ -290,9 +291,9 @@ describe('LinkButton', () => {
|
|
290
291
|
await act(() => editor.selectText(5));
|
291
292
|
|
292
293
|
await openModal();
|
293
|
-
|
294
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
294
295
|
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '123' } });
|
295
|
-
|
296
|
+
fireEvent.click(document.querySelector('div.squiz-fte-checkbox button') as HTMLButtonElement);
|
296
297
|
fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
|
297
298
|
|
298
299
|
await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
|
@@ -332,7 +333,7 @@ describe('LinkButton', () => {
|
|
332
333
|
await renderWithEditor(<LinkButton />, { context: { matrix: { resolveMatrixAsset } } });
|
333
334
|
|
334
335
|
await openModal();
|
335
|
-
|
336
|
+
fireEvent.click(screen.getByRole('button', { name: 'From source' }));
|
336
337
|
fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: 'invalid-asset-id' } });
|
337
338
|
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
338
339
|
|
@@ -351,7 +352,6 @@ describe('LinkButton', () => {
|
|
351
352
|
|
352
353
|
expect(screen.getByText('URL is required')).toBeInTheDocument();
|
353
354
|
expect(screen.getByText('Text is required')).toBeInTheDocument();
|
354
|
-
expect(screen.getByText('Title is required')).toBeInTheDocument();
|
355
355
|
});
|
356
356
|
|
357
357
|
it('Shows an error if the field value is just an empty space', async () => {
|
@@ -363,6 +363,6 @@ describe('LinkButton', () => {
|
|
363
363
|
fireEvent.change(screen.getByLabelText('Title'), { target: { value: ' ' } });
|
364
364
|
await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
|
365
365
|
|
366
|
-
expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(
|
366
|
+
expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(2);
|
367
367
|
});
|
368
368
|
});
|
@@ -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
|
);
|
@@ -49,7 +49,7 @@ export class AssetLinkExtension extends MarkExtension<AssetLinkOptions> {
|
|
49
49
|
createMarkSpec(extra: ApplySchemaAttributes, override: MarkSpecOverride): MarkExtensionSpec {
|
50
50
|
return {
|
51
51
|
inclusive: false,
|
52
|
-
excludes: MarkName.Link,
|
52
|
+
excludes: [this.name, MarkName.Link].join(' '),
|
53
53
|
...override,
|
54
54
|
attrs: {
|
55
55
|
...extra.defaults(),
|
@@ -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
@@ -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}>
|
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,5 +1,6 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
2
|
import { fireEvent, render, screen } from '@testing-library/react';
|
3
|
+
import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
|
3
4
|
import React from 'react';
|
4
5
|
import FormModal from './FormModal';
|
5
6
|
|
@@ -8,7 +9,7 @@ describe('FormModal', () => {
|
|
8
9
|
const handleSubmit = jest.fn();
|
9
10
|
|
10
11
|
render(
|
11
|
-
<FormModal title="Modal title" onCancel={jest.fn()}>
|
12
|
+
<FormModal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={jest.fn()}>
|
12
13
|
<form onSubmit={handleSubmit}></form>
|
13
14
|
</FormModal>,
|
14
15
|
);
|
@@ -1,5 +1,6 @@
|
|
1
1
|
import '@testing-library/jest-dom';
|
2
2
|
import { render, screen, fireEvent } from '@testing-library/react';
|
3
|
+
import InsertLinkRoundedIcon from '@mui/icons-material/InsertLinkRounded';
|
3
4
|
import React from 'react';
|
4
5
|
import Modal from './Modal';
|
5
6
|
import { Select } from '../Fields/Select/Select';
|
@@ -11,7 +12,7 @@ describe('Modal', () => {
|
|
11
12
|
|
12
13
|
const ModalComponent = () => {
|
13
14
|
return (
|
14
|
-
<Modal title="Modal title" onCancel={mockOnCancel}>
|
15
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel}>
|
15
16
|
<div>I am a child in the modal</div>
|
16
17
|
</Modal>
|
17
18
|
);
|
@@ -26,6 +27,13 @@ describe('Modal', () => {
|
|
26
27
|
expect(modalHeading).toBeInTheDocument();
|
27
28
|
});
|
28
29
|
|
30
|
+
it('Renders the modal image', () => {
|
31
|
+
render(<ModalComponent />);
|
32
|
+
// Check that the modal image displays
|
33
|
+
const modalImage = screen.getByTestId('InsertLinkRoundedIcon');
|
34
|
+
expect(modalImage).toBeInTheDocument();
|
35
|
+
});
|
36
|
+
|
29
37
|
it('Renders the child', () => {
|
30
38
|
render(<ModalComponent />);
|
31
39
|
// Check that the modal heading displays
|
@@ -52,7 +60,7 @@ describe('Modal', () => {
|
|
52
60
|
|
53
61
|
it('Renders the submit button if there is a submit function supplied', () => {
|
54
62
|
render(
|
55
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
63
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
56
64
|
<div>I am a child in the modal</div>
|
57
65
|
</Modal>,
|
58
66
|
);
|
@@ -63,7 +71,7 @@ describe('Modal', () => {
|
|
63
71
|
|
64
72
|
it('Checks that the submit function fires if you click on the submit button', () => {
|
65
73
|
render(
|
66
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
74
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
67
75
|
<div>I am a child in the modal</div>
|
68
76
|
</Modal>,
|
69
77
|
);
|
@@ -77,7 +85,7 @@ describe('Modal', () => {
|
|
77
85
|
|
78
86
|
it('Calls the onSubmit handler when the enter key is pressed', () => {
|
79
87
|
render(
|
80
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
88
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
81
89
|
<div>Modal content</div>
|
82
90
|
</Modal>,
|
83
91
|
);
|
@@ -89,7 +97,7 @@ describe('Modal', () => {
|
|
89
97
|
|
90
98
|
it('Calls the onCancel handler when the escape key is pressed', () => {
|
91
99
|
render(
|
92
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
100
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
93
101
|
<div>Modal content</div>
|
94
102
|
</Modal>,
|
95
103
|
);
|
@@ -101,7 +109,7 @@ describe('Modal', () => {
|
|
101
109
|
|
102
110
|
it('Auto-focuses on the first non-hidden input on mount', () => {
|
103
111
|
render(
|
104
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
112
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
105
113
|
<>
|
106
114
|
<input id="hidden-input" type="hidden" />
|
107
115
|
<label htmlFor="my-input">My input</label>
|
@@ -115,7 +123,7 @@ describe('Modal', () => {
|
|
115
123
|
|
116
124
|
it('Auto-focuses on the first select field on mount', () => {
|
117
125
|
render(
|
118
|
-
<Modal title="Modal title" onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
126
|
+
<Modal title="Modal title" icon={<InsertLinkRoundedIcon />} onCancel={mockOnCancel} onSubmit={mockOnSubmit}>
|
119
127
|
<>
|
120
128
|
<Select label="Dropdown" name="select" options={{}} />
|
121
129
|
<Input label="Input" name="input" />
|
package/src/ui/Modal/Modal.tsx
CHANGED
@@ -6,6 +6,7 @@ import clsx from 'clsx';
|
|
6
6
|
|
7
7
|
export type ModalProps = {
|
8
8
|
title: string;
|
9
|
+
icon: ReactElement;
|
9
10
|
children: ReactElement;
|
10
11
|
onCancel: () => void;
|
11
12
|
onSubmit?: () => void;
|
@@ -13,7 +14,7 @@ export type ModalProps = {
|
|
13
14
|
};
|
14
15
|
|
15
16
|
const Modal = (
|
16
|
-
{ children, title, onCancel, onSubmit, className }: ModalProps,
|
17
|
+
{ children, title, icon, onCancel, onSubmit, className }: ModalProps,
|
17
18
|
ref: ForwardedRef<HTMLDivElement>,
|
18
19
|
): ReactElement => {
|
19
20
|
const content = useRef<HTMLDivElement>(null);
|
@@ -55,7 +56,8 @@ const Modal = (
|
|
55
56
|
<div ref={ref} className={clsx('squiz-fte-modal-wrapper', className)} tabIndex={-1}>
|
56
57
|
<div className="w-modal-sm my-6 mx-auto">
|
57
58
|
<div className="squiz-fte-modal">
|
58
|
-
<div className="squiz-fte-modal-header p-6 pb-
|
59
|
+
<div className="squiz-fte-modal-header p-6 pb-4">
|
60
|
+
<div className="squiz-fte-modal-header-icon mr-1.5 mt-[-1px]">{icon}</div>
|
59
61
|
<h2 className="font-semibold text-gray-900 text-heading-2">{title}</h2>
|
60
62
|
<button
|
61
63
|
type="button"
|