Repository navigation
fix(watch-later): inject button outside aria-hidden anchor (#4305) - #4307
shoaibyazdani wants to merge 5 commits into
Conversation
wahajahmed010
left a comment
There was a problem hiding this comment.
Good catch on the a11y issue — the explanation is clear and the diff is minimal. Two concerns:
1. The new test is a static string assertion, not a behavioral test
expect(generalJs).not.toMatch(/thumbnail\.appendChild\s*\(\s*button\s*\)/);
expect(generalJs).toMatch(/(?:container|parentElement)\.appendChild\s*\(\s*button\s*\)/);This passes as long as the source contains those patterns somewhere — it doesn't verify the function actually appends to the right element at runtime. For example, if someone refactors addWatchLaterButton to a new helper but accidentally leaves a stale thumbnail.appendChild(button) in a comment or fallback branch, the test still passes. Consider asserting against the rendered DOM instead:
test('appends watch-later button to parent container, not thumbnail', () => {
document.body.innerHTML = `
<ytd-rich-item-renderer>
<a class="ytLockupViewModelContentImage" aria-hidden="true" href="/watch?v=abc"></a>
</ytd-rich-item-renderer>`;
const renderer = document.querySelector('ytd-rich-item-renderer');
const thumbnail = renderer.querySelector('a');
addWatchLaterButton(thumbnail);
expect(thumbnail.querySelector('button')).toBeNull(); // not direct child
expect(renderer.querySelector('button')).not.toBeNull(); // sibling-ish
});(Adjust to your actual test harness — but the principle is the same: assert behavior, not source.)
2. Edge case: thumbnail.parentElement === null
container = thumbnail ? thumbnail.parentElement : null and the early-return if (... && container && ...) correctly guards against null. But parentElement is only null when thumbnail is the document root, which won't happen for YouTube renderers in practice. Worth either:
- A short code comment documenting the assumption, or
- A test that asserts graceful behavior when
thumbnail.parentElementisnull.
3. Minor: hover semantics change
The author notes the hover area now extends to the entire renderer rather than just the link. If this regresses visual behavior (button appearing on areas that shouldn't trigger it), consider tightening the CSS selector as suggested in the PR body. Otherwise it's a fine trade-off.
The fix itself is correct and the surgical scope is appreciated. Lgtm modulo the test-quality concern.
|
Good call on the static-string test — replaced it with three behavioral tests that run the actual
The functions are extracted from Added CI status on this push will tell us if the jsdom install plays nicely with your existing runner setup. Happy to pin differently or move the tests to a separate suite if you'd prefer. |
…ity#4305) YouTube marks thumbnail anchors (e.g. <a class="ytLockupViewModelContentImage">) with aria-hidden="true". The Watch Later button was appended as a direct child of the anchor, so clicking the button triggered the browser's focus-block warning: Blocked aria-hidden on an element because its descendant retained focus. Append the button to the thumbnail's parent container (the renderer — ytd-rich-item-renderer, yt-lockup-view-model, etc.) instead. The button is positioned absolutely so visual placement is unchanged. - addWatchLaterButton: append to thumbnail.parentElement; track container for the click handler so it can be passed to findNativeWatchLaterButton (which uses closest() to locate the renderer internally anyway). - findNativeWatchLaterButton(container) still resolves the same renderer via closest(), so this is a no-op for the native-button lookup. - Existing hover selector *:hover>.it-watch-later-button still matches because the button's new direct parent (the renderer) is what gets hovered. - New test: 'injects button outside the aria-hidden thumbnail anchor (code-charity#4305)'.
…ertions (code-charity#4305) Replaces the regex-on-source test Wahaj flagged with three jsdom-based tests that actually verify behavior against the rendered DOM: - appends the button to the parent container, not the aria-hidden anchor (the core a11y fix — catches regressions) - does not duplicate the button on repeat calls (idempotency) - skips a thumbnail without a video id The behavioral tests extract addWatchLaterButton (+ its in-closure dependencies getVideoId and findNativeWatchLaterButton) from general.js and eval them in a jsdom context so the real function runs against a real DOM, not just source-text patterns. Adds jsdom@^22.1.0 as a devDependency (22.x is the last pre-ESM-deps release line that works cleanly with Jest 29).
Review follow-up on code-charity#4307. container comes from thumbnail.parentElement, which is null only for a detached node or the document root. Documents the assumption in code and adds a test asserting the function bails out rather than throwing.
6ba8c16 to
cd47a6e
Compare
wahajahmed010
left a comment
There was a problem hiding this comment.
Good catch and the right structural fix — moving the focusable button off the aria-hidden anchor onto the renderer root preserves the screen-reader semantics without changing layout (since the button is already absolutely positioned relative to a higher ancestor). Notes:
-
Hover-area widening is a real regression, not a follow-up. The PR description handwaves this with "slightly larger hover area" but the change is from "hover the thumbnail link" to "hover the renderer card" — for
gridviews that's potentially the entire card + title + meta strip, which is a noticeable UX change and conflicts with YouTube's own card-hover affordances. Before landing I'd want at least one of:- Tighten the CSS selector from
*:hover>.it-watch-later-buttonto a more specific selector that still scopes to the thumbnail's hover region (e.g.ytd-rich-item-renderer:hover > .it-watch-later-buttonplushtml[it-watch-later-buttons='always']). - Or: programmatically wrap the button in a hover-only span via a separate
mouseenter/mouseleavehandler so the selector stays*:hover>.it-watch-later-buttonbut the hover target is the original thumbnail. - Or: disclose the behavior change in the PR description and accept it as the trade-off.
Right now any user mouse-traversing the card will see the watch-later button appear, which is a noisier feel than the previous hover.
- Tighten the CSS selector from
-
container.contains(thumbnail.itWatchLaterButton)stale state on re-render. If YouTube replaces the renderer (SPA navigation, infinite scroll) the old button DOM node is gone butthumbnail.itWatchLaterButtonmay still reference it. The current guard correctly resets the property in that case, butfindNativeWatchLaterButton(container)will now find the old detached node if any module-level cache references it — worth verifying thatfindNativeWatchLaterButtondoes a freshquerySelectorrather than walking from a cached parent. -
button.dataset.idset where? I don't seedataset.id = videoIdin the visible diff. The new click handler readsthis.dataset.id; if that's set elsewhere (outside the snippet I have) fine, but the closure as shown only setspath.d,svg.appendChild,button.appendChild— noidassignment onbutton.dataset. Worth confirming the click handler actually receives a non-empty id. -
Test extraction is regex-based.
extractFndoes brace-counting on the source string to liftaddWatchLaterButtonand helpers. Regex-source-extraction works but is brittle: any nested function with the same name elsewhere in the file (or an unrelated function with the same prefix) would silently pick up the wrong one. A small refactor to exposeaddWatchLaterButtonfor tests (e.g.if (typeof module !== 'undefined') module.exports = { addWatchLaterButton };) would make the test resilient to source changes. -
Re-validating the a11y claim. The PR claim is "fixes the focus-blocked warning". Has anyone reproduced the original YouTube environment and confirmed no
aria-hiddenconsole warnings now appear with the fix? A short "before/after" dev-tools console screenshot in the PR description would help reviewers who can't run YouTube today.
The structural fix is right. #1 is the one that needs an explicit decision before merge.
|
Thanks for the careful read — fully agree on #1. Three honest options on the table, all defensible; not comfortable silently picking one without your call. My read of each:
I'd lean toward option 2 if you'd like the fix to match the previous UX, or option 3 (with a one-line note in the PR description) if YouTube's wider card-hover affordance is fine. Which would you like? I won't push a code change before you call it. |
…card @wahajahmed010's review on code-charity#4307 flagged that `*:hover>.it-watch-later-button` widens the hover region to the entire renderer card in grid views (title, meta strip, and adjacent cards), which is noisier than the original thumbnail-only affordance the user expects. Fix: replace the wildcard `*` selector with a specific renderer type list, gated on `:has(a#thumbnail:hover)` so the button only shows when the user is over the actual thumbnail anchor inside the renderer. /* before */ html[it-watch-later-buttons='hover'] *:hover>.it-watch-later-button /* after */ html[it-watch-later-buttons='hover'] :is(ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer, yt-lockup-view-model, ytd-playlist-video-renderer):has(a#thumbnail:hover) > .it-watch-later-button The `> .it-watch-later-button` combinator still matches because addWatchLaterButton appends the button to the renderer's direct children, not to the aria-hidden anchor. `:is()` and `:has()` are already used elsewhere in the same stylesheet so this does not introduce a new browser-support requirement. Review feedback addressed: - code-charity#1 (hover-area widening): chosen Option A, narrow selector to the renderer with a thumbnail-hovered condition. Visual affordance matches the pre-fix behavior the user expects. - code-charity#2 (stale state): not addressed here; the existing `!thumbnail.itWatchLaterButton` guard plus the per-call findNativeWatchLaterButton already cover the re-render case. - code-charity#3 (button.dataset.id set where?): not a real concern; the assignment is at the createButton site (line 481) and the click handler reads from `this.dataset.id`. - code-charity#4 (regex source extraction): not addressed here; the extraction is intentionally surgical to test the addWatchLaterButton function in isolation. An `if (typeof module !== 'undefined')` export is a larger refactor that the maintainer can do as a follow-up. - code-charity#5 (a11y validation): out of scope without a YouTube dev environment; the structural fix in the original commit is the load-bearing change. Updated unit test asserts the old wildcard selector is gone and the new scoped selector is present, so a future revert of the scoping change will fail the test. Refs code-charity#4307, addresses @wahajahmed010 review. Signed-off-by: Shoaib Latif <shoaibyazdani7@gmail.com>
|
Thanks for the careful review — addressed finding #1 in Picked Option A (tighten the selector) as the smallest correct change. The new selector: html[it-watch-later-buttons='hover'] :is(ytd-rich-item-renderer, ytd-video-renderer, ytd-grid-video-renderer, ytd-compact-video-renderer, yt-lockup-view-model, ytd-playlist-video-renderer):has(a#thumbnail:hover) > .it-watch-later-buttonTrade-off: I went with Visual result: the button now appears only when the cursor is over the actual thumbnail anchor inside a renderer, not when the user hovers the title, meta, or adjacent card. That's the same affordance the pre-PR behaviour had (when the button was a child of the For findings #2, #3, #4, #5, I dug in and noted the outcome in the commit message:
Updated the Not validated locally here (no jest in this environment; project's |
|
Re-review when you have a moment —
|
|
hi and thank you! @shoaibyazdani package-lock.json +673 Lines ? |
d883ffa to
e3d578b
Compare
The four DOM-behaviour tests needed jsdom, which pulled 52 transitive packages and added 673 lines to package-lock.json. This repo has jest but no DOM environment, and no existing unit test uses jsdom, so that was a new dependency introduced solely to support this PR's own test. Drop jsdom and those four tests; keep the four string/regex assertions that cover the selector scoping, init wiring, menu options and native control path. package.json and package-lock.json are now identical to upstream master (zero diff).
e3d578b to
3fe3bc6
Compare
|
Good catch — that's all
I added it to back four DOM-behaviour tests in Dropped in If you'd rather have the DOM tests, I can bring them back in a separate PR that adds jsdom as its own deliberate change rather than smuggling it in here. One more thing: the CI run on |
Summary
Fixes #4305.
YouTube marks thumbnail anchors (e.g.
<a class="ytLockupViewModelContentImage">) witharia-hidden="true". The Watch Later button was being appended as a direct child of the anchor viathumbnail.appendChild(button), so when a user clicked the button the browser blocked focus with:Appending the button to the thumbnail's parent container (the renderer —
ytd-rich-item-renderer/yt-lockup-view-model/ etc.) keeps the button focusable while preserving YouTube's a11y semantics.Why this works
position: absolute; top: 4px; right: 4px;and is already positioned relative to a higher ancestor (the renderer). Moving it from the anchor to the renderer's child list doesn't change visual placement.findNativeWatchLaterButton(container)works correctly because that helper usesclosest()to locate the renderer internally — passing the renderer directly just short-circuits the lookup.alwaysmode is unaffected. The existing*:hover>.it-watch-later-buttonselector still matches because the button's direct parent (the renderer) is now what gets hovered — so hover semantics still work, just with a slightly larger hover area. If reviewers prefer to preserve link-only hover, the selector can be tightened in a follow-up.Changes
js&css/extension/www.youtube.com/general/general.js—addWatchLaterButton: append tothumbnail.parentElementinstead ofthumbnail; trackcontainerso the click handler can pass it tofindNativeWatchLaterButton(which usesclosest()anyway, but passing the renderer explicitly is cleaner).tests/unit/watch-later-buttons.test.js— added a test asserting the button is not appended directly to the thumbnail anchor.Test
Notes
addWatchLaterButton) + one click handler reference.