@squiz/formatted-text-editor 1.22.1-alpha.6 → 1.22.1-alpha.8

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.
@@ -37,11 +37,11 @@ const clsx_1 = __importDefault(require("clsx"));
37
37
  const Extensions_1 = require("../../../../Extensions/Extensions");
38
38
  const Select_1 = require("../../../../ui/Fields/Select/Select");
39
39
  const EditorContext_1 = require("../../../../Editor/EditorContext");
40
+ const validation_1 = require("../../../../utils/validation");
40
41
  const imageTypeOptions = {
41
42
  [Extensions_1.NodeName.Image]: { label: 'External image' },
42
43
  [Extensions_1.NodeName.AssetImage]: { label: 'Asset image' },
43
44
  };
44
- const regexDataURI = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)$/i;
45
45
  const ImageForm = ({ data, onSubmit }) => {
46
46
  const { register, handleSubmit, setValue, watch, formState: { errors }, } = (0, react_hook_form_1.useForm)({
47
47
  defaultValues: data,
@@ -97,17 +97,23 @@ const ImageForm = ({ data, onSubmit }) => {
97
97
  required: 'Source is required',
98
98
  validate: {
99
99
  isValidImage: async (value) => {
100
- if (value && regexDataURI.test(value)) {
100
+ if (value && validation_1.regexDataURI.test(value)) {
101
101
  return 'Must not be a data URI';
102
102
  }
103
103
  if (value && (await validateIsNotImage(value))) {
104
104
  return 'Must be a valid image URL';
105
105
  }
106
106
  },
107
+ noEmptySpaces: validation_1.noEmptySpacesValidation,
107
108
  },
108
109
  }) })),
109
110
  react_1.default.createElement("div", { className: "squiz-fte-form-group mb-2" },
110
- react_1.default.createElement(Input_1.Input, { label: "Alternative description", required: true, error: errors?.image?.alt?.message, ...register('image.alt', { required: 'Alternative description is required' }) })),
111
+ react_1.default.createElement(Input_1.Input, { label: "Alternative description", required: true, error: errors?.image?.alt?.message, ...register('image.alt', {
112
+ required: 'Alternative description is required',
113
+ validate: {
114
+ noEmptySpaces: validation_1.noEmptySpacesValidation,
115
+ },
116
+ }) })),
111
117
  react_1.default.createElement("div", { className: "flex flex-row" },
112
118
  react_1.default.createElement("div", { className: "squiz-fte-form-group mb-2" },
113
119
  react_1.default.createElement(Input_1.Input, { label: "Width", type: "number", required: true, error: errors?.image?.width?.message, ...register('image.width', {
@@ -146,6 +152,7 @@ const ImageForm = ({ data, onSubmit }) => {
146
152
  return 'Asset ID is invalid or not an image';
147
153
  }
148
154
  },
155
+ noEmptySpaces: validation_1.noEmptySpacesValidation,
149
156
  },
150
157
  }) }))))));
151
158
  };
@@ -35,6 +35,7 @@ const Select_1 = require("../../../../ui/Fields/Select/Select");
35
35
  const common_1 = require("../../../../Extensions/LinkExtension/common");
36
36
  const EditorContext_1 = require("../../../../Editor/EditorContext");
37
37
  const Extensions_1 = require("../../../../Extensions/Extensions");
