From 129af8084eab1d47df84d6ee7281fd28bbaef8ec Mon Sep 17 00:00:00 2001 From: aagash-ni <123377167+aagash-ni@users.noreply.github.com> Date: Sat, 16 Sep 2023 02:27:49 +0530 Subject: [PATCH] Rich Text Editor | Added HardBreak extension in editor for force line break (
tag) (#1517) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Pull Request ## ๐Ÿคจ Rationale Fix for [Bug 2516897](https://dev.azure.com/ni/DevCentral/_workitems/edit/2516897): Switching to a new line by pressing enter adds more spaces than expected. - Pressing enter in the editor for a new line adds a `Paragraph tag` which has more space than expected between lines. - Enabling [HardBreak ](https://tiptap.dev/api/nodes/hard-break) will allow the user to switch to a new line by pressing `Shift/Ctrl/Cmd + Enter` which will make minimal space between lines by adding `
tag` instead of `Paragraph Tag`. ## ๐Ÿ‘ฉโ€๐Ÿ’ป Implementation - Added [`HardBreak` ](https://tiptap.dev/api/nodes/hard-break) node in Tiptap Editor. - Enabled [`newline`](https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/rules_inline/newline.js#L27C8-L28C42) and [`escape`](https://github.com/markdown-it/markdown-it/blob/2b6cac25823af011ff3bc7628bc9b06e483c5a08/lib/rules_inline/escape.js#L1) rules in MarkdownIt for `RichTextMarkdownParser` to parse the HardBreak markdown syntax as per [CommonMark Spec](https://spec.commonmark.org/0.30/#hard-line-breaks) to `
tag`. - Added `hardBreak` node to `RichTextMarkdownSerializer` to serialize the `
tag` to respective hard break markdown syntax. - As we are using `prosemirror-markdown`, it serializes the `
` tag to backslash with [line ending](https://spec.commonmark.org/0.30/#hard-line-breaks) syntax as per the [CommonMark Spec example 633](https://spec.commonmark.org/0.30/#example-633). In Windows `Shift/Ctrl+ Enter` and in Mac `Cmd + Enter` will switch to a new line (line break). We are not exposing any format buttons to add line breaks (Hard Break), The only way to break the line is by using the Keys mentioned above. ## ๐Ÿงช Testing - Added unit tests and visual tests for the functionality. - Manually tested and verified the functionality of the supported features in Storybook build. ## โœ… Checklist - [x] I have updated the project documentation to reflect my changes or determined no changes are needed. --- ...-90bda8e4-208e-4e0c-ae48-b3730a529c57.json | 7 + package-lock.json | 13 ++ packages/nimble-components/package.json | 1 + .../src/rich-text/editor/index.ts | 2 + .../testing/rich-text-editor.pageobject.ts | 19 ++ .../tests/rich-text-editor-matrix.stories.ts | 20 ++ .../editor/tests/rich-text-editor.spec.ts | 180 +++++++++++++++++- .../src/rich-text/models/markdown-parser.ts | 3 +- .../rich-text/models/markdown-serializer.ts | 3 +- .../models/tests/markdown-parser.spec.ts | 76 ++++++++ .../models/tests/markdown-serializer.spec.ts | 91 ++++++++- .../src/rich-text/specs/README.md | 3 + 12 files changed, 409 insertions(+), 9 deletions(-) create mode 100644 change/@ni-nimble-components-90bda8e4-208e-4e0c-ae48-b3730a529c57.json diff --git a/change/@ni-nimble-components-90bda8e4-208e-4e0c-ae48-b3730a529c57.json b/change/@ni-nimble-components-90bda8e4-208e-4e0c-ae48-b3730a529c57.json new file mode 100644 index 0000000000..0c8a655141 --- /dev/null +++ b/change/@ni-nimble-components-90bda8e4-208e-4e0c-ae48-b3730a529c57.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Shift/Ctrl/Cmd + Enter will add line break in editor", + "packageName": "@ni/nimble-components", + "email": "123377167+aagash-ni@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/package-lock.json b/package-lock.json index f68a07b5c6..70d93315b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10680,6 +10680,18 @@ "@tiptap/core": "^2.0.0" } }, + "node_modules/@tiptap/extension-hard-break": { + "version": "2.1.8", + "resolved": "https://registry.npmjs.org/@tiptap/extension-hard-break/-/extension-hard-break-2.1.8.tgz", + "integrity": "sha512-K86FTizvZu7779Gz2XigW1IxAjZXduyZ7w0ipwe+5QBa/Lh6Vfl9wa8TgV1lFAkC2VATsAa3aa36llMIDBgeew==", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/ueberdosis" + }, + "peerDependencies": { + "@tiptap/core": "^2.0.0" + } + }, "node_modules/@tiptap/extension-history": { "version": "2.1.6", "resolved": "https://registry.npmjs.org/@tiptap/extension-history/-/extension-history-2.1.6.tgz", @@ -34753,6 +34765,7 @@ "@tiptap/extension-bold": "^2.1.6", "@tiptap/extension-bullet-list": "^2.1.6", "@tiptap/extension-document": "^2.1.6", + "@tiptap/extension-hard-break": "^2.1.6", "@tiptap/extension-history": "^2.1.6", "@tiptap/extension-italic": "^2.1.6", "@tiptap/extension-link": "^2.1.6", diff --git a/packages/nimble-components/package.json b/packages/nimble-components/package.json index d79a0ab5b8..ecb8e93825 100644 --- a/packages/nimble-components/package.json +++ b/packages/nimble-components/package.json @@ -76,6 +76,7 @@ "@tiptap/extension-paragraph": "^2.1.6", "@tiptap/extension-placeholder": "^2.1.6", "@tiptap/extension-text": "^2.1.6", + "@tiptap/extension-hard-break": "^2.1.6", "@types/d3-array": "^3.0.4", "@types/d3-random": "^3.0.1", "@types/d3-scale": "^4.0.2", diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index c9b5337666..f0b3064d4a 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -26,6 +26,7 @@ import Paragraph from '@tiptap/extension-paragraph'; import Placeholder from '@tiptap/extension-placeholder'; import type { PlaceholderOptions } from '@tiptap/extension-placeholder'; import Text from '@tiptap/extension-text'; +import HardBreak from '@tiptap/extension-hard-break'; import { template } from './template'; import { styles } from './styles'; import type { ToggleButton } from '../../toggle-button'; @@ -356,6 +357,7 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { placeholder: '', showOnlyWhenEditable: false }), + HardBreak, customLink.configure({ // HTMLAttribute cannot be in camelCase as we want to match it with the name in Tiptap // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts index c953291676..8d700b4491 100644 --- a/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts +++ b/packages/nimble-components/src/rich-text/editor/testing/rich-text-editor.pageobject.ts @@ -55,6 +55,18 @@ export class RichTextEditorPageObject { await waitForUpdatesAsync(); } + public async pressShiftEnterKeysInEditor(): Promise { + const editor = this.getTiptapEditor(); + const shiftEnterEvent = new KeyboardEvent('keydown', { + key: keyEnter, + shiftKey: true, + bubbles: true, + cancelable: true + }); + editor!.dispatchEvent(shiftEnterEvent); + await waitForUpdatesAsync(); + } + public async pressTabKeyInEditor(): Promise { const editor = this.getTiptapEditor(); const event = new KeyboardEvent('keydown', { @@ -117,6 +129,13 @@ export class RichTextEditorPageObject { } public async setEditorTextContent(value: string): Promise { + const lastElement = this.getEditorLastChildElement(); + const textNode = document.createTextNode(value); + lastElement.parentElement!.appendChild(textNode); + await waitForUpdatesAsync(); + } + + public async replaceEditorContent(value: string): Promise { const lastElement = this.getEditorLastChildElement(); lastElement.parentElement!.textContent = value; await waitForUpdatesAsync(); diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts index bfe669f555..3512fa62de 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor-matrix.stories.ts @@ -197,6 +197,26 @@ longWordContentInMobileWidth.play = (): void => { ); }; +const newLineWithForceLineBreakContent = ` +This is a line 1\\ +This line enters new line using hardbreak
tag + + +This line enters new line in paragraph tag + + +1. Point 1 + * Sub point 1\\ + Hard break sub point content +`; + +export const newLineWithForceLineBreakInMobileWidth: StoryFn = createStory(mobileWidthComponent); +newLineWithForceLineBreakInMobileWidth.play = (): void => { + document + .querySelector('nimble-rich-text-editor')! + .setMarkdown(newLineWithForceLineBreakContent); +}; + export const longLinkInMobileWidth: StoryFn = createStory(mobileWidthComponent); longLinkInMobileWidth.play = (): void => { document diff --git a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts index 9b55942546..db2472e34e 100644 --- a/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts +++ b/packages/nimble-components/src/rich-text/editor/tests/rich-text-editor.spec.ts @@ -349,6 +349,15 @@ describe('RichTextEditor', () => { }); describe('rich text formatting options to its respective HTML elements', () => { + it('should have "br" tag name when clicking shift + enter', async () => { + await pageObject.setEditorTextContent('Plain text 1'); + await pageObject.pressShiftEnterKeysInEditor(); + await pageObject.setEditorTextContent('Plain text 2'); + await pageObject.pressShiftEnterKeysInEditor(); + await pageObject.setEditorTextContent('Plain text 3'); + expect(pageObject.getEditorTagNames()).toEqual(['P', 'BR', 'BR']); + }); + it('should have "strong" tag name for bold button click', async () => { await pageObject.clickFooterButton(ToolbarButton.bold); await pageObject.setEditorTextContent('bold'); @@ -357,6 +366,20 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorLeafContents()).toEqual(['bold']); }); + it('should have br tag name when pressing shift + Enter with bold content', async () => { + await pageObject.clickFooterButton(ToolbarButton.bold); + await pageObject.setEditorTextContent('bold1'); + await pageObject.pressShiftEnterKeysInEditor(); + await pageObject.setEditorTextContent('bold after hard break'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'STRONG', + 'BR', + 'STRONG' + ]); + }); + it('should have "em" tag name for italics button click', async () => { await pageObject.clickFooterButton(ToolbarButton.italics); await pageObject.setEditorTextContent('italics'); @@ -365,6 +388,20 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorLeafContents()).toEqual(['italics']); }); + it('should have br tag name when pressing shift + Enter with Italics content', async () => { + await pageObject.clickFooterButton(ToolbarButton.italics); + await pageObject.setEditorTextContent('italics1'); + await pageObject.pressShiftEnterKeysInEditor(); + await pageObject.setEditorTextContent('italics after hard break'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'P', + 'EM', + 'BR', + 'EM' + ]); + }); + it('should have "ol" tag name for numbered list button click', async () => { await pageObject.setEditorTextContent('numbered list'); await pageObject.clickFooterButton(ToolbarButton.numberedList); @@ -375,6 +412,22 @@ describe('RichTextEditor', () => { ]); }); + it('should have br tag name when pressing shift + Enter with numbered list content', async () => { + await pageObject.setEditorTextContent('numbered list1'); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + await pageObject.pressShiftEnterKeysInEditor(); + await pageObject.setEditorTextContent( + 'Hard break in first level of numbered list' + ); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'BR' + ]); + }); + it('should have multiple "ol" tag names for numbered list button click', async () => { await pageObject.setEditorTextContent('numbered list 1'); await pageObject.clickFooterButton(ToolbarButton.numberedList); @@ -418,6 +471,25 @@ describe('RichTextEditor', () => { ).toBeTrue(); }); + it('should have br tag name when pressing shift + Enter with nested numbered lists content', async () => { + await pageObject.setEditorTextContent('List'); + await pageObject.clickFooterButton(ToolbarButton.numberedList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.pressTabKeyInEditor(); + await pageObject.pressShiftEnterKeysInEditor(); + await pageObject.setEditorTextContent('Hard break in Nested list'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'OL', + 'LI', + 'P', + 'OL', + 'LI', + 'P', + 'BR' + ]); + }); + it('should have "ol" tag names for numbered lists when clicking "tab" to make it nested and "shift+Tab" to make it usual list', async () => { await pageObject.setEditorTextContent('List'); await pageObject.clickFooterButton(ToolbarButton.numberedList); @@ -488,6 +560,22 @@ describe('RichTextEditor', () => { expect(pageObject.getEditorLeafContents()).toEqual(['Bullet List']); }); + it('should have br tag name when pressing shift + Enter with bulleted list content', async () => { + await pageObject.setEditorTextContent('Bulleted List 1'); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + await pageObject.pressShiftEnterKeysInEditor(); + await pageObject.setEditorTextContent( + 'Hard break in first level of bulleted List' + ); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'BR' + ]); + }); + it('should have multiple "ul" tag names for bullet list button click', async () => { await pageObject.setEditorTextContent('Bullet List 1'); await pageObject.clickFooterButton(ToolbarButton.bulletList); @@ -531,6 +619,25 @@ describe('RichTextEditor', () => { ).toBeTrue(); }); + it('should have br tag name when pressing shift + Enter with nested bulleted lists content', async () => { + await pageObject.setEditorTextContent('List'); + await pageObject.clickFooterButton(ToolbarButton.bulletList); + await pageObject.pressEnterKeyInEditor(); + await pageObject.pressTabKeyInEditor(); + await pageObject.pressShiftEnterKeysInEditor(); + await pageObject.setEditorTextContent('Nested List'); + + expect(pageObject.getEditorTagNames()).toEqual([ + 'UL', + 'LI', + 'P', + 'UL', + 'LI', + 'P', + 'BR' + ]); + }); + it('should have "ul" tag name for bullet list and "ol" tag name for nested numbered list', async () => { await pageObject.setEditorTextContent('Bullet List'); await pageObject.clickFooterButton(ToolbarButton.bulletList); @@ -1074,6 +1181,11 @@ describe('RichTextEditor', () => { expect(element.getMarkdown()).toBe('new markdown string'); }); + it('setting an markdown with hard break syntax should have respective br tag', () => { + element.setMarkdown('markdown\\\nstring'); + expect(pageObject.getEditorTagNames()).toEqual(['P', 'BR']); + }); + describe('Should return respective markdown when supported rich text formatting options from markdown string is assigned', () => { beforeEach(async () => { await connect(); @@ -1266,6 +1378,70 @@ describe('RichTextEditor', () => { } }); + describe('`getMarkdown` with hard break backslashes should be same immediately after `setMarkdown`', () => { + const r = String.raw; + const hardBreakMarkdownStrings: { name: string, value: string }[] = [ + { + name: 'bold and italics', + value: r`**bold**\ +*Italics*` + }, + { + name: 'two first level bulleted list items', + value: r`* list\ + hard break content + +* list` + }, + { + name: 'two first level bulleted list items and with nested list', + value: r`* list\ + hard break content + +* list + + * nested list\ + nested hard break content` + }, + { + name: 'two first level numbered list items', + value: r`1. list\ + hard break content + +2. list` + }, + { + name: 'two first level numbered list items and with nested list', + value: r`1. list\ + hard break content + +2. list + + 1. nested list\ + nested hard break content` + } + ]; + + const focused: string[] = []; + const disabled: string[] = []; + for (const value of hardBreakMarkdownStrings) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + specType( + `markdown string with hard break in "${value.name}" returns as same without any change`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + async () => { + element.setMarkdown(value.value); + + await connect(); + + expect(element.getMarkdown()).toBe(value.value); + + await disconnect(); + } + ); + } + }); + describe('Should return markdown without any changes when various wacky string values are assigned', () => { const focused: string[] = []; const disabled: string[] = []; @@ -1479,10 +1655,10 @@ describe('RichTextEditor', () => { it('should initialize "empty" to true and set false when there is content', async () => { expect(element.empty).toBeTrue(); - await pageObject.setEditorTextContent('not empty'); + await pageObject.replaceEditorContent('not empty'); expect(element.empty).toBeFalse(); - await pageObject.setEditorTextContent(''); + await pageObject.replaceEditorContent(''); expect(element.empty).toBeTrue(); }); diff --git a/packages/nimble-components/src/rich-text/models/markdown-parser.ts b/packages/nimble-components/src/rich-text/models/markdown-parser.ts index ce7b2aaea4..36d881e174 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-parser.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-parser.ts @@ -48,8 +48,9 @@ export class RichTextMarkdownParser { const supportedTokenizerRules = zeroTokenizerConfiguration.enable([ 'emphasis', 'list', + 'escape', 'autolink', - 'escape' + 'newline' ]); supportedTokenizerRules.validateLink = href => /^https?:\/\//i.test(href); diff --git a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts index b6e8a145ef..12096c0f0b 100644 --- a/packages/nimble-components/src/rich-text/models/markdown-serializer.ts +++ b/packages/nimble-components/src/rich-text/models/markdown-serializer.ts @@ -47,7 +47,8 @@ export class RichTextMarkdownSerializer { orderedList: orderedListNode, doc: defaultMarkdownSerializer.nodes.doc!, paragraph: defaultMarkdownSerializer.nodes.paragraph!, - text: defaultMarkdownSerializer.nodes.text! + text: defaultMarkdownSerializer.nodes.text!, + hardBreak: defaultMarkdownSerializer.nodes.hard_break! }; const marks = { italic: defaultMarkdownSerializer.marks.em!, diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts index c84da64367..87949d5e06 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-parser.spec.ts @@ -847,4 +847,80 @@ describe('Markdown parser', () => { ); } }); + + describe('Markdown string with hard break should have respective br tag when rendered', () => { + const focused: string[] = []; + const disabled: string[] = []; + const r = String.raw; + const markdownStringWithHardBreak: { + name: string, + value: string, + tags: string[] + }[] = [ + { + name: 'bold and italics', + value: r`**bold**\ +*Italics*`, + tags: ['P', 'STRONG', 'BR', 'EM'] + }, + { + name: 'bold and back slash followed by italics', + value: r`**bold**\ + \ *Italics*`, + tags: ['P', 'STRONG', 'BR', 'EM'] + }, + { + name: 'two first level bulleted list items', + value: r`* list\ + hard break content + +* list`, + tags: ['UL', 'LI', 'P', 'BR', 'LI', 'P'] + }, + { + name: 'two first level bulleted list items and with nested list', + value: r`* list\ + hard break content + +* list + + * nested list\ + nested hard break content`, + tags: ['UL', 'LI', 'P', 'BR', 'LI', 'P', 'UL', 'LI', 'P', 'BR'] + }, + { + name: 'two first level numbered list items', + value: r`1. list\ + hard break content + +2. list`, + tags: ['OL', 'LI', 'P', 'BR', 'LI', 'P'] + }, + { + name: 'two first level numbered list items and with nested list', + value: r`1. list\ + hard break content + +2. list + + 1. nested list\ + nested hard break content`, + tags: ['OL', 'LI', 'P', 'BR', 'LI', 'P', 'OL', 'LI', 'P', 'BR'] + } + ]; + + for (const value of markdownStringWithHardBreak) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + specType( + `should render br tag with "${value.name}"`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const doc = RichTextMarkdownParser.parseMarkdownToDOM( + value.value + ); + expect(getTagsFromElement(doc)).toEqual(value.tags); + } + ); + } + }); }); diff --git a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts index 85f9c43111..23fb586a61 100644 --- a/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts +++ b/packages/nimble-components/src/rich-text/models/tests/markdown-serializer.spec.ts @@ -2,6 +2,7 @@ import { Editor } from '@tiptap/core'; import Bold from '@tiptap/extension-bold'; import BulletList from '@tiptap/extension-bullet-list'; import Document from '@tiptap/extension-document'; +import HardBreak from '@tiptap/extension-hard-break'; import Italic from '@tiptap/extension-italic'; import ListItem from '@tiptap/extension-list-item'; import OrderedList from '@tiptap/extension-ordered-list'; @@ -24,6 +25,7 @@ describe('Markdown serializer', () => { ListItem, Bold, Italic, + HardBreak, Link.extend({ excludes: '_' }) @@ -271,11 +273,6 @@ describe('Markdown serializer', () => { plainText: 'CodeBlock' }, { name: 'Heading', html: '

Heading

', plainText: 'Heading' }, - { - name: 'HardBreak', - html: '

Hard
Break

', - plainText: 'Hard Break' - }, { name: 'HorizontalRule', html: '

Horizontal


Rule

', @@ -331,4 +328,88 @@ describe('Markdown serializer', () => { ); } }); + + describe('HardBreak node should be serialized to back slash (hard break syntax) markdown output', () => { + const r = String.raw; + const supportedNodesMarks: { + name: string, + html: string, + markdown: string + }[] = [ + { + name: 'Hard Break', + html: '

Hard
Break

', + markdown: r`Hard\ +Break` + }, + { + name: 'Bold', + html: 'Bold
Bold', + markdown: r`**Bold**\ +**Bold**` + }, + { + name: 'Italics', + html: 'Italics
Italics', + markdown: r`*Italics*\ +*Italics*` + }, + { + name: 'Bold, Hard break and Italics', + html: 'Bold
Italics', + markdown: r`**Bold**\ +*Italics*` + }, + { + name: 'Numbered list', + html: '
  1. Numbered
    list

', + markdown: r`1. Numbered\ + list` + }, + { + name: 'Bulleted list', + html: '
  • Bulleted
    list

', + markdown: r`* Bulleted\ + list` + }, + { + name: 'Nested Bulleted list and hard break', + html: '
  • list
    hard break content

  • list

    • nested list
      nested hard break content

', + markdown: r`* list\ + hard break content + +* list + + * nested list\ + nested hard break content` + }, + { + name: 'Nested Numbered list and hard break', + html: '
  1. list
    hard break content

  2. list

    1. nested list
      nested hard break content

', + markdown: r`1. list\ + hard break content + +2. list + + 1. nested list\ + nested hard break content` + } + ]; + + const focused: string[] = []; + const disabled: string[] = []; + for (const value of supportedNodesMarks) { + const specType = getSpecTypeByNamedList(value, focused, disabled); + specType( + `Should serialize ${value.name} to markdown`, + // eslint-disable-next-line @typescript-eslint/no-loop-func + () => { + const node = getNode(value.html); + expect( + RichTextMarkdownSerializer.serializeDOMToMarkdown(node) + ).toBe(value.markdown); + } + ); + } + }); }); diff --git a/packages/nimble-components/src/rich-text/specs/README.md b/packages/nimble-components/src/rich-text/specs/README.md index e5d58601cd..6dc77beb0a 100644 --- a/packages/nimble-components/src/rich-text/specs/README.md +++ b/packages/nimble-components/src/rich-text/specs/README.md @@ -344,6 +344,7 @@ markdown based on [CommonMark](http://commonmark.org/) flavor: - Numbered list - `1. Numbered list` - Bulleted list - `* Bulleted list` - Absolute URL links - `` (For more details on the markdown syntax for absolute URL links, see [Autolinks in CommonMark](https://spec.commonmark.org/0.30/#autolink)) +- Hard line break - a backslash before the line ending `line1\\nline2` (For more details on the markdown syntax for Hard line breaks, see [Hard line breaks in CommanMark](https://spec.commonmark.org/0.30/#hard-line-breaks)) _Configurations on Tiptap to support only absolute links_: @@ -459,6 +460,7 @@ in macOS. | Ctrl + Shift + 8 | To enable the focused paragraph a bulleted list | | Tab (For lists) | To create a sub point within a bulleted or numbered list | | Shift + Tab (For lists) | To remove a sub point from a bulleted or numbered lists | +| Shift/Ctrl + Enter | To enter new line by forces a line break | _Keyboard navigation with toolbar buttons focused_ @@ -501,6 +503,7 @@ library. For the currently supported features, we will include the following lib - [@tiptap/extension-bold](https://www.npmjs.com/package/@tiptap/extension-bold) - [@tiptap/extension-bullet-list](https://www.npmjs.com/package/@tiptap/extension-bullet-list) - [@tiptap/extension-document](https://www.npmjs.com/package/@tiptap/extension-document) +- [@tiptap/extension-hard-break](https://www.npmjs.com/package/@tiptap/extension-hard-break) - [@tiptap/extension-history](https://www.npmjs.com/package/@tiptap/extension-history) - [@tiptap/extension-italic](https://www.npmjs.com/package/@tiptap/extension-italic) - [@tiptap/extension-list-item](https://www.npmjs.com/package/@tiptap/extension-list-item)