diff --git a/change/@ni-nimble-components-d0517ff8-95e8-4312-b8d6-92b04d72a666.json b/change/@ni-nimble-components-d0517ff8-95e8-4312-b8d6-92b04d72a666.json new file mode 100644 index 0000000000..5a86b3c85b --- /dev/null +++ b/change/@ni-nimble-components-d0517ff8-95e8-4312-b8d6-92b04d72a666.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Avoid converting markdown syntax characters like star (*) and underscore(_) to format the editor content automatically while typing", + "packageName": "@ni/nimble-components", + "email": "123377523+vivinkrishna-ni@users.noreply.github.com", + "dependentChangeType": "patch" +} diff --git a/packages/nimble-components/src/rich-text/editor/index.ts b/packages/nimble-components/src/rich-text/editor/index.ts index f0b3064d4a..f94b74fdbe 100644 --- a/packages/nimble-components/src/rich-text/editor/index.ts +++ b/packages/nimble-components/src/rich-text/editor/index.ts @@ -343,6 +343,13 @@ export class RichTextEditor extends FoundationElement implements ErrorPattern { */ return new Editor({ element: this.editor, + // The editor will detect markdown syntax for an input only for these items + // https://tiptap.dev/api/editor#enable-input-rules + enableInputRules: [BulletList, OrderedList], + // The editor will not detect markdown syntax when pasting content in any supported items + // Lists do not have any default paste rules, they have only input rules, so disabled paste rules + // https://tiptap.dev/api/editor#enable-paste-rules + enablePasteRules: false, extensions: [ Document, Paragraph, 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 081236b7f7..56a0d620cc 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 @@ -122,6 +122,15 @@ export class RichTextEditorPageObject { toggleButton.control.dispatchEvent(event); } + public pasteToEditor(text: string): void { + const editor = this.getTiptapEditor(); + const pasteEvent = new ClipboardEvent('paste', { + clipboardData: new DataTransfer() + }); + pasteEvent.clipboardData?.setData('text/plain', text); + editor!.dispatchEvent(pasteEvent); + } + public async setEditorTextContent(value: string): Promise { const lastElement = this.getEditorLastChildElement(); const textNode = document.createTextNode(value); 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 54bee01563..1b03c83414 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 @@ -1,7 +1,10 @@ import { html } from '@microsoft/fast-element'; import { richTextEditorTag, RichTextEditor } from '..'; import { type Fixture, fixture } from '../../../utilities/tests/fixture'; -import { getSpecTypeByNamedList } from '../../../utilities/tests/parameterized'; +import { + getSpecTypeByNamedList, + parameterizeNamedList +} from '../../../utilities/tests/parameterized'; import { RichTextEditorPageObject } from '../testing/rich-text-editor.pageobject'; import { wackyStrings } from '../../../utilities/tests/wacky-strings'; import type { Button } from '../../../button'; @@ -390,6 +393,125 @@ describe('RichTextEditor', () => { ]); }); + describe('should render as a plain text for bold and italics input rule entered into the editor', () => { + const markdownInput = [ + { name: 'bold(**)', input: '**bold**' }, + { name: 'bold(__)', input: '__bold__' }, + { name: 'italics(*)', input: '*italics*' }, + { name: 'italics(_)', input: '_italics_' } + ] as const; + parameterizeNamedList(markdownInput, (spec, name, value) => { + spec(`for ${name} markdown input to the editor`, async () => { + await pageObject.setEditorTextContent(value.input); + + expect(pageObject.getEditorTagNames()).toEqual(['P']); + expect(pageObject.getEditorLeafContents()).toEqual([ + value.input + ]); + }); + }); + }); + + describe('should render as lists when its input rule is entered into the editor', () => { + const markdownInput = [ + { name: 'bullet list', input: '*', tagName: 'UL' }, + { name: 'bullet list', input: '+', tagName: 'UL' }, + { name: 'bullet list', input: '-', tagName: 'UL' }, + { name: 'numbered list', input: '1.', tagName: 'OL' }, + { name: 'numbered list', input: '5.', tagName: 'OL' } + ] as const; + parameterizeNamedList(markdownInput, (spec, name, value) => { + spec(`for ${name} markdown input to the editor`, async () => { + await pageObject.setEditorTextContent(value.input); + await pageObject.pressEnterKeyInEditor(); + await pageObject.setEditorTextContent(value.name); + + expect( + pageObject.getEditorTagNamesWithClosingTags() + ).toEqual([ + value.tagName, + 'LI', + 'P', + '/P', + '/LI', + `/${value.tagName}` + ]); + expect(pageObject.getEditorLeafContents()).toEqual([ + value.name + ]); + }); + }); + }); + + describe('should render as a plain text for all supported markdown strings are pasted into the editor', () => { + const markdownInput = [ + { name: 'bold(**)', input: '**bold**' }, + { name: 'bold(__)', input: '__bold__' }, + { name: 'italics(*)', input: '*italics*' }, + { name: 'italics(_)', input: '_italics_' }, + { name: 'bullet list(*)', input: '* ' }, + { name: 'bullet list(+)', input: '+ ' }, + { name: 'bullet list(-)', input: '- ' }, + { name: 'numbered list(1.)', input: '1. ' }, + { name: 'numbered list(5.)', input: '5. ' }, + { name: 'autolink()', input: '' }, + { name: 'autolink()', input: '' }, + { name: 'autolink()', input: '' }, + { name: 'hard break', input: 'hard\\nbreak' }, + { name: 'blockquote', input: '> blockquote' }, + { name: 'code', input: '`code`' }, + { name: 'fence', input: '```fence```' }, + { name: 'strikethrough', input: '~~strikethrough~~' }, + { name: 'heading 1', input: '# heading 1' }, + { name: 'heading 2', input: '## heading 2' }, + { name: 'heading 3', input: '### heading 3' }, + { name: 'hyperlink', input: '[link](url)' }, + { + name: 'reference', + input: '[ref][link] [link]:url' + }, + { name: 'image', input: '![Text](Image)' }, + { name: 'horizontal rule(-)', input: '---' }, + { name: 'horizontal rule(*)', input: '***' }, + { name: 'horizontal rule(_)', input: '___' }, + { name: 'Infinity', input: '-Infinity' }, + { name: 'entity', input: ' ' }, + { + name: 'symbols', + input: '(c) (C) (r) (R) (tm) (TM) (p) (P) +-' + }, + { name: 'html string(p)', input: '

text

' }, + { name: 'html string(b)', input: 'not bold' }, + { name: 'html string(em)', input: 'not italic' }, + { + name: 'html string(ol)', + input: '
  1. not list
  2. not list
' + }, + { + name: 'html string(ul)', + input: '
  • not list
  • not list
' + }, + { + name: 'html string(a)', + input: 'https://nimble.ni.dev/' + }, + { + name: 'html string(script)', + input: '' + } + ] as const; + parameterizeNamedList(markdownInput, (spec, name, value) => { + spec(`for ${name} markdown syntax to the editor`, () => { + pageObject.pasteToEditor(value.input); + + expect(pageObject.getEditorTagNames()).toEqual(['P']); + expect(pageObject.getEditorLeafContents()).toEqual([ + value.input + ]); + }); + }); + }); + 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);