38
+ const validation_1 = require("../../../../utils/validation");
38
39
  const linkTypeOptions = {
39
40
  [Extensions_1.MarkName.Link]: { label: 'Link to URL' },
40
41
  [Extensions_1.MarkName.AssetLink]: { label: 'Link to asset' },
@@ -54,27 +55,49 @@ const LinkForm = ({ data, onSubmit }) => {
54
55
  react_1.default.createElement(Select_1.Select, { name: "linkType", label: "Type", value: linkType, options: linkTypeOptions, onChange: (value) => setValue('linkType', value) })),
55
56
  linkType === Extensions_1.MarkName.Link && (react_1.default.createElement(react_1.default.Fragment, null,
56
57
  react_1.default.createElement("div", { className: (0, clsx_1.default)('squiz-fte-form-group mb-2') },
57
- react_1.default.createElement(Input_1.Input, { label: "URL", ...register('link.href') })),
58
+ react_1.default.createElement(Input_1.Input, { label: "URL", required: true, error: errors?.link?.href?.message, ...register('link.href', {
59
+ required: 'URL is required',
60
+ validate: {
61
+ noEmptySpaces: validation_1.noEmptySpacesValidation,
62
+ },
63
+ }) })),
58
64
  react_1.default.createElement("div", { className: (0, clsx_1.default)('squiz-fte-form-group mb-2') },
59
- react_1.default.createElement(Input_1.Input, { label: "Text", ...register('text') })),
65
+ react_1.default.createElement(Input_1.Input, { label: "Text", required: true, error: errors?.text?.message, ...register('text', {
66
+ required: 'Text is required',
67
+ validate: {
68
+ noEmptySpaces: validation_1.noEmptySpacesValidation,
69
+ },
70
+ }) })),
60
71
  react_1.default.createElement("div", { className: (0, clsx_1.default)('squiz-fte-form-group mb-2') },
61
- react_1.default.createElement(Input_1.Input, { label: "Title", ...register('link.title') })),
72
+ react_1.default.createElement(Input_1.Input, { label: "Title", required: true, error: errors?.link?.title?.message, ...register('link.title', {
73
+ required: 'Title is required',
74
+ validate: {
75
+ noEmptySpaces: validation_1.noEmptySpacesValidation,
76
+ },
77
+ }) })),
62
78
  react_1.default.createElement("div", { className: (0, clsx_1.default)('squiz-fte-form-group mb-0') },
63
79
  react_1.default.createElement(Select_1.Select, { name: "link.target", label: "Target", value: data?.link?.target || '_self', options: targetOptions, onChange: (value) => setValue('link.target', value) })))),
64
80
  linkType === Extensions_1.MarkName.AssetLink && (react_1.default.createElement(react_1.default.Fragment, null,
65
81
  react_1.default.createElement("div", { className: (0, clsx_1.default)('squiz-fte-form-group mb-2') },
66
- react_1.default.createElement(Input_1.Input, { label: "Asset ID", error: errors?.assetLink?.matrixAssetId?.message, ...register('assetLink.matrixAssetId', {
82
+ react_1.default.createElement(Input_1.Input, { label: "Asset ID", required: true, error: errors?.assetLink?.matrixAssetId?.message, ...register('assetLink.matrixAssetId', {
83
+ required: 'Asset ID is required',
67
84
  validate: {
68
85
  isValidAsset: async (assetId) => {
69
86
  if (assetId && !(await context.matrix.resolveMatrixAsset(assetId))) {
70
87
  return 'Invalid asset ID';
71
88
  }
72
89
  },
90
+ noEmptySpaces: validation_1.noEmptySpacesValidation,
73
91
  },
74
92
  }) })),
75
93
  react_1.default.createElement("div", { className: (0, clsx_1.default)('squiz-fte-form-group mb-2') },
76
- react_1.default.createElement(Input_1.Input, { label: "Text", ...register('text') })),
94
+ react_1.default.createElement(Input_1.Input, { label: "Text", required: true, error: errors?.text?.message, ...register('text', {
95
+ required: 'Text is required',
96
+ validate: {
97
+ noEmptySpaces: validation_1.noEmptySpacesValidation,
98
+ },
99
+ }) })),
77
100
  react_1.default.createElement("div", { className: (0, clsx_1.default)('squiz-fte-form-group mb-0') },
78
- react_1.default.createElement(Select_1.Select, { name: "assetLink.target", label: "Target", value: data?.link?.target || '_self', options: targetOptions, onChange: (value) => setValue('assetLink.target', value) }))))));
101
+ react_1.default.createElement(Select_1.Select, { name: "assetLink.target", label: "Target", value: data?.assetLink?.target || '_self', options: targetOptions, onChange: (value) => setValue('assetLink.target', value) }))))));
79
102
  };
80
103
  exports.LinkForm = LinkForm;
@@ -48,8 +48,8 @@ const Modal = ({ children, title, onCancel, onSubmit, className }, ref) => {
48
48
  };
49
49
  // register key listeners for Enter/Escape on key up so the editor doesn't handle the event as well
50
50
  (0, react_1.useEffect)(() => {
51
- window.addEventListener('keyup', keydown);
52
- return () => window.removeEventListener('keyup', keydown);
51
+ window.addEventListener('keydown', keydown);
52
+ return () => window.removeEventListener('keydown', keydown);
53
53
  }, []);
54
54
  // add/remove the modal container from the DOM and focus on the first input
