@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.
Files changed (36) hide show
  1. package/lib/EditorToolbar/Tools/Image/Form/ImageForm.js +9 -14
  2. package/lib/EditorToolbar/Tools/Image/ImageModal.js +3 -2
  3. package/lib/EditorToolbar/Tools/Link/Form/LinkForm.js +12 -20
  4. package/lib/EditorToolbar/Tools/Link/LinkModal.js +3 -2
  5. package/lib/Extensions/LinkExtension/AssetLinkExtension.js +1 -1
  6. package/lib/Extensions/LinkExtension/LinkExtension.js +1 -1
  7. package/lib/index.css +77 -2
  8. package/lib/ui/Fields/Checkbox/Checkbox.d.ts +8 -0
  9. package/lib/ui/Fields/Checkbox/Checkbox.js +47 -0
  10. package/lib/ui/Modal/Modal.d.ts +1 -0
  11. package/lib/ui/Modal/Modal.js +3 -2
  12. package/lib/ui/Tabs/Tabs.d.ts +10 -0
  13. package/lib/ui/Tabs/Tabs.js +46 -0
  14. package/package.json +2 -2
  15. package/src/EditorToolbar/Tools/Image/Form/ImageForm.spec.tsx +2 -2
  16. package/src/EditorToolbar/Tools/Image/Form/ImageForm.tsx +12 -24
  17. package/src/EditorToolbar/Tools/Image/ImageButton.spec.tsx +11 -10
  18. package/src/EditorToolbar/Tools/Image/ImageModal.spec.tsx +1 -0
  19. package/src/EditorToolbar/Tools/Image/ImageModal.tsx +3 -2
  20. package/src/EditorToolbar/Tools/Link/Form/LinkForm.spec.tsx +10 -11
  21. package/src/EditorToolbar/Tools/Link/Form/LinkForm.tsx +21 -37
  22. package/src/EditorToolbar/Tools/Link/LinkButton.spec.tsx +8 -8
  23. package/src/EditorToolbar/Tools/Link/LinkModal.tsx +3 -2
  24. package/src/Extensions/LinkExtension/AssetLinkExtension.ts +1 -1
  25. package/src/Extensions/LinkExtension/LinkExtension.ts +1 -1
  26. package/src/index.scss +1 -0
  27. package/src/ui/Fields/Checkbox/Checkbox.spec.tsx +50 -0
  28. package/src/ui/Fields/Checkbox/Checkbox.tsx +49 -0
  29. package/src/ui/Fields/Checkbox/_checkbox.scss +26 -0
  30. package/src/ui/Modal/FormModal.spec.tsx +2 -1
  31. package/src/ui/Modal/Modal.spec.tsx +15 -7
  32. package/src/ui/Modal/Modal.tsx +4 -2
  33. package/src/ui/Tabs/Tabs.spec.tsx +44 -0
  34. package/src/ui/Tabs/Tabs.tsx +41 -0
  35. package/lib/ui/Fields/Select/Select.d.ts +0 -12
  36. 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, select } from '../../../../tests';
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('Source')).toHaveValue('');
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
- expect(await screen.findByText('Alternative description is required')).toBeInTheDocument();
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
- select(screen.getByLabelText('Type'), 'Asset image');
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
- select(screen.getByLabelText('Type'), 'Asset image');
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
- select(screen.getByLabelText('Type'), 'Asset image');
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(2);
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.AssetImage ? NodeName.AssetImage : NodeName.Image,
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(screen.getByLabelText('Type')).toHaveTextContent('Link to URL');
31
- expect(screen.getByLabelText('URL')).toHaveValue('');
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(screen.getByLabelText('Title')).toHaveValue('');
34
- expect(screen.getByLabelText('Target')).toHaveTextContent('Current window');
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(screen.getByLabelText('Type')).toHaveTextContent('Link to URL');
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(screen.getByLabelText('Target')).toHaveTextContent('New window');
46
- expect(document.querySelectorAll('label')).toHaveLength(5);
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(screen.getByLabelText('Type')).toHaveTextContent('Link to asset');
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(screen.getByLabelText('Target')).toHaveTextContent('New window');
56
- expect(document.querySelectorAll('label')).toHaveLength(4);
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 { Select, SelectOptions } from '../../../../ui/Fields/Select/Select';
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: SelectOptions = {
29
- [MarkName.Link]: { label: 'Link to URL' },
30
- [MarkName.AssetLink]: { label: 'Link to asset' },
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.Link;
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-2">
54
- <Select
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-0')}>
106
- <Select
107
- name="link.target"
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-0')}>
151
- <Select
152
- name="assetLink.target"
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, select } from '../../../../tests';
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
- select(screen.getByLabelText('Target'), 'New window');
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
- select(screen.getByLabelText('Type'), 'Link to asset');
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
- select(screen.getByLabelText('Type'), 'Link to asset');
294
+ fireEvent.click(screen.getByRole('button', { name: 'From source' }));
294
295
  fireEvent.change(screen.getByLabelText('Asset ID'), { target: { value: '123' } });
295
- select(screen.getByLabelText('Target'), 'New window');
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
- select(screen.getByLabelText('Type'), 'Link to asset');
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(3);
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.AssetLink ? MarkName.AssetLink : MarkName.Link,
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
@@ -15,5 +15,6 @@
15
15
  @import './ui/Button/button';
16
16
  @import './ui/ToolbarDropdown/toolbar-dropdown';
17
17
  @import './ui/ToolbarDropdownButton/toolbar-dropdown-button';
18
+ @import './ui/Fields/Checkbox/checkbox';
18
19
 
19
20
  @import './ui/Modal/modal';
@@ -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" />
@@ -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-2">
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"