Skip to content

Commit fd74106

Browse files
authored
Merge pull request #2512 from tf/prevent-empty-text-block
Prevent text blocks with empty value
2 parents c0b132f + 7e88127 commit fd74106

9 files changed

Lines changed: 140 additions & 9 deletions

File tree

‎entry_types/scrolled/package/spec/contentElements/textBlock/editor-spec.js‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import 'contentElements/textBlock/editor';
22
import {editor} from 'editor';
3+
import {ContentElement} from 'editor/models/ContentElement';
34

45
describe('contentElements/textBlock/editor', () => {
56
const type = editor.contentElementTypes.findByTypeName('textBlock');
@@ -129,4 +130,31 @@ describe('contentElements/textBlock/editor', () => {
129130
});
130131
});
131132

133+
describe('#handleDestroy', () => {
134+
it('removes selected blocks if selection does not cover all blocks', () => {
135+
const contentElement = new ContentElement({
136+
typeName: 'textBlock',
137+
transientState: {editableTextSelectionCoversAllBlocks: false}
138+
});
139+
jest.spyOn(contentElement, 'postCommand');
140+
141+
const result = type.handleDestroy(contentElement);
142+
143+
expect(contentElement.postCommand).toHaveBeenCalledWith({type: 'REMOVE'});
144+
expect(result).toBe(false);
145+
});
146+
147+
it('lets content element be deleted if selection covers all blocks', () => {
148+
const contentElement = new ContentElement({
149+
typeName: 'textBlock',
150+
transientState: {editableTextSelectionCoversAllBlocks: true}
151+
});
152+
jest.spyOn(contentElement, 'postCommand');
153+
154+
const result = type.handleDestroy(contentElement);
155+
156+
expect(contentElement.postCommand).not.toHaveBeenCalled();
157+
expect(result).not.toBe(false);
158+
});
159+
});
132160
});

‎entry_types/scrolled/package/spec/frontend/commenting/EditableText-spec.js‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@ describe('commenting EditableText', () => {
3333
expect(entry.queryByText('Select text to comment')).not.toBeInTheDocument();
3434
});
3535