55
55
  (0, react_1.useEffect)(() => {
@@ -0,0 +1,2 @@
1
+ export declare const noEmptySpacesValidation: (value: string | undefined) => Promise<"Empty space is not allowed" | undefined>;
2
+ export declare const regexDataURI: RegExp;
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.regexDataURI = exports.noEmptySpacesValidation = void 0;
4
+ const noEmptySpacesValidation = async (value) => {
5
+ if (value && !(value.trim().length > 0)) {
6
+ return 'Empty space is not allowed';
7
+ }
8
+ };
9
+ exports.noEmptySpacesValidation = noEmptySpacesValidation;
10
+ exports.regexDataURI = /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)$/i;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@squiz/formatted-text-editor",
3
- "version": "1.22.1-alpha.6",
3
+ "version": "1.22.1-alpha.8",
4
4
  "main": "lib/index.js",
5
5
  "types": "lib/index.d.ts",
6
6
  "scripts": {
@@ -74,5 +74,5 @@
74
74
  "volta": {
75
75
  "node": "18.15.0"
76
76
  },
77
- "gitHead": "74678e99718430c58c5d5d813e02be8869f95f39"
77
+ "gitHead": "ffec707da1d32e94ba004fd16da25c7e3a259e01"
78
78
  }
@@ -12,6 +12,7 @@ import { AssetImageAttributes } from '../../../../Extensions/ImageExtension/Asse
12
12
  import { DeepPartial } from '../../../../types';
13
13
  import { Select, SelectOptions } from '../../../../ui/Fields/Select/Select';
14
14
  import { EditorContext } from '../../../../Editor/EditorContext';
15
+ import { noEmptySpacesValidation, regexDataURI } from '../../../../utils/validation';
15
16
 
16
17
  export type ImageFormData = {
17
18
  imageType: NodeName;
@@ -30,9 +31,6 @@ export type FormProps = {
30
31
  };
31
32
  export type Dimensions = 'image.width' | 'image.height';
32
33
 
33
- const regexDataURI =
34
- /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)$/i;
35
-
36
34
  const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
37
35
  const {
38
36
  register,
@@ -118,6 +116,7 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
118
116
  return 'Must be a valid image URL';
119
117
  }
120
118
  },
119
+ noEmptySpaces: noEmptySpacesValidation,
121
120
  },
122
121
  })}
123
122
  />
@@ -127,7 +126,12 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
127
126
  label="Alternative description"
128
127
  required
129
128
  error={errors?.image?.alt?.message}
130
- {...register('image.alt', { required: 'Alternative description is required' })}
129
+ {...register('image.alt', {
130
+ required: 'Alternative description is required',
131
+ validate: {
132
+ noEmptySpaces: noEmptySpacesValidation,
133
+ },
134
+ })}
131
135
  />
132
136
  </div>
133
137
  <div className="flex flex-row">
@@ -199,6 +203,7 @@ const ImageForm = ({ data, onSubmit }: FormProps): ReactElement => {
199
203
  return 'Asset ID is invalid or not an image';
200
204
  }
201
205
  },
206
+ noEmptySpaces: noEmptySpacesValidation,
202
207
  },
203
208
  })}
204
209
  />
@@ -324,4 +324,15 @@ describe('ImageButton', () => {
324
324
  expect(screen.getByText(expectedError)).toBeInTheDocument();
325
325
  },
326
326
  );
327
+
328
+ it('Shows an error if the field value is just an empty space', async () => {
329
+ await renderWithEditor(<ImageButton />);
330
+
331
+ await openModal();
332
+ fireEvent.change(screen.getByLabelText('Source'), { target: { value: ' ' } });
333
+ fireEvent.change(screen.getByLabelText('Alternative description'), { target: { value: ' ' } });
334
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
335
+
336
+ expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(2);
337
+ });
327
338
  });
@@ -10,6 +10,7 @@ import { LinkTarget } from '../../../../Extensions/LinkExtension/common';
10
10
  import { EditorContext } from '../../../../Editor/EditorContext';
11
11
  import { MarkName } from '../../../../Extensions/Extensions';
12
12
  import { DeepPartial } from '../../../../types';
13
+ import { noEmptySpacesValidation } from '../../../../utils/validation';
13
14
 
