pdf.js/src/display/editor/alt_text.js
Calixte Denizet 7fce3eac93 [Editor] Fix the role of the different editors in order to make them interactive elements (bug 1953290)
Having some interactive elements forces the screen readers to switch to form mode
and consequently they delegate the keyboard stuff to the browser.
This patch sets an aria label on each editor in order to have a better description than just
'application'.
2025-03-12 22:19:31 +01:00

339 lines
8.8 KiB
JavaScript

/* Copyright 2023 Mozilla Foundation
*
* 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.
*/
import { noContextMenu } from "../display_utils.js";
class AltText {
#altText = null;
#altTextDecorative = false;
#altTextButton = null;
#altTextButtonLabel = null;
#altTextTooltip = null;
#altTextTooltipTimeout = null;
#altTextWasFromKeyBoard = false;
#badge = null;
#editor = null;
#guessedText = null;
#textWithDisclaimer = null;
#useNewAltTextFlow = false;
static #l10nNewButton = null;
static _l10n = null;
constructor(editor) {
this.#editor = editor;
this.#useNewAltTextFlow = editor._uiManager.useNewAltTextFlow;
AltText.#l10nNewButton ||= Object.freeze({
added: "pdfjs-editor-new-alt-text-added-button",
"added-label": "pdfjs-editor-new-alt-text-added-button-label",
missing: "pdfjs-editor-new-alt-text-missing-button",
"missing-label": "pdfjs-editor-new-alt-text-missing-button-label",
review: "pdfjs-editor-new-alt-text-to-review-button",
"review-label": "pdfjs-editor-new-alt-text-to-review-button-label",
});
}
static initialize(l10n) {
AltText._l10n ??= l10n;
}
async render() {
const altText = (this.#altTextButton = document.createElement("button"));
altText.className = "altText";
altText.tabIndex = "0";
const label = (this.#altTextButtonLabel = document.createElement("span"));
altText.append(label);
if (this.#useNewAltTextFlow) {
altText.classList.add("new");
altText.setAttribute("data-l10n-id", AltText.#l10nNewButton.missing);
label.setAttribute(
"data-l10n-id",
AltText.#l10nNewButton["missing-label"]
);
} else {
altText.setAttribute("data-l10n-id", "pdfjs-editor-alt-text-button");
label.setAttribute("data-l10n-id", "pdfjs-editor-alt-text-button-label");
}
const signal = this.#editor._uiManager._signal;
altText.addEventListener("contextmenu", noContextMenu, { signal });
altText.addEventListener("pointerdown", event => event.stopPropagation(), {
signal,
});
const onClick = event => {
event.preventDefault();
this.#editor._uiManager.editAltText(this.#editor);
if (this.#useNewAltTextFlow) {
this.#editor._reportTelemetry({
action: "pdfjs.image.alt_text.image_status_label_clicked",
data: { label: this.#label },
});
}
};
altText.addEventListener("click", onClick, { capture: true, signal });
altText.addEventListener(
"keydown",
event => {
if (event.target === altText && event.key === "Enter") {
this.#altTextWasFromKeyBoard = true;
onClick(event);
}
},
{ signal }
);
await this.#setState();
return altText;
}
get #label() {
return (
(this.#altText && "added") ||
(this.#altText === null && this.guessedText && "review") ||
"missing"
);
}
finish() {
if (!this.#altTextButton) {
return;
}
this.#altTextButton.focus({ focusVisible: this.#altTextWasFromKeyBoard });
this.#altTextWasFromKeyBoard = false;
}
isEmpty() {
if (this.#useNewAltTextFlow) {
return this.#altText === null;
}
return !this.#altText && !this.#altTextDecorative;
}
hasData() {
if (this.#useNewAltTextFlow) {
return this.#altText !== null || !!this.#guessedText;
}
return this.isEmpty();
}
get guessedText() {
return this.#guessedText;
}
async setGuessedText(guessedText) {
if (this.#altText !== null) {
// The user provided their own alt text, so we don't want to overwrite it.
return;
}
this.#guessedText = guessedText;
this.#textWithDisclaimer = await AltText._l10n.get(
"pdfjs-editor-new-alt-text-generated-alt-text-with-disclaimer",
{ generatedAltText: guessedText }
);
this.#setState();
}
toggleAltTextBadge(visibility = false) {
if (!this.#useNewAltTextFlow || this.#altText) {
this.#badge?.remove();
this.#badge = null;
return;
}
if (!this.#badge) {
const badge = (this.#badge = document.createElement("div"));
badge.className = "noAltTextBadge";
this.#editor.div.append(badge);
}
this.#badge.classList.toggle("hidden", !visibility);
}
serialize(isForCopying) {
let altText = this.#altText;
if (!isForCopying && this.#guessedText === altText) {
altText = this.#textWithDisclaimer;
}
return {
altText,
decorative: this.#altTextDecorative,
guessedText: this.#guessedText,
textWithDisclaimer: this.#textWithDisclaimer,
};
}
get data() {
return {
altText: this.#altText,
decorative: this.#altTextDecorative,
};
}
/**
* Set the alt text data.
*/
set data({
altText,
decorative,
guessedText,
textWithDisclaimer,
cancel = false,
}) {
if (guessedText) {
this.#guessedText = guessedText;
this.#textWithDisclaimer = textWithDisclaimer;
}
if (this.#altText === altText && this.#altTextDecorative === decorative) {
return;
}
if (!cancel) {
this.#altText = altText;
this.#altTextDecorative = decorative;
}
this.#setState();
}
toggle(enabled = false) {
if (!this.#altTextButton) {
return;
}
if (!enabled && this.#altTextTooltipTimeout) {
clearTimeout(this.#altTextTooltipTimeout);
this.#altTextTooltipTimeout = null;
}
this.#altTextButton.disabled = !enabled;
}
shown() {
this.#editor._reportTelemetry({
action: "pdfjs.image.alt_text.image_status_label_displayed",
data: { label: this.#label },
});
}
destroy() {
this.#altTextButton?.remove();
this.#altTextButton = null;
this.#altTextButtonLabel = null;
this.#altTextTooltip = null;
this.#badge?.remove();
this.#badge = null;
}
async #setState() {
const button = this.#altTextButton;
if (!button) {
return;
}
if (this.#useNewAltTextFlow) {
button.classList.toggle("done", !!this.#altText);
button.setAttribute("data-l10n-id", AltText.#l10nNewButton[this.#label]);
this.#altTextButtonLabel?.setAttribute(
"data-l10n-id",
AltText.#l10nNewButton[`${this.#label}-label`]
);
if (!this.#altText) {
this.#altTextTooltip?.remove();
return;
}
} else {
if (!this.#altText && !this.#altTextDecorative) {
button.classList.remove("done");
this.#altTextTooltip?.remove();
return;
}
button.classList.add("done");
button.setAttribute("data-l10n-id", "pdfjs-editor-alt-text-edit-button");
}
let tooltip = this.#altTextTooltip;
if (!tooltip) {
this.#altTextTooltip = tooltip = document.createElement("span");
tooltip.className = "tooltip";
tooltip.setAttribute("role", "tooltip");
tooltip.id = `alt-text-tooltip-${this.#editor.id}`;
const DELAY_TO_SHOW_TOOLTIP = 100;
const signal = this.#editor._uiManager._signal;
signal.addEventListener(
"abort",
() => {
clearTimeout(this.#altTextTooltipTimeout);
this.#altTextTooltipTimeout = null;
},
{ once: true }
);
button.addEventListener(
"mouseenter",
() => {
this.#altTextTooltipTimeout = setTimeout(() => {
this.#altTextTooltipTimeout = null;
this.#altTextTooltip.classList.add("show");
this.#editor._reportTelemetry({
action: "alt_text_tooltip",
});
}, DELAY_TO_SHOW_TOOLTIP);
},
{ signal }
);
button.addEventListener(
"mouseleave",
() => {
if (this.#altTextTooltipTimeout) {
clearTimeout(this.#altTextTooltipTimeout);
this.#altTextTooltipTimeout = null;
}
this.#altTextTooltip?.classList.remove("show");
},
{ signal }
);
}
if (this.#altTextDecorative) {
tooltip.setAttribute(
"data-l10n-id",
"pdfjs-editor-alt-text-decorative-tooltip"
);
} else {
tooltip.removeAttribute("data-l10n-id");
tooltip.textContent = this.#altText;
}
if (!tooltip.parentNode) {
button.append(tooltip);
}
const element = this.#editor.getElementForAltText();
element?.setAttribute("aria-describedby", tooltip.id);
}
}
export { AltText };