diff --git a/src/event/behavior/keypress.ts b/src/event/behavior/keypress.ts index dd417ec3..c63f0969 100644 --- a/src/event/behavior/keypress.ts +++ b/src/event/behavior/keypress.ts @@ -35,7 +35,10 @@ behavior.keypress = (event, target, instance) => { if (isEditable(target)) { const inputType = event.key === 'Enter' - ? isContentEditable(target) && !instance.system.keyboard.modifiers.Shift + ? isContentEditable(target) && + // A plaintext-only host has no paragraphs to break. + target.getAttribute('contenteditable') !== 'plaintext-only' && + !instance.system.keyboard.modifiers.Shift ? 'insertParagraph' : 'insertLineBreak' : 'insertText' diff --git a/src/utils/edit/isContentEditable.ts b/src/utils/edit/isContentEditable.ts index e44298be..46a24dde 100644 --- a/src/utils/edit/isContentEditable.ts +++ b/src/utils/edit/isContentEditable.ts @@ -5,7 +5,8 @@ export function isContentEditable( return ( element.hasAttribute('contenteditable') && (element.getAttribute('contenteditable') == 'true' || - element.getAttribute('contenteditable') == '') + element.getAttribute('contenteditable') == '' || + element.getAttribute('contenteditable') == 'plaintext-only') ) } @@ -13,12 +14,14 @@ export function isContentEditable( * If a node is a contenteditable or inside one, return that element. */ export function getContentEditable(node: Node): Element | null { - const element = getElement(node) - return ( - element && - (element.closest('[contenteditable=""]') || - element.closest('[contenteditable="true"]')) - ) + let element = getElement(node) + while (element) { + if (isContentEditable(element)) { + return element + } + element = element.parentElement + } + return null } function getElement(node: Node) { diff --git a/src/utils/focus/selector.ts b/src/utils/focus/selector.ts index 01100baa..7914ce8b 100644 --- a/src/utils/focus/selector.ts +++ b/src/utils/focus/selector.ts @@ -5,6 +5,7 @@ export const FOCUSABLE_SELECTOR = [ 'textarea:not([disabled])', '[contenteditable=""]', '[contenteditable="true"]', + '[contenteditable="plaintext-only"]', 'a[href]', '[tabindex]:not([disabled])', 'details > summary', diff --git a/tests/clipboard/paste.ts b/tests/clipboard/paste.ts index 9e163921..ce0d5399 100644 --- a/tests/clipboard/paste.ts +++ b/tests/clipboard/paste.ts @@ -159,3 +159,20 @@ describe('without Clipboard API', () => { expect(getEvents()).toHaveLength(0) }) }) + +test.each([['true'], ['plaintext-only']])( + 'paste into contenteditable="%s"', + async attr => { + const {element, getEvents, user} = setup( + `
foo
`, + {focus: false}, + ) + + await user.click(element) + await user.paste('bar') + + expect(element).toHaveTextContent('foobar') + expect(getEvents('paste')).toHaveLength(1) + expect(getEvents('input')).toHaveLength(1) + }, +) diff --git a/tests/event/behavior/keypress.ts b/tests/event/behavior/keypress.ts index 38d60e05..4ca49d85 100644 --- a/tests/event/behavior/keypress.ts +++ b/tests/event/behavior/keypress.ts @@ -75,6 +75,18 @@ cases( inputType: 'insertLineBreak', expectedHtml: '\n', }, + 'trigger insertLineBreak on contenteditable=plaintext-only': { + html: `
`, + inputType: 'insertLineBreak', + expectedHtml: '\n', + }, + 'with shiftKey=true trigger insertLineBreak on contenteditable=plaintext-only': + { + html: `
`, + shiftKey: true, + inputType: 'insertLineBreak', + expectedHtml: '\n', + }, }, ) diff --git a/tests/utility/type.ts b/tests/utility/type.ts index 88c063fb..0ef16c9d 100644 --- a/tests/utility/type.ts +++ b/tests/utility/type.ts @@ -1,5 +1,16 @@ import {setup} from '#testHelpers' +test('type into contenteditable plaintext-only', async () => { + const {element, user} = setup( + '
foo
', + {focus: false}, + ) + + await user.type(element, 'bar') + + expect(element).toHaveTextContent('foobar') +}) + test('type into input', async () => { const {element, getEventSnapshot, user} = setup('', { focus: false, diff --git a/tests/utils/edit/isContentEditable.ts b/tests/utils/edit/isContentEditable.ts index aab44bcd..8749552a 100644 --- a/tests/utils/edit/isContentEditable.ts +++ b/tests/utils/edit/isContentEditable.ts @@ -1,13 +1,41 @@ -import {setup} from '#testHelpers' -import {isContentEditable} from '#src/utils' +import {render, setup} from '#testHelpers' +import {getContentEditable, isContentEditable} from '#src/utils' test('report if element is contenteditable', async () => { const {elements} = setup( - `
`, + `
`, ) expect(isContentEditable(elements[0])).toBe(false) expect(isContentEditable(elements[1])).toBe(false) expect(isContentEditable(elements[2])).toBe(true) expect(isContentEditable(elements[3])).toBe(true) + expect(isContentEditable(elements[4])).toBe(true) +}) + +test.each([['true'], ['plaintext-only']])( + 'resolve the nearest editing host when it is contenteditable="%s"', + async attr => { + const {element} = render( + `
foo
`, + ) + const host = element.firstElementChild + const span = element.querySelector('span') as Element + + expect(getContentEditable(span)).toBe(host) + }, +) + +test('resolve the element itself when it is the editing host', async () => { + const {element} = render(`
foo
`) + + expect(getContentEditable(element)).toBe(element) +}) + +test('resolve no editing host outside a contenteditable', async () => { + const {element} = render(`
foo
`) + + expect(getContentEditable(element.querySelector('span') as Element)).toBe( + null, + ) }) diff --git a/tests/utils/edit/isEditable.ts b/tests/utils/edit/isEditable.ts index ec8fab82..ad2f6e1c 100644 --- a/tests/utils/edit/isEditable.ts +++ b/tests/utils/edit/isEditable.ts @@ -23,6 +23,7 @@ test.each([ [``], [`
`], [`
`], + [`
`], ])('consider %s an editable element', html => { const {element} = render(html)