36+
it('renders empty paragraph if value is empty array', () => {
37+
const entry = renderEntry({
38+
contentElement: {
39+
ui: <EditableText value={[]} />,
40+
typeOptions: {inlineComments: true}
41+
}
42+
});
43+
44+
expect(entry.container.querySelector('[data-slate-node="element"]')).toBeInTheDocument();
45+
});
46+
3647
it('shows hint in add comment mode', async () => {
3748
const user = userEvent.setup();
3849
const entry = renderEntry({

‎entry_types/scrolled/package/spec/frontend/inlineEditing/EditableText/features/placeholder-spec.js‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,16 @@ describe('inline editing EditableText placeholder', () => {
3636
expect(entry.queryByText('Some placeholder')).toBeInTheDocument();
3737
});
3838

39+
it('renders placeholder if value is empty array', () => {
40+
const entry = renderEntry({
41+
contentElement: {
42+
ui: <EditableText value={[]} placeholder="Some placeholder" />
43+
}
44+
});
45+
46+
expect(entry.queryByText('Some placeholder')).toBeInTheDocument();
47+
});
48+
3949
it('does not render placeholder if value is present', () => {
4050
const entry = renderEntry({
4151
contentElement: {
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import React from 'react';
2+
3+
import {EditableText} from 'frontend';
4+
import {renderEntry, useInlineEditingPageObjects} from 'support/pageObjects/inlineEditing';
5+
6+
import {waitFor} from '@testing-library/react';
7+
import '@testing-library/jest-dom/extend-expect';
8+
9+
describe('inline editing EditableText selection transient state', () => {
10+
useInlineEditingPageObjects();
11+
12+
beforeEach(() => {
13+
Range.prototype.getBoundingClientRect = () => document.body.getBoundingClientRect();
14+
Range.prototype.getClientRects = () => [];
15+
});
16+
17+
afterEach(() => {
18+
delete Range.prototype.getBoundingClientRect;
19+
delete Range.prototype.getClientRects;
20+
});
21+
22+
const value = [
23+
{type: 'paragraph', children: [{text: 'First paragraph'}]},
24+
{type: 'paragraph', children: [{text: 'Second paragraph'}]}
25+
];
26+
27+
it('reports selection covering all blocks', async () => {
28+
const entry = renderEntry({
29+
contentElement: {
30+
ui: <EditableText value={value} contentElementId={1} selectionRect={true} />,
31+
typeOptions: {customSelectionRect: true}
32+
}
33+
});
34+
35+
entry.simulateSelectMessage({type: 'contentElement', id: 1, range: [0, 2]});
36+
37+
await waitFor(() =>
38+
expect(entry.getTransientContentElementState(1)).toMatchObject({
39+
editableTextSelectionCoversAllBlocks: true
40+
})
41+
);
42+
});
43+
44+
it('reports selection not covering all blocks', async () => {
45+
const entry = renderEntry({
46+
contentElement: {
47+
ui: <EditableText value={value} contentElementId={1} selectionRect={true} />,
48+
typeOptions: {customSelectionRect: true}
49+
}
50+
});
51+
52+
entry.simulateSelectMessage({type: 'contentElement', id: 1, range: [1, 2]});
53+
54+
await waitFor(() =>
55+
expect(entry.getTransientContentElementState(1)).toMatchObject({
56+
editableTextSelectionCoversAllBlocks: false
57+
})
58+
);
59+
});
60+
});

‎entry_types/scrolled/package/spec/support/pageObjects/inlineEditing.js‎

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,27 @@ export function renderEntry({commenting, ...options} = {}) {
2424
return {
2525
...result,
2626
queryAllCommentBadges: () =>
27-
result.queryAllByRole('status').map(createCommentBadgePageObject)
27+
result.queryAllByRole('status').map(createCommentBadgePageObject),
28+
29+
simulateSelectMessage(payload) {
30+
act(() => {
31+
window.dispatchEvent(new MessageEvent('message', {
32+
data: {type: 'SELECT', payload},
33+
origin: window.location.origin
34+
}));
35+
});
36+
},
37+
38+
getTransientContentElementState(id) {
39+
return window.parent.postMessage.mock.calls
40+
.map(([message]) => message)
41+
.filter(message =>
42+
message.type === 'UPDATE_TRANSIENT_CONTENT_ELEMENT_STATE' &&
43+
message.payload.id === id
44+
)
45+
.map(message => message.payload.state)
46+
.pop();
47+
}
2848
};
2949
}
3050

‎entry_types/scrolled/package/src/contentElements/textBlock/editor.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,7 @@ editor.contentElementTypes.register('textBlock', {
161161
handleDestroy(contentElement) {
162162
const transientState = contentElement.get('transientState') || {};
163163

164-
if (!transientState.editableTextIsSingleBlock) {
164+
if (!transientState.editableTextSelectionCoversAllBlocks) {
165165
contentElement.postCommand({type: 'REMOVE'});
166166
return false;
167167
}

‎entry_types/scrolled/package/src/frontend/commenting/EditableText.js‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ function CommentingEditableText({
9191
typographyVariant={typographyVariant}
9292
typographySize={typographySize}>
9393
<Slate editor={editor}
94-
value={value || defaultValue}
94+
value={value?.length ? value : defaultValue}
9595
onChange={() => {}}>
9696
<Editable key={(subjectRange ? 'highlighted' : 'plain') + threads.length}
9797
onMouseUp={handleMouseUp}

‎entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/Selection.js‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -119,7 +119,8 @@ export function Selection(props) {
119119
}
120120

121121
setTransientState({
122-
editableTextIsSingleBlock: editor.children.length <= 1,
122+
editableTextSelectionCoversAllBlocks:
123+
start === 0 && end === editor.children.length - 1,
123124
exampleNode: getUniformSelectedNode(editor, 'type'),
124125
typographyVariant: getUniformSelectedNode(editor, 'variant')?.variant,
125126
typographySize: getUniformSelectedNode(editor, 'size')?.size,

‎entry_types/scrolled/package/src/frontend/inlineEditing/EditableText/index.js‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,11 +113,12 @@ export const EditableText = React.memo(function EditableText({
113113
getTrackedSubjectRanges
114114
} = useCommenting(editor);
115115

116-
const [cachedValue, setCachedValue] = useCachedValue(value, {
117-
defaultValue: [{
118-
type: 'paragraph',
119-
children: [{ text: '' }],
120-
}],
116+
const emptyValue = useMemo(() => [{
117+
type: 'paragraph',
118+
children: [{text: ''}]
119+
}], []);
120+
121+
const [cachedValue, setCachedValue] = useCachedValue(value?.length ? value : emptyValue, {
121122
onDebouncedChange: nextValue => {
122123
if (onChange) {
123124
onChange(nextValue, {commentThreadSubjectRanges: getTrackedSubjectRanges()});

0 commit comments

Comments
 (0)