14
15
  export type LinkFormData = {
15
16
  linkType: MarkName;
@@ -62,13 +63,44 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
62
63
  {linkType === MarkName.Link && (
63
64
  <>
64
65
  <div className={clsx('squiz-fte-form-group mb-2')}>
65
- <Input label="URL" {...register('link.href')} />
66
+ <Input
67
+ label="URL"
68
+ required
69
+ error={errors?.link?.href?.message}
70
+ {...register('link.href', {
71
+ required: 'URL is required',
72
+ validate: {
73
+ noEmptySpaces: noEmptySpacesValidation,
74
+ },
75
+ })}
76
+ />
66
77
  </div>
78
+
67
79
  <div className={clsx('squiz-fte-form-group mb-2')}>
68
- <Input label="Text" {...register('text')} />
80
+ <Input
81
+ label="Text"
82
+ required
83
+ error={errors?.text?.message}
84
+ {...register('text', {
85
+ required: 'Text is required',
86
+ validate: {
87
+ noEmptySpaces: noEmptySpacesValidation,
88
+ },
89
+ })}
90
+ />
69
91
  </div>
70
92
  <div className={clsx('squiz-fte-form-group mb-2')}>
71
- <Input label="Title" {...register('link.title')} />
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
+ />
72
104
  </div>
73
105
  <div className={clsx('squiz-fte-form-group mb-0')}>
74
106
  <Select
@@ -87,26 +119,39 @@ export const LinkForm = ({ data, onSubmit }: FormProps): ReactElement => {
87
119
  <div className={clsx('squiz-fte-form-group mb-2')}>
88
120
  <Input
89
121
  label="Asset ID"
122
+ required
90
123
  error={errors?.assetLink?.matrixAssetId?.message}
91
124
  {...register('assetLink.matrixAssetId', {
125
+ required: 'Asset ID is required',
92
126
  validate: {
93
127
  isValidAsset: async (assetId: string | undefined) => {
94
128
  if (assetId && !(await context.matrix.resolveMatrixAsset(assetId))) {
95
129
  return 'Invalid asset ID';
96
130
  }
97
131
  },
132
+ noEmptySpaces: noEmptySpacesValidation,
98
133
  },
99
134
  })}
100
135
  />
101
136
  </div>
102
137
  <div className={clsx('squiz-fte-form-group mb-2')}>
103
- <Input label="Text" {...register('text')} />
138
+ <Input
139
+ label="Text"
140
+ required
141
+ error={errors?.text?.message}
142
+ {...register('text', {
143
+ required: 'Text is required',
144
+ validate: {
145
+ noEmptySpaces: noEmptySpacesValidation,
146
+ },
147
+ })}
148
+ />
104
149
  </div>
105
150
  <div className={clsx('squiz-fte-form-group mb-0')}>
106
151
  <Select
107
152
  name="assetLink.target"
108
153
  label="Target"
109
- value={data?.link?.target || '_self'}
154
+ value={data?.assetLink?.target || '_self'}
110
155
  options={targetOptions}
111
156
  onChange={(value) => setValue('assetLink.target', value as LinkTarget)}
112
157
  />
@@ -48,7 +48,8 @@ describe('LinkButton', () => {
48
48
 
49
49
  it('Updates the attributes of an existing link', async () => {
50
50
  const { editor, getJsonContent } = await renderWithEditor(<LinkButton />, {
51
- content: '<a href="https://www.example.org/my-link">Sample link</a> with some other content.',
51
+ content:
52
+ '<a href="https://www.example.org/my-link" title="Sample title">Sample link</a> with some other content.',
52
53
  });
53
54
 
54
55
  // jump to the middle of the link.
@@ -58,6 +59,7 @@ describe('LinkButton', () => {
58
59
  await openModal();
59
60
  fireEvent.change(screen.getByLabelText('URL'), { target: { value: 'https://www.example.org/updated-link' } });
60
61
  fireEvent.change(screen.getByLabelText('Text'), { target: { value: 'Updated sample link' } });
62
+ fireEvent.change(screen.getByLabelText('Title'), { target: { value: 'Updated sample title' } });
61
63
 
62
64
  // verify the content matches what was initially set prior to applying.
63
65
  expect(getJsonContent()).toEqual({
@@ -70,7 +72,7 @@ describe('LinkButton', () => {
70
72
  marks: [
71
73
  {
72
74
  type: 'link',
73
- attrs: { href: 'https://www.example.org/my-link', target: '_self', title: null },
75
+ attrs: { href: 'https://www.example.org/my-link', target: '_self', title: 'Sample title' },
74
76
  },
75
77
  ],
76
78
  },
@@ -96,7 +98,7 @@ describe('LinkButton', () => {
96
98
  marks: [
97
99
  {
98
100
  type: 'link',
99
- attrs: { href: 'https://www.example.org/updated-link', target: '_self', title: null },
101
+ attrs: { href: 'https://www.example.org/updated-link', target: '_self', title: 'Updated sample title' },
100
102
  },
101
103
  ],
102
104
  },
@@ -105,59 +107,10 @@ describe('LinkButton', () => {
105
107
  });
106
108
  });
107
109
 
108
- it('Removes the link when the URL is cleared', async () => {
109
- const { editor, getJsonContent } = await renderWithEditor(<LinkButton />, {
110
- content: '<a href="https://www.example.org/my-link">Sample link</a> with some other content.',
111
- });
112
-
113
- // jump to the middle of the link.
114
- await act(() => editor.selectText(3));
115
-
116
- // open the modal and clear the link.
117
- await openModal();
118
- fireEvent.change(screen.getByLabelText('URL'), { target: { value: '' } });
119
- fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
120
-
121
- await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
122
-
123
- // cursor should be positioned after the link and the link should be removed.
124
- expect(editor.from).toBe(12);
125
- expect(editor.to).toBe(12);
126
- expect(getJsonContent()).toEqual({
127
- type: 'paragraph',
128
- attrs: expect.any(Object),
129
- content: [{ type: 'text', text: 'Sample link with some other content.' }],
130
- });
131
- });
132
-
133
- it('Removes the content when the text is cleared', async () => {
134
- const { editor, getJsonContent } = await renderWithEditor(<LinkButton />, {
135
- content: '<a href="https://www.example.org/my-link">Sample link</a> with some other content.',
136
- });
137
-
138
- // jump to the middle of the link.
139
- await act(() => editor.selectText(3));
140
-
141
- // open the modal and clear the text.
142
- await openModal();
143
- fireEvent.change(screen.getByLabelText('Text'), { target: { value: '' } });
144
- fireEvent.click(screen.getByRole('button', { name: 'Apply' }));
145
-
146
- await waitForElementToBeRemoved(() => screen.getByRole('button', { name: 'Apply' }));
147
-
148
- // cursor should be positioned where the link was and the link+text should be removed.
149
- expect(editor.from).toBe(1);
150
- expect(editor.to).toBe(1);
151
- expect(getJsonContent()).toEqual({
152
- type: 'paragraph',
153
- attrs: expect.any(Object),
154
- content: [{ type: 'text', text: ' with some other content.' }],
155
- });
156
- });
157
-
158
110
  it('Updates unselected part of link when link is partially selected', async () => {
159
111
  const { editor, getJsonContent } = await renderWithEditor(<LinkButton />, {
160
- content: '<a href="https://www.example.org/my-link">Sample link</a> <strong>with</strong> some other content.',
112
+ content:
113
+ '<a href="https://www.example.org/my-link" title="Sample title">Sample link</a> <strong>with</strong> some other content.',
161
114
  });
162
115
 
163
116
  // jump to the middle of the link and select some of the text after it.
@@ -182,7 +135,7 @@ describe('LinkButton', () => {
182
135
  marks: [
183
136
  {
184
137
  type: 'link',
185
- attrs: { href: 'https://www.example.org/my-link', target: '_self', title: null },
138
+ attrs: { href: 'https://www.example.org/my-link', target: '_self', title: 'Sample title' },
186
139
  },
187
140
  ],
188
141
  },
@@ -192,7 +145,7 @@ describe('LinkButton', () => {
192
145
  marks: [
193
146
  {
194
147
  type: 'link',
195
- attrs: { href: 'https://www.example.org/my-link', target: '_self', title: null },
148
+ attrs: { href: 'https://www.example.org/my-link', target: '_self', title: 'Sample title' },
196
149
  },
197
150
  { type: 'bold' },
198
151
  ],
@@ -203,7 +156,7 @@ describe('LinkButton', () => {
203
156
  marks: [
204
157
  {
205
158
  type: 'link',
206
- attrs: { href: 'https://www.example.org/my-link', target: '_self', title: null },
159
+ attrs: { href: 'https://www.example.org/my-link', target: '_self', title: 'Sample title' },
207
160
  },
208
161
  ],
209
162
  },
@@ -214,7 +167,8 @@ describe('LinkButton', () => {
214
167
 
215
168
  it('Updates text and formatting when selection has a mixture of formatting', async () => {
216
169
  const { editor, getJsonContent } = await renderWithEditor(<LinkButton />, {
217
- content: '<a href="https://www.example.org/my-link">Sample link</a> <strong>with</strong> some other content.',
170
+ content:
171
+ '<a href="https://www.example.org/my-link" title="Sample title">Sample link</a> <strong>with</strong> some other content.',
218
172
  });
219
173
 
220
174
  // jump to the middle of the link and select some of the text after it.
@@ -240,7 +194,7 @@ describe('LinkButton', () => {
240
194
  marks: [
241
195
  {
242
196
  type: 'link',
243
- attrs: { href: 'https://www.example.org/my-link', target: '_self', title: null },
197
+ attrs: { href: 'https://www.example.org/my-link', target: '_self', title: 'Sample title' },
244
198
  },
245
199
  ],
246
200
  },
@@ -385,4 +339,30 @@ describe('LinkButton', () => {
385
339
  expect(screen.getByText('Invalid asset ID')).toBeInTheDocument();
386
340
  expect(resolveMatrixAsset).toHaveBeenCalledWith('invalid-asset-id');
387
341
  });
342
+
343
+ it('Shows an error if a required field is not provided', async () => {
344
+ await renderWithEditor(<LinkButton />);
345
+
346
+ await openModal();
347
+ fireEvent.change(screen.getByLabelText('URL'), { target: { value: '' } });
348
+ fireEvent.change(screen.getByLabelText('Text'), { target: { value: '' } });
349
+ fireEvent.change(screen.getByLabelText('Title'), { target: { value: '' } });
350
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
351
+
352
+ expect(screen.getByText('URL is required')).toBeInTheDocument();
353
+ expect(screen.getByText('Text is required')).toBeInTheDocument();
354
+ expect(screen.getByText('Title is required')).toBeInTheDocument();
355
+ });
356
+
357
+ it('Shows an error if the field value is just an empty space', async () => {
358
+ await renderWithEditor(<LinkButton />);
359
+
360
+ await openModal();
361
+ fireEvent.change(screen.getByLabelText('URL'), { target: { value: ' ' } });
362
+ fireEvent.change(screen.getByLabelText('Text'), { target: { value: ' ' } });
363
+ fireEvent.change(screen.getByLabelText('Title'), { target: { value: ' ' } });
364
+ await act(() => fireEvent.click(screen.getByRole('button', { name: 'Apply' })));
365
+
366
+ expect(screen.getAllByText('Empty space is not allowed')).toHaveLength(3);
367
+ });
388
368
  });
@@ -82,7 +82,7 @@ describe('Modal', () => {
82
82
  </Modal>,
83
83
  );
84
84
 
85
- fireEvent.keyUp(screen.getByText('Modal content'), { key: 'Enter' });
85
+ fireEvent.keyDown(screen.getByText('Modal content'), { key: 'Enter' });
86
86
 
87
87
  expect(mockOnSubmit).toHaveBeenCalled();
88
88
  });
@@ -94,7 +94,7 @@ describe('Modal', () => {
94
94
  </Modal>,
95
95
  );
96
96
 
97
- fireEvent.keyUp(screen.getByText('Modal content'), { key: 'Escape' });
97
+ fireEvent.keyDown(screen.getByText('Modal content'), { key: 'Escape' });
98
98
 
99
99
  expect(mockOnCancel).toHaveBeenCalled();
100
100
  });
@@ -33,8 +33,8 @@ const Modal = (
33
33
 
34
34
  // register key listeners for Enter/Escape on key up so the editor doesn't handle the event as well
35
35
  useEffect(() => {
36
- window.addEventListener('keyup', keydown);
37
- return () => window.removeEventListener('keyup', keydown);
36
+ window.addEventListener('keydown', keydown);
37
+ return () => window.removeEventListener('keydown', keydown);
38
38
  }, []);
39
39
 
40
40
  // add/remove the modal container from the DOM and focus on the first input
@@ -0,0 +1,8 @@
1
+ export const noEmptySpacesValidation = async (value: string | undefined) => {
2
+ if (value && !(value.trim().length > 0)) {
3
+ return 'Empty space is not allowed';
4
+ }
5
+ };
6
+
7
+ export const regexDataURI =
8
+ /^data:([a-z]+\/[a-z0-9-+.]+(;[a-z-]+=[a-z0-9-]+)?)?(;base64)?,([a-z0-9!$&',()*+;=\-._~:@/?%\s]*)$/i;