Skip to content

Commit 9cdbe56

Browse files
authored
Respect contenteditable elements when checking for editability
1 parent 65d9f22 commit 9cdbe56

File tree

2 files changed

+56
-20
lines changed

2 files changed

+56
-20
lines changed

src/focus-zone.ts

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {polyfill as eventListenerSignalPolyfill} from './polyfills/event-listene
22
import {isMacOS} from './utils/user-agent.js'
33
import {IterateFocusableElements, iterateFocusableElements} from './utils/iterate-focusable-elements.js'
44
import {uniqueId} from './utils/unique-id.js'
5+
import {isEditableElement} from './utils/is-editable-elements.js'
56

67
eventListenerSignalPolyfill()
78

@@ -267,22 +268,16 @@ function shouldIgnoreFocusHandling(keyboardEvent: KeyboardEvent, activeElement:
267268
// its function to move focus rather than type a <TAB> character.
268269
const keyLength = [...key].length
269270

270-
const isTextInput =
271-
(activeElement instanceof HTMLInputElement && activeElement.type === 'text') ||
272-
activeElement instanceof HTMLTextAreaElement
271+
const isEditable = isEditableElement(activeElement)
273272

274273
// If we would normally type a character into an input, ignore
275274
// Also, Home and End keys should never affect focus when in a text input
276-
if (isTextInput && (keyLength === 1 || key === 'Home' || key === 'End')) {
275+
if (isEditable && (keyLength === 1 || key === 'Home' || key === 'End')) {
277276
return true
278277
}
279278

280-
// Some situations we want to ignore with <select> elements
279+
// Some situations we specifically want to ignore with <select> elements
281280
if (activeElement instanceof HTMLSelectElement) {
282-
// Regular typeable characters change the selection, so ignore those
283-
if (keyLength === 1) {
284-
return true
285-
}
286281
// On macOS, bare ArrowDown opens the select, so ignore that
287282
if (key === 'ArrowDown' && isMacOS() && !keyboardEvent.metaKey) {
288283
return true
@@ -293,16 +288,15 @@ function shouldIgnoreFocusHandling(keyboardEvent: KeyboardEvent, activeElement:
293288
}
294289
}
295290

296-
// Ignore page up and page down for textareas
297-
if (activeElement instanceof HTMLTextAreaElement && (key === 'PageUp' || key === 'PageDown')) {
298-
return true
299-
}
300-
301-
if (isTextInput) {
302-
const textInput = activeElement as HTMLInputElement | HTMLTextAreaElement
303-
const cursorAtStart = textInput.selectionStart === 0 && textInput.selectionEnd === 0
291+
if (isEditable) {
292+
// An editable element might not be an input element if it is a `contenteditable` element, but it's significantly
293+
// harder and less reliable to get the caret position for those elements, so for them we just always ignore arrows
294+
const isInputElement = activeElement instanceof HTMLTextAreaElement || activeElement instanceof HTMLInputElement
295+
const cursorAtStart = isInputElement && activeElement.selectionStart === 0 && activeElement.selectionEnd === 0
304296
const cursorAtEnd =
305-
textInput.selectionStart === textInput.value.length && textInput.selectionEnd === textInput.value.length
297+
isInputElement &&
298+
activeElement.selectionStart === activeElement.value.length &&
299+
activeElement.selectionEnd === activeElement.value.length
306300

307301
// When in a text area or text input, only move focus left/right if at beginning/end of the field
308302
if (key === 'ArrowLeft' && !cursorAtStart) {
@@ -312,8 +306,15 @@ function shouldIgnoreFocusHandling(keyboardEvent: KeyboardEvent, activeElement:
312306
return true
313307
}
314308

315-
// When in a text area, only move focus up/down if at beginning/end of the field
316-
if (textInput instanceof HTMLTextAreaElement) {
309+
const isContentEditable = activeElement instanceof HTMLElement && activeElement.isContentEditable
310+
311+
// When in multiline inputs
312+
if (activeElement instanceof HTMLTextAreaElement || isContentEditable) {
313+
// Always ignore page up/down
314+
if (key === 'PageUp' || key === 'PageDown') {
315+
return true
316+
}
317+
// Only move focus up/down if at beginning/end of the field
317318
if (key === 'ArrowUp' && !cursorAtStart) {
318319
return true
319320
}

src/utils/is-editable-elements.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
const nonEditableInputTypes = new Set([
2+
'button',
3+
'checkbox',
4+
'color',
5+
'file',
6+
'hidden',
7+
'image',
8+
'radio',
9+
'range',
10+
'reset',
11+
'submit',
12+
])
13+
14+
/**
15+
* Returns true if `element` is editable - that is, if it can be focused and typed in like an input or textarea.
16+
*/
17+
export function isEditableElement(target: EventTarget | null): boolean {
18+
if (!(target instanceof HTMLElement)) return false
19+
20+
const name = target.nodeName.toLowerCase()
21+
const type = target.getAttribute('type')?.toLowerCase() ?? 'text'
22+
23+
const isReadonly =
24+
target.ariaReadOnly === 'true' ||
25+
target.getAttribute('aria-readonly') === 'true' ||
26+
target.getAttribute('readonly') !== null
27+
28+
return (
29+
(name === 'select' ||
30+
name === 'textarea' ||
31+
(name === 'input' && !nonEditableInputTypes.has(type)) ||
32+
target.isContentEditable) &&
33+
!isReadonly
34+
)
35+
}

0 commit comments

Comments
 (0)