-
Notifications
You must be signed in to change notification settings - Fork 4.5k
dom: Add types to document-has-selection #30386
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
Conversation
isTextField( doc.activeElement ) || | ||
isNumberInput( doc.activeElement ) || | ||
documentHasTextSelection( doc ) | ||
!! doc.activeElement && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If a document has no activeElement
then it cannot have a selection. Furthermore, isTextField
and isNumberInput
access the passed in element completely unguarded, so the assumption here was always that activeElement
was defined.
const range = selection.rangeCount ? selection.getRangeAt( 0 ) : null; | ||
return range && ! range.collapsed; | ||
return !! range && ! range.collapsed; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
range
is not a boolean so we cast it to a boolean to appease the return value.
@@ -23,7 +23,9 @@ export default function isTextField( element ) { | |||
'number', | |||
]; | |||
return ( | |||
( nodeName === 'INPUT' && ! nonTextInputs.includes( element.type ) ) || | |||
( nodeName === 'INPUT' && | |||
element.type && |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nonTextInputs
is a string[]
and because element.type
could be undefined | string
it doesn't fit in the box of a string[]
. Checking for it's presence first turns it into a string
which we can then pass to the includes
function on the next line.
Size Change: +19 B (0%) Total Size: 1.43 MB
ℹ️ View Unchanged
|
packages/dom/README.md
Outdated
@@ -204,7 +204,7 @@ and has a valueAsNumber | |||
|
|||
_Parameters_ | |||
|
|||
- _element_ `HTMLElement`: The HTML element. | |||
- _element_ `Partial<HTMLInputElement>`: The HTML element. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pretty new to this. Why is Partial
needed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Partial
is needed because the Element
itself could be any HTML element that we're testing to check to see if it's an input, textarea or contentEditable
Element. I wasn't sure how else to express that the Input
part is optional and I think this is what @sirreal suggested as a potential solution to this problem (though I may be mis remembering, I can't find the original comment where that was suggested.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this was introduced because we have some type that HTMLInputElement
extends, but we don't know that it's an HTMLInputElement
.
Partial<HTMLInputElement>
doesn't seem ideal to me, it's typically used when we have an interface with optional properties. This usage is really that we have an Element
(I believe, could be another type) and want to use it safely if it satisfies HTMLInputElement
.
In my opinion, a better approach would be to accept the element type we expect to pass in (Element
?), and use a type guard to narrow our element to our desired type. I implemented a type guard like this in #29220 to handle this type of case:
gutenberg/packages/dom/src/dom.js
Lines 83 to 89 in 29601bd
/** | |
* @param {Element} element Element to test | |
* @return {element is HTMLTextAreaElement | HTMLInputElement} True if element is Input or TextArea | |
*/ | |
function isInputOrTextAreaElement( element ) { | |
return 'INPUT' === element.tagName || 'TEXTAREA' === element.tagName; | |
} |
@sarayourfriend I believe you're thinking of this comment: #30030 (comment)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Right, maybe just HTMLElement
? We should allow any element to be checked with this function.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Aha, yes, I did try to create a type guard but got stuck on contentEditable
... I'll revisit this though and see if I can make it work with a type guard.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Meaning: sometimes the flexibility is nice so you don't have to check the type before calling a function that's being strict about the type. I think this is generally good for all functions in this package.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I just needed a couple type casts and it works out well even without the type guard.
8eca339
to
1201ddd
Compare
packages/dom/README.md
Outdated
@@ -204,7 +204,7 @@ and has a valueAsNumber | |||
|
|||
_Parameters_ | |||
|
|||
- _element_ `HTMLElement`: The HTML element. | |||
- _element_ `Element`: The HTML element. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HTMLElement
doesn't work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It seems like HTMLElement
is a narrow type than is necessary. From the documentation on Element
:
Element
is the most general base class from which all objects in aDocument
inherit. It only has methods and properties common to all kinds of elements. More specific classes inherit fromElement
Whereas HTMLElement
is:
Any HTML element. Some elements directly implement this interface, while others implement it via an interface that inherits it.
Maybe it should be left as HTMLElement
instead? 🤔 I'm not sure 😬 I'll defer to @sirreal again on this one, sorry to pull you back in Jon!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
AS far as I know, Element
can also be SVGElement
and perhaps custom types. Just Element
sounds good though. 🤷♀️
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'd say it's good practice to generalize parameter types as much as possible. That will limit what we can do with the values internally. Externally, it will allow these functions to be used in more places.
For example, this function checks the nodeName
property, which exists on Node
(from which Element inherits):
export default function isHTMLInputElement( node: Node ): node is HTMLInputElement {
return node.nodeName === 'INPUT';
}
That seems ideal to me. The parameter is exactly as strict as it must be for it to be used internally.
@@ -0,0 +1,9 @@ | |||
/* eslint-disable jsdoc/valid-types */ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it disabled here and enabled below?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because the element is HTMLInputElement
syntax isn't considered a "valid type" by the JSDoc parser (doctrine) that the eslint JSDoc rules use. If you search the codebase you'll see that we've unfortunately had to disable these all over the place.
@@ -0,0 +1,9 @@ | |||
/* eslint-disable jsdoc/valid-types */ | |||
/** | |||
* @param {Element | null | undefined} element |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Are we planning to pass null
or undefined
? I see in isNumberInput
we already require an Element
, so can't we require it here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If this function is used in other places it might be helpful to also handle null
and undefined
correctly. On the other hand, might be a YAGNI situation. I'll leave it up to you.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's easy to widen types at any point, but a breaking change to narrow them. If this is ever released accepting X | null | undefined
, that's more difficult to narrow to X
than the other way around. A function that accepts X
can start the wider accepting X | null | undefined
at any point.
I agree, it might be handy to accept null
for taking input from things like Document.querySelector
or Node.parentNode
that may return null
. However, I'd avoid widening the type until it's absolutely necessary.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not super comfortable with the typing and the bigger picture/effort, so maybe good to have a second blessing. 😄
I requested review from @tyxla who's been helping with these recently :) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks great to me! 👍
I wonder why the lint check failed, though - I didn't see any related failures.
e3de612
to
446f03b
Compare
Not sure @tyxla. I rebased, hopefully that will resolve the error coming from the interface package 🤞 |
Well after rebasing now the e2e tests are failing 😞 I'm digging into it now. I think the test is failing on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good stuff here, thanks! There are some interesting API design questions around accepting null | undefined values.
I've left some notes but nothing blocking.
ea3d5b4
to
d70d734
Compare
d70d734
to
3d652fc
Compare
Description
Add types to
document-has-selection
and its three dependencies.Some runtime changes are necessary to appease TypeScript. I've annotated each with the reason it is needed and why I think it's safe.
Part of #18838
How has this been tested?
Unit tests and e2e tests.
Types of changes
Non-breaking changes.
Checklist:
*.native.js
files for terms that need renaming or removal).