Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

DOM: Allow copying text from non-text input elements #40192

Merged
merged 5 commits into from
May 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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',
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could any component by negatively affected by this spec correction?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I really dislike isTextField's exclusion approach. Since, in fact, most input types are not text fields, we might be better off with a stricter inclusion approach, i.e. [ 'text', ... ].includes( type ).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think anything would be negatively affected by this.. And I also think a reversed approach would be okay, but we would really need to make sure we have all the needed types 😄

'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