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(
+ ``,
+ )
+ 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)