-
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
new_audit(tap-targets): check that tap targets are big enough and don't overlap #5846
Conversation
So there's good news and bad news. 👍 The good news is that everyone that needs to sign a CLA (the pull request submitter and all commit authors) have done so. Everything is all good there. 😕 The bad news is that it appears that one or more commits were authored or co-authored by someone other than the pull request submitter. We need to confirm that all authors are ok with their commits being contributed to this project. Please have them confirm that here in the pull request. Note to project maintainer: This is a terminal state, meaning the |
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'll do a more thorough lookthrough of the approach soon, just flushing some initial impressions!
/** | ||
* @param {Element} node | ||
*/ | ||
function isVisible({node, bcrs = getBCRs(node, false)}) { |
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.
this is a nice little generic helper! might be nice in page-functions.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.
we've also been trying to keep all our in-page functions there, but we're going to have so many for this function it's probably worth creating a dedicated file for them.
do you think it's easiest for it to be basically one giant in-page evaluation?
I ask because it's generally hard to debug and figure out what's going on in the in-page functions than the node side, but it looks like you've worked out a decent method of development here with the fs
call. There might also be some opportunity to share some BCR logic between audit and gatherer?
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've mostly been using a manually synced gist for development. It also gives a visual output, which we're hoping to add to the DevTools Audit panel one day.
Will move the BCR logic to a shared file.
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.
just trying to get a handle on a few things that might change impl quite a bit
lighthouse-core/gather/gatherers/dobetterweb/anchors-with-no-rel-noopener.js
Outdated
Show resolved
Hide resolved
57704d7
to
6afbf13
Compare
@patrickhulce For reference, here's a drawing that Rick made for how we detect a failure-causing overlap between two tap targets. The approach is taken from the Mobile-Friendly Test implementation.
Also, I think you noticed already, but the code is still very much work-in-progress, so don't spend too much time on the details 😂. |
df8a312
to
7be42d9
Compare
7be42d9
to
b3e068e
Compare
b3e068e
to
30669ce
Compare
fd2f9f0
to
366f033
Compare
Thanks for the initial feedback @brendankenny!
Let me know if you see a good way I could have split the PR into smaller bits. Might make future PRs easier 🙂. @patrickhulce already told me to split off the two smaller PRs for rect helpers and page functions. |
/** | ||
* @param {Element} element | ||
*/ | ||
function hasTextNodeSiblingsFormingTextBlock(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.
hasTextNodeSiblingsFormingTextBlock
doesn't rely on being closed over by elementIsInTextBlock
, so move outside to a top-level 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.
Makes sense, I was trying to avoid having another function that needs to be imported in the evalAsync string, but this is cleaner 👍
continue; | ||
} | ||
const siblingTextContent = (sibling.textContent || '').trim(); | ||
if (sibling.nodeType === Node.TEXT_NODE && siblingTextContent.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.
Will add a comment to the code.
great, that definitely helps
overflowX, | ||
overflowY, | ||
} = getComputedStyle(element); | ||
if ((overflowX === 'hidden' && overflowY === 'hidden') || element.children.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.
if all the client rects are size 0, does it matter whether or not the children would be visible since their client rects are also size 0 (since they're included in the allClientRectsEmpty(clientRects)
check)?
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.
No it shouldn't matter, and from trying it out on a few sites it doesn't matter. Will remove the if.
types/artifacts.d.ts
Outdated
/** The value of the <meta name="theme=color">'s content attribute, or null. */ | ||
ThemeColor: string|null; | ||
/** The value of the <meta name="viewport">'s content attribute, or null. */ | ||
Viewport: string|null; |
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.
ThemeColor
and Viewport
accidentally got merged back in 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.
feeling good :)
Some of the n^2 stuff in the audit could be an issue for nastier pages, but let's get good correctness coverage in the audit test and it'll be easy to follow up later to speed it up in those cases
* @return {LH.Audit.Product} | ||
*/ | ||
static audit(artifacts) { | ||
const hasViewportSet = ViewportAudit.audit(artifacts).rawValue; |
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 you file an issue to move the guts of the viewport
audit to a computed artifact? We should avoid audits calling each other if at all possible, but not worth adding to this PR
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.
return { | ||
rawValue: false, | ||
explanation: | ||
'Tap targets are too small because of a missing viewport config', |
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.
should this only happen with the missing meta tag? The audit rawValue
will also be false if the width
/initial-scale
check fails.
(if not, may need to tweak the explanation)
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.
Yeah it should fail if the viewport tag doesn't make it work for mobile. What do you think of this explanation:
Tap targets are too small because there's no viewport meta tag optimized for mobile screens
Changed it here and in the font size audit.
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.
What do you think of this explanation
sounds great!
* @returns {LH.Artifacts.TapTarget[]} | ||
*/ | ||
function getTooSmallTargets(targets) { | ||
return targets.filter(targetIsTooSmall); |
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 wasteful to split this up so much. Maybe
/**
* @param {LH.Artifacts.Rect} cr
*/
function clientRectBelowMinimumSize(cr) {
return cr.width < FINGER_SIZE_PX || cr.height < FINGER_SIZE_PX;
}
/**
* A target is "too small" if none of its clientRects are at least the size of a finger.
* @param {LH.Artifacts.TapTarget[]} targets
* @returns {LH.Artifacts.TapTarget[]}
*/
function getTooSmallTargets(targets) {
return targets.filter(target => {
return target.clientRects.every(clientRectBelowMinimumSize);
});
}
*/ | ||
function getOverlapFailureForTargetPair(tapTarget, maybeOverlappingTarget) { | ||
// Convert client rects to unique tappable areas from a user's perspective | ||
const tappableRects = getTappableRectsFromClientRects(tapTarget.clientRects); |
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 below the href
checks to where it's used (and maybe save a little getTappableRects
work :)
allTargets | ||
); | ||
|
||
if (overlappingTargets.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.
nit: since overlappingTargets
is always an array, no need to check the length before concating
tapTargetScore, | ||
overlappingTargetScore, | ||
overlapScoreRatio, | ||
}) => { |
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.
style nit: I don't personally find this very readable, not sure how others feel about it. You've lost track of the map()
by the time you get to the bottom of the destructured props :) I'd rather just have something like
const tableItems = overlapFailures.map(failure => {
const largestCR = getLargestRect(failure.tapTarget.clientRects);
// ...
});
types/audit.d.ts
Outdated
@@ -70,17 +70,32 @@ declare global { | |||
wastedPercent?: number; | |||
} | |||
|
|||
export interface TapTargetTableItem extends DetailsObject { |
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 this be moved to tap-targets.js
as well? Since it's a DetailsObject
and the table renderer can handle it, seems like it's ok to be internal to the audit until/unless we need it externally
TapTargets: tapTargets, | ||
MetaElements: [{ | ||
name: 'viewport', | ||
content: 'width=device-width', |
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.
There are a few cases that still need covering, e.g. need viewport failure test(s). See the red lines in https://coveralls.io/builds/21238984/source?filename=lighthouse-core%2Faudits%2Fseo%2Ftap-targets.js#L42
Thanks for the comments @brendankenny, can you take another look? |
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.
🖐 🎯 📐
LGTM!
Summary
This audit checks if all potential tap targets (see 1) are sized appropriately (see 2) and are far enough from each other to be unambiguous (see 3).
Limitations
a, button, input, …
) - this list may not include some tap targets that are made "tappable" via JS (e.g. some div used as a button) and may include some tap targets that don't cause any action (e.g. dummy button that does nothing)Demo (note that visual output is not part of this audit):
TODO
Related Issues/PRs
Closes #4358