Skip to content

Commit

Permalink
DOM: Allow copying text from non-text input elements (#40192)
Browse files Browse the repository at this point in the history
* DOM: Fix inputFieldHasUncollapsedSelection for non-text inputs

The selection state of non-text input elements (e.g. `number`, but even
`email`) is opaque to Gutenberg, per the HTML spec [1]. Unaware of this
nuance, `inputFieldHasUncollapsedSelection` would incorrectly report
that some non-text input element had no text selected when in fact it
did. This caused the block editor to take over `copy` events to copy
whole blocks, preventing the user from copying what they had actually
selected inside the input element in question.

The workaround in this commit is to always assume that there could be an
uncollapsed selection in an active element if that element's selection
state is opaque.

[1]: https://html.spec.whatwg.org/multipage/input.html#do-not-apply

* Add input type 'time' to isTextField's blacklist

* Unit tests: update expectations for isTextField()

* inputFieldHasUncollapsedSelection: Rewrite comments, default to true

* documentHasSelection: Accept any input types

... as oppposed to just text inputs (isTextField) and number inputs
(isNumberInput).
  • Loading branch information
mcsf committed May 6, 2022
1 parent 29aa181 commit 1bb122c
Show file tree
Hide file tree
Showing 7 changed files with 47 additions and 41 deletions.
12 changes: 6 additions & 6 deletions packages/dom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ _Returns_

### documentHasSelection

Check whether the current document has a selection. This checks for both
focus in an input field and general text selection.
Check whether the current document has a selection. This includes focus in
input fields, textareas, and general rich-text selection.

_Parameters_

Expand Down Expand Up @@ -57,17 +57,17 @@ _Returns_

### documentHasUncollapsedSelection

Check whether the current document has any sort of selection. This includes
ranges of text across elements and any selection inside `<input>` and
`<textarea>` elements.
Check whether the current document has any sort of (uncollapsed) selection.
This includes ranges of text across elements and any selection inside
textual `<input>` and `<textarea>` elements.

_Parameters_

- _doc_ `Document`: The document to check.

_Returns_

- `boolean`: Whether there is any sort of "selection" in the document.
- `boolean`: Whether there is any recognizable text selection in the document.

### focus

Expand Down
10 changes: 5 additions & 5 deletions packages/dom/src/dom/document-has-selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
* Internal dependencies
*/
import isTextField from './is-text-field';
import isNumberInput from './is-number-input';
import isHTMLInputElement from './is-html-input-element';
import documentHasTextSelection from './document-has-text-selection';

/**
* Check whether the current document has a selection. This checks for both
* focus in an input field and general text selection.
* Check whether the current document has a selection. This includes focus in
* input fields, textareas, and general rich-text selection.
*
* @param {Document} doc The document to check.
*
Expand All @@ -16,8 +16,8 @@ import documentHasTextSelection from './document-has-text-selection';
export default function documentHasSelection( doc ) {
return (
!! doc.activeElement &&
( isTextField( doc.activeElement ) ||
isNumberInput( doc.activeElement ) ||
( isHTMLInputElement( doc.activeElement ) ||
isTextField( doc.activeElement ) ||
documentHasTextSelection( doc ) )
);
}
8 changes: 4 additions & 4 deletions packages/dom/src/dom/document-has-uncollapsed-selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,13 @@ import documentHasTextSelection from './document-has-text-selection';
import inputFieldHasUncollapsedSelection from './input-field-has-uncollapsed-selection';

/**
* Check whether the current document has any sort of selection. This includes
* ranges of text across elements and any selection inside `<input>` and
* `<textarea>` elements.
* Check whether the current document has any sort of (uncollapsed) selection.
* This includes ranges of text across elements and any selection inside
* textual `<input>` and `<textarea>` elements.
*
* @param {Document} doc The document to check.
*
* @return {boolean} Whether there is any sort of "selection" in the document.
* @return {boolean} Whether there is any recognizable text selection in the document.
*/
export default function documentHasUncollapsedSelection( doc ) {
return (
Expand Down
44 changes: 26 additions & 18 deletions packages/dom/src/dom/input-field-has-uncollapsed-selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,41 +2,49 @@
* Internal dependencies
*/
import isTextField from './is-text-field';
import isNumberInput from './is-number-input';
import isHTMLInputElement from './is-html-input-element';

/**
* Check whether the given element, assumed an input field or textarea,
* contains a (uncollapsed) selection of text.
* Check whether the given input field or textarea contains a (uncollapsed)
* selection of text.
*
* Note: this is perhaps an abuse of the term "selection", since these elements
* manage selection differently and aren't covered by Selection#collapsed.
* CAVEAT: Only specific text-based HTML inputs support the selection APIs
* needed to determine whether they have a collapsed or uncollapsed selection.
* This function defaults to returning `true` when the selection cannot be
* inspected, such as with `<input type="time">`. The rationale is that this
* should cause the block editor to defer to the browser's native selection
* handling (e.g. copying and pasting), thereby reducing friction for the user.
*
* See: https://developer.mozilla.org/en-US/docs/Web/API/Window/getSelection#Related_objects.
* See: https://html.spec.whatwg.org/multipage/input.html#do-not-apply
*
* @param {Element} element The HTML element.
*
* @return {boolean} Whether the input/textareaa element has some "selection".
*/
export default function inputFieldHasUncollapsedSelection( element ) {
if ( ! isTextField( element ) && ! isNumberInput( element ) ) {
if ( ! isHTMLInputElement( element ) && ! isTextField( element ) ) {
return false;
}

// Safari throws a type error when trying to get `selectionStart` and
// `selectionEnd` on non-text <input> elements, so a try/catch construct is
// necessary.
try {
const {
selectionStart,
selectionEnd,
} = /** @type {HTMLInputElement | HTMLTextAreaElement} */ ( element );

return selectionStart !== null && selectionStart !== selectionEnd;
return (
// `null` means the input type doesn't implement selection, thus we
// cannot determine whether the selection is collapsed, so we
// default to true.
selectionStart === null ||
// when not null, compare the two points
selectionStart !== selectionEnd
);
} catch ( error ) {
// Safari throws an exception when trying to get `selectionStart`
// on non-text <input> elements (which, understandably, don't
// have the text selection API). We catch this via a try/catch
// block, as opposed to a more explicit check of the element's
// input types, because of Safari's non-standard behavior. This
// also means we don't have to worry about the list of input
// types that support `selectionStart` changing as the HTML spec
// evolves over time.
return false;
// This is Safari's way of saying that the input type doesn't implement
// selection, so we default to true.
return true;
}
}
2 changes: 1 addition & 1 deletion packages/dom/src/dom/is-html-input-element.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@
*/
export default function isHTMLInputElement( node ) {
/* eslint-enable jsdoc/valid-types */
return !! node && node.nodeName === 'INPUT';
return node?.nodeName === 'INPUT';
}
2 changes: 2 additions & 0 deletions packages/dom/src/dom/is-text-field.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ export default function isTextField( node ) {
'reset',
'submit',
'number',
'email',
'time',
];
return (
( isHTMLInputElement( node ) &&
Expand Down
10 changes: 3 additions & 7 deletions packages/dom/src/test/dom.js
Original file line number Diff line number Diff line change
Expand Up @@ -125,20 +125,16 @@ describe( 'DOM', () => {
'range',
'reset',
'submit',
'email',
'time',
];

/**
* A sampling of input types expected to be text eligible.
*
* @type {string[]}
*/
const TEXT_INPUT_TYPES = [
'text',
'password',
'search',
'url',
'email',
];
const TEXT_INPUT_TYPES = [ 'text', 'password', 'search', 'url' ];

it( 'should return false for non-text input elements', () => {
NON_TEXT_INPUT_TYPES.forEach( ( type ) => {
Expand Down

0 comments on commit 1bb122c

Please sign in to comment.