@use-kona/editor 0.1.32 → 0.1.33

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.
@@ -11,6 +11,21 @@ class CollapsibleBlocksPlugin {
11
11
  }
12
12
  init(editor) {
13
13
  sectionTypesByEditor.set(editor, this.types);
14
+ const { insertBreak } = editor;
15
+ editor.insertBreak = ()=>{
16
+ const { selection } = editor;
17
+ const boundary = selection && Range.isCollapsed(selection) ? Editor.above(editor, {
18
+ match: (node, path)=>1 === path.length && Element.isElement(node) && this.types.includes(node.type) && true === node.collapsed
19
+ }) : void 0;
20
+ Editor.withoutNormalizing(editor, ()=>{
21
+ if (boundary) Transforms.setNodes(editor, {
22
+ collapsed: false
23
+ }, {
24
+ at: boundary[1]
25
+ });
26
+ insertBreak();
27
+ });
28
+ };
14
29
  return editor;
15
30
  }
16
31
  renderBlock = (props)=>{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@use-kona/editor",
3
- "version": "0.1.32",
3
+ "version": "0.1.33",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": "./src/index.ts"
@@ -9,11 +9,13 @@ import {
9
9
  waitFor,
10
10
  } from '@testing-library/react';
11
11
  import React from 'react';
12
- import { type Editor, Transforms } from 'slate';
12
+ import { Editor, Transforms } from 'slate';
13
13
  import { afterEach, describe, expect, it, vi } from 'vitest';
14
+ import type { CustomElement } from '../../../types';
14
15
  import { KonaEditor } from '../../editor';
15
16
  import type { IPlugin } from '../../types';
16
17
  import { BasicFormattingPlugin } from '../BasicFormattingPlugin';
18
+ import { BreaksPlugin } from '../BreaksPlugin';
17
19
  import { HeadingsPlugin } from '../HeadingsPlugin';
18
20
  import { CollapsibleBlocksPlugin } from './CollapsibleBlocksPlugin';
19
21
 
@@ -214,3 +216,139 @@ describe('CollapsibleBlocksPlugin', () => {
214
216
  });
215
217
  });
216
218
  });
219
+
220
+ describe('Enter in collapsed sections', () => {
221
+ const headingTypes = [
222
+ HeadingsPlugin.HeadingLevel1,
223
+ HeadingsPlugin.HeadingLevel2,
224
+ HeadingsPlugin.HeadingLevel3,
225
+ ];
226
+
227
+ const renderEditor = (value: CustomElement[]) => {
228
+ let editor: Editor | undefined;
229
+ const capturePlugin: IPlugin = {
230
+ init(value) {
231
+ editor = value;
232
+ return value;
233
+ },
234
+ };
235
+ const { container, unmount } = render(
236
+ <KonaEditor
237
+ initialValue={value}
238
+ plugins={[
239
+ ...plugins(),
240
+ new BreaksPlugin({ breakNodes: headingTypes }),
241
+ capturePlugin,
242
+ ]}
243
+ onChange={() => {}}
244
+ />,
245
+ );
246
+
247
+ if (!editor) throw new Error('Expected editor to be initialized');
248
+
249
+ return { editor, container, cleanup: unmount };
250
+ };
251
+
252
+ it.each(headingTypes)(
253
+ 'expands collapsed %s when Enter creates a paragraph after it',
254
+ async (type) => {
255
+ const { editor, container, cleanup } = renderEditor([
256
+ {
257
+ type,
258
+ collapsed: true,
259
+ children: [{ text: 'Heading' }],
260
+ },
261
+ { type: 'paragraph', children: [{ text: 'Existing content' }] },
262
+ ]);
263
+
264
+ try {
265
+ expect(container.querySelector('[hidden]')).not.toBeNull();
266
+
267
+ await act(async () => {
268
+ Transforms.select(editor, Editor.end(editor, [0]));
269
+ editor.insertBreak();
270
+ editor.insertText('New content');
271
+ });
272
+
273
+ expect(editor.children[0]).toMatchObject({ collapsed: false });
274
+ expect(editor.children[1]).toMatchObject({
275
+ type: 'paragraph',
276
+ children: [{ text: 'New content' }],
277
+ });
278
+ expect(container.querySelector('[hidden]')).toBeNull();
279
+ expect(container.textContent).toContain('New content');
280
+ expect(
281
+ container
282
+ .querySelector('[aria-label="Collapse section"]')
283
+ ?.getAttribute('aria-expanded'),
284
+ ).toBe('true');
285
+ } finally {
286
+ cleanup();
287
+ }
288
+ },
289
+ );
290
+
291
+ it('keeps a section collapsed when Enter inserts a paragraph before its heading', async () => {
292
+ const { editor, container, cleanup } = renderEditor([
293
+ {
294
+ type: HeadingsPlugin.HeadingLevel1,
295
+ collapsed: true,
296
+ children: [{ text: 'Heading' }],
297
+ },
298
+ { type: 'paragraph', children: [{ text: 'Existing content' }] },
299
+ ]);
300
+
301
+ try {
302
+ await act(async () => {
303
+ Transforms.select(editor, Editor.start(editor, [0]));
304
+ editor.insertBreak();
305
+ });
306
+
307
+ expect(editor.children[0]).toMatchObject({ type: 'paragraph' });
308
+ expect(editor.children[1]).toMatchObject({ collapsed: true });
309
+ expect(container.querySelectorAll('[hidden]')).toHaveLength(1);
310
+ } finally {
311
+ cleanup();
312
+ }
313
+ });
314
+
315
+ it('expands the edited section while preserving nested and adjacent collapsed sections', async () => {
316
+ const { editor, container, cleanup } = renderEditor([
317
+ {
318
+ type: HeadingsPlugin.HeadingLevel1,
319
+ collapsed: true,
320
+ children: [{ text: 'Heading' }],
321
+ },
322
+ {
323
+ type: HeadingsPlugin.HeadingLevel2,
324
+ collapsed: true,
325
+ children: [{ text: 'Nested heading' }],
326
+ },
327
+ { type: 'paragraph', children: [{ text: 'Nested content' }] },
328
+ {
329
+ type: HeadingsPlugin.HeadingLevel1,
330
+ collapsed: true,
331
+ children: [{ text: 'Adjacent heading' }],
332
+ },
333
+ { type: 'paragraph', children: [{ text: 'Adjacent content' }] },
334
+ ]);
335
+
336
+ try {
337
+ await act(async () => {
338
+ Transforms.select(editor, Editor.end(editor, [0]));
339
+ editor.insertBreak();
340
+ editor.insertText('New content');
341
+ });
342
+
343
+ expect(editor.children[0]).toMatchObject({ collapsed: false });
344
+ expect(editor.children[2]).toMatchObject({ collapsed: true });
345
+ expect(editor.children[4]).toMatchObject({ collapsed: true });
346
+ expect(container.querySelectorAll('[hidden]')).toHaveLength(2);
347
+ expect(
348
+ container.querySelectorAll('[aria-label="Expand section"]'),
349
+ ).toHaveLength(2);
350
+ } finally {
351
+ cleanup();
352
+ }
353
+ });
354
+ });
@@ -18,6 +18,35 @@ export class CollapsibleBlocksPlugin implements IPlugin {
18
18
 
19
19
  init(editor: Editor) {
20
20
  sectionTypesByEditor.set(editor, this.types);
21
+
22
+ const { insertBreak } = editor;
23
+
24
+ editor.insertBreak = () => {
25
+ const { selection } = editor;
26
+ const boundary =
27
+ selection && Range.isCollapsed(selection)
28
+ ? Editor.above<CollapsibleElement>(editor, {
29
+ match: (node, path) =>
30
+ path.length === 1 &&
31
+ Element.isElement(node) &&
32
+ this.types.includes(node.type) &&
33
+ node.collapsed === true,
34
+ })
35
+ : undefined;
36
+
37
+ Editor.withoutNormalizing(editor, () => {
38
+ if (boundary) {
39
+ Transforms.setNodes<CollapsibleElement>(
40
+ editor,
41
+ { collapsed: false },
42
+ { at: boundary[1] },
43
+ );
44
+ }
45
+
46
+ insertBreak();
47
+ });
48
+ };
49
+
21
50
  return editor;
22
51
  }
23
52