-
Notifications
You must be signed in to change notification settings - Fork 9.4k
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
core(tap-targets): Helper functions for working with ClientRects #6703
core(tap-targets): Helper functions for working with ClientRects #6703
Conversation
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.
Code all LGTM! Just a request for some explanations so future peeps won't think they know better
Thanks again for splitting this up!! MUCH easier to see what's going on :D
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 | ||
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. | ||
*/ | ||
'use strict'; |
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.
maybe add a quick note to the functions that are executed in the browser too?
the instanbul ignore next
is a good clue but explicitly linking is even better :D
* @returns {LH.Artifacts.ClientRect[]} | ||
*/ | ||
function filterOutTinyClientRects(clientRects) { | ||
// 1x1px rect shouldn't be reason to treat the rect as something the user should tap 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.
A tad unfortunate that we're letting the tap targets bit leak out into here :/
maybe we can clarify a bit in simplifyClientRects that it's removing small ones?
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.
A tad unfortunate that we're letting the tap targets bit leak out into here :/
True... some of the logic isn't really useful in a more general way though, especially the simplifyClientRects
logic is very specific to this audit.
Maybe we can split it into two files, rectHelpers
and tapTargetClientRectHelpers
? And in the rectHelpers
we can address @hoten's point about renaming to just rects
then.
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've thought about this a bit more, and I'm actually somewhat convinced that this is something we'd probably want to do with anything that's looking at real objects on a page, so it's really just the "tap" language I think I'm pushing back a bit on.
I'm fine either way with A) just documenting in the jsdoc what exactly it does to the rects during simplification or B) splitting into two files as you suggested
const nonTinyClientRects = clientRects.filter( | ||
rect => rect.width > 1 && rect.height > 1 | ||
); | ||
if (nonTinyClientRects.length > 0) { |
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.
maybe add a note why we want to return the tiny ones anyway if it's empty?
*/ | ||
function filterOutClientRectsContainedByOthers(clientRects) { | ||
const filteredOutRects = new Set(); | ||
return clientRects.filter(cr => { |
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.
nit: the filter but also keeping state with a bool return has me a bit confused, maybe just refactor this to a normal double for
loop so we can just return the set at the end?
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.
We could keep track of the rects we want to keep instead of the ones we want to filter out, but then we'd need to infer what we decided to filter out based on that. I played around with some options, but I'm not quite sure what you have in mind.
Agreed that the current code doesn't read well.
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 was thinking the flip to rects to keep too
wouldn't the check just then be !rectsToKeep.has(possiblyContainingRect)
? or did I miss a step?
is the below the same?
for (const cr of clientRects) {
for (const possiblyContainingRect of clientRects) {
if (possiblyContainingRect === cr) continue
if (!rectsToKeep.has(possiblyContainingRect)) continue
if (rectContains()) { rectsToKeep.delete(cr); break; }
}
}
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.
Oh, in my mind I was adding to rectsToKeep instead of deleting from it. This is much more sensible 👍
* @param {LH.Artifacts.ClientRect} crB | ||
* @returns {boolean} | ||
*/ | ||
function clientRectsTouch(crA, crB) { |
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.
nit: this sounds a little more like clientRectsTouchOrOverlap
const crB = clientRects[j]; | ||
|
||
/** | ||
* Examples of what we want to merge: |
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.
Maybe a brief comment on why we wouldn't merge them if they overlap but don't align? perhaps this belongs on what simplify actually does, but it's not clear from this alone why it shouldn't merge them
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.
We want to merge two rects if the user would perceive them as a single tappable area to tap on. The user would intuitively tap on the center of the merged rect – we try to determine that rect and then place the simulated finger area in the middle.
Tried to make this a bit clearer in the code.
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.
Ah gotcha, thanks!
continue; | ||
} | ||
|
||
clientRects = clientRects.filter(cr => cr !== crA && cr !== crB); |
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.
maybe just a quick comment that we're replacing the two client rects with the merged version? the recursive call is a bit unexpected too, I'm guessing it was messy to deal with concurrent modification but a bit of explanation of heads up in function description would help
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.
the recursive call is a bit unexpected too
Yeah it felt simpler to just start over than modifying the indices and checking if the mutation introduces any edge cases. For client rects the arrays are also rarely more than 5 rects, so starting again doesn't cause perf issues.
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.
For client rects the arrays are also rarely more than 5 rects, so starting again doesn't cause perf issues.
👍 throwing that in a comment would also be ace :)
* @param {LH.Artifacts.ClientRect} rect2 | ||
*/ | ||
function getRectXOverlap(rect1, rect2) { | ||
// https:// stackoverflow.com/a/9325084/1290545 |
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.
nit: remove the space so this one's clickable :)
* @param {LH.Artifacts.ClientRect} rect2 | ||
*/ | ||
function getRectYOverlap(rect1, rect2) { | ||
// https:// stackoverflow.com/a/9325084/1290545 |
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.
nit: remove the space so this one's clickable :)
* @param {LH.Artifacts.ClientRect} clientRect | ||
* @param {number} fingerSize | ||
*/ | ||
function getFingerAtCenter(clientRect, fingerSize) { |
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.
nit: maybe rename from finger? getRectAtCenter
width: 100, | ||
height: 10, | ||
}), | ||
makeClientRect({ |
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.
would be really neat to replace all the x/y/w/h rect definitions with some sort of visual ascii
const input = makeRects(`
+--+--+--+
|..|..|..|
+--+--+--+
`);
const simple = simplifyClientRects(input)
assertEqual(simple, makeRect(`
+--------+
|........|
+--------+
`));
Obviously this is way more work. Maybe if we had dozens of these tests it'd be worth it :)
x: cr2.left, | ||
y: cr2.bottom, | ||
}; | ||
const bottomRight = { |
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.
maybe inline all these? Seems like rectContains
will be a hot function, and most calls would fail on the first part of the below condition. Could be worth it to minimize object allocations
@@ -0,0 +1,311 @@ | |||
/** |
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.
Can all instances of client
be removed? seems this could be generalized to just fun with rectangles.
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 point, I've split it into rect helpers and tap target audit specific code now.
const right = Math.max(crA.right, crB.right); | ||
const top = Math.min(crA.top, crB.top); | ||
const bottom = Math.max(crA.bottom, crB.bottom); | ||
const replacementClientRect = addRectWidthAndHeight({ |
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.
Move this stuff to getBoundingRect function.
c930e79
to
10c0e9f
Compare
@patrickhulce if you're happy then this can land. |
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.
sorrrrrrrrrrrry 😞 last thing I promise than I am so 🏅 on merging!
@@ -0,0 +1,246 @@ | |||
/** |
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.
can we get this filename aligned with the source one? no preference and not sure if you already had some refactoring done that makes one name easier than the other, so 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.
Sure, changed it to tappable-rects-test.js
.
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.
huzzah! awesome job @mattzeunert!! 🎉 thank you 🙏
Summary
Splitting this out from the main tap targets PR.
The tap targets gatherer collects a list of tappable targets and their client rects. The audit then checks if these client rects overlap.
The gatherer often collects multiple client rects for each tap target, but the client rects may overlap each other or can be ignored for some other reason. So
simplifyClientRects
prepares them to be consumed in the audit.There are also a few general helper functions related to working with client rects.
Related Issues/PRs
Tap targets ticket: #4358