fix(controller): make overlay controls accessible

This commit is contained in:
2026-07-31 22:29:44 -04:00
parent 3ee970a54b
commit 0c9a6a1a1b
4 changed files with 51 additions and 35 deletions
+1 -1
View File
@@ -10,7 +10,7 @@ Constraints: local commits only; no push; no browser testing; one commit per fea
## Accessibility and usability
- [ ] Give in-player controls accessible names, keyboard behavior, and visible focus.
- [x] Give in-player controls accessible names, keyboard behavior, and visible focus.
- [ ] Make control-bar customization operable by keyboard as well as drag and drop.
- [ ] Label generated shortcut and site-rule form controls.
- [ ] Improve popup status announcements, focus indicators, and icon-search semantics.
+29 -33
View File
@@ -239,21 +239,25 @@ var controllerLocationStyles = {
/* `label` fallback only when ui-icons has no path for the action. */
var controllerButtonDefs = {
rewind: { label: "", className: "rw" },
slower: { label: "", className: "" },
faster: { label: "", className: "" },
advance: { label: "", className: "rw" },
display: { label: "", className: "hideButton" },
reset: { label: "\u21BB", className: "" },
fast: { label: "", className: "" },
nudge: { label: "", className: "" },
pause: { label: "", className: "" },
muted: { label: "", className: "" },
louder: { label: "", className: "" },
softer: { label: "", className: "" },
mark: { label: "", className: "" },
jump: { label: "", className: "" },
settings: { label: "", className: "" }
rewind: { label: "", name: "Rewind", className: "rw" },
slower: { label: "", name: "Decrease speed", className: "" },
faster: { label: "", name: "Increase speed", className: "" },
advance: { label: "", name: "Advance", className: "rw" },
display: {
label: "",
name: "Show or hide controller",
className: "hideButton"
},
reset: { label: "\u21BB", name: "Reset speed", className: "" },
fast: { label: "", name: "Toggle preferred speed", className: "" },
nudge: { label: "", name: "Toggle subtitle nudge", className: "" },
pause: { label: "", name: "Play or pause", className: "" },
muted: { label: "", name: "Mute or unmute", className: "" },
louder: { label: "", name: "Increase volume", className: "" },
softer: { label: "", name: "Decrease volume", className: "" },
mark: { label: "", name: "Mark position", className: "" },
jump: { label: "", name: "Jump to marked position", className: "" },
settings: { label: "", name: "Open Speeder settings", className: "" }
};
function createDefaultBinding(action, code, value) {
@@ -2601,7 +2605,11 @@ function setKeyBindings(action, value) {
function createControllerButton(doc, action, label, className) {
var button = doc.createElement("button");
var name = controllerButtonDefs[action] && controllerButtonDefs[action].name;
button.type = "button";
button.dataset.action = action;
button.setAttribute("aria-label", name || action);
button.title = name || action;
var custom =
tc.settings.customButtonIcons &&
tc.settings.customButtonIcons[action] &&
@@ -3873,11 +3881,14 @@ function defineVideoController() {
buttonConfig.forEach(function(btnId) {
if (btnId === "nudge") {
subtitleNudgeIndicator = doc.createElement("span");
subtitleNudgeIndicator = createControllerButton(
doc,
btnId,
controllerButtonDefs.nudge.label,
controllerButtonDefs.nudge.className
);
subtitleNudgeIndicator.id = "nudge-indicator";
subtitleNudgeIndicator.setAttribute("role", "button");
subtitleNudgeIndicator.setAttribute("aria-live", "polite");
subtitleNudgeIndicator.setAttribute("tabindex", "0");
controls.appendChild(subtitleNudgeIndicator);
} else {
var def = controllerButtonDefs[btnId];
@@ -3961,21 +3972,6 @@ function defineVideoController() {
true
);
});
if (subtitleNudgeIndicator) {
subtitleNudgeIndicator.addEventListener(
"click",
(e) => {
var video = this.video;
if (video) {
var newState = !isSubtitleNudgeEnabledForVideo(video);
setSubtitleNudgeEnabledForVideo(video, newState);
}
blurAfterPointerTap(subtitleNudgeIndicator, e);
e.stopPropagation();
},
true
);
}
controller.addEventListener("click", (e) => e.stopPropagation(), false);
controller.addEventListener("mousedown", (e) => e.stopPropagation(), false);
+6 -1
View File
@@ -334,10 +334,15 @@ button .vsc-btn-icon svg {
transform: translateY(0.5px);
}
button:focus {
button:focus:not(:focus-visible) {
outline: 0;
}
button:focus-visible {
outline: 2px solid #fff;
outline-offset: 2px;
}
button:hover {
opacity: 1;
}
+15
View File
@@ -896,6 +896,21 @@ describe("inject.js media/controller lifecycle regressions", () => {
expect(wrapper.classList.contains("vsc-idle-hidden")).toBe(true);
});
it("creates named native buttons for in-player controls", async () => {
bootInject({
syncData: { controllerButtons: ["rewind", "nudge"] }
});
await settleLifecycle();
const { wrapper } = createControlledVideo();
const rewind = wrapper.shadowRoot.querySelector('[data-action="rewind"]');
const nudge = wrapper.shadowRoot.querySelector("#nudge-indicator");
expect(rewind.tagName).toBe("BUTTON");
expect(rewind.getAttribute("aria-label")).toBe("Rewind");
expect(nudge.tagName).toBe("BUTTON");
expect(nudge.getAttribute("aria-label")).toContain("Subtitle nudge");
});
it("does not let YouTube auto-hide collapse controls under the pointer", async () => {
vi.useFakeTimers();
bootInject({