From a216836fd55046b4bd999fa2d945e7d4218f3441 Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Fri, 15 Sep 2023 16:32:16 +0200 Subject: [PATCH] [Editor] Add a button to trigger a dialog for adding an alt text (bug 1844952) --- l10n/en-US/viewer.properties | 5 + src/display/editor/editor.js | 71 ++++++++- src/display/editor/freetext.js | 16 +- src/display/editor/ink.js | 16 +- src/display/editor/stamp.js | 6 + src/display/editor/tools.js | 8 + web/annotation_editor_layer_builder.css | 200 ++++++++++++++++++++++++ web/annotation_editor_layer_builder.js | 1 + web/images/altText_add.svg | 3 + web/images/altText_done.svg | 3 + web/l10n_utils.js | 1 + 11 files changed, 304 insertions(+), 26 deletions(-) create mode 100644 web/images/altText_add.svg create mode 100644 web/images/altText_done.svg diff --git a/l10n/en-US/viewer.properties b/l10n/en-US/viewer.properties index db404c896..56ccca1cd 100644 --- a/l10n/en-US/viewer.properties +++ b/l10n/en-US/viewer.properties @@ -263,3 +263,8 @@ editor_stamp_add_image.title=Add image editor_free_text2_aria_label=Text Editor editor_ink2_aria_label=Draw Editor editor_ink_canvas_aria_label=User-created image + +# Alt-text dialog +# LOCALIZATION NOTE (alt_text_button_label): Alternative text (alt text) helps +# when people can't see the image. +alt_text_button_label=Alt text diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index 0641248fe..093402c66 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -34,6 +34,8 @@ import { FeatureTest, shadow, unreachable } from "../../shared/util.js"; * Base class for editors. */ class AnnotationEditor { + #altTextButton = null; + #keepAspectRatio = false; #resizersDiv = null; @@ -54,6 +56,8 @@ class AnnotationEditor { _focusEventsAllowed = true; + _l10nPromise = null; + #isDraggable = false; #zIndex = AnnotationEditor._zIndex++; @@ -64,6 +68,10 @@ class AnnotationEditor { static _zIndex = 1; + // When one of the dimensions of an editor is smaller than this value, the + // button to edit the alt text is visually moved outside of the editor. + static SMALL_EDITOR_SIZE = 0; + /** * @param {AnnotationEditorParameters} parameters */ @@ -124,9 +132,17 @@ class AnnotationEditor { /** * Initialize the l10n stuff for this type of editor. - * @param {Object} _l10n + * @param {Object} l10n */ - static initialize(_l10n) { + static initialize(l10n, options = null) { + AnnotationEditor._l10nPromise ||= new Map( + ["alt_text_button_label"].map(str => [str, l10n.get(str)]) + ); + if (options?.strings) { + for (const str of options.strings) { + AnnotationEditor._l10nPromise.set(str, l10n.get(str)); + } + } if (AnnotationEditor._borderLineWidth !== -1) { return; } @@ -522,6 +538,11 @@ class AnnotationEditor { if (!this.#keepAspectRatio) { this.div.style.height = `${((100 * height) / parentHeight).toFixed(2)}%`; } + this.#altTextButton?.classList.toggle( + "small", + width < AnnotationEditor.SMALL_EDITOR_SIZE || + height < AnnotationEditor.SMALL_EDITOR_SIZE + ); } fixDims() { @@ -785,6 +806,40 @@ class AnnotationEditor { this.fixAndSetPosition(); } + addAltTextButton() { + if (this.#altTextButton) { + return; + } + const altText = (this.#altTextButton = document.createElement("span")); + altText.className = "altText"; + AnnotationEditor._l10nPromise.get("alt_text_button_label").then(msg => { + altText.textContent = msg; + }); + altText.tabIndex = "0"; + altText.addEventListener( + "click", + event => { + event.preventDefault(); + }, + { capture: true } + ); + altText.addEventListener("keydown", event => { + if (event.target === altText && event.key === "Enter") { + event.preventDefault(); + } + }); + this.div.append(altText); + if (!AnnotationEditor.SMALL_EDITOR_SIZE) { + // We take the width of the alt text button and we add 40% to it to be + // sure to have enough space for it. + const PERCENT = 40; + AnnotationEditor.SMALL_EDITOR_SIZE = Math.min( + 128, + Math.round(altText.getBoundingClientRect().width * (1 + PERCENT / 100)) + ); + } + } + /** * Render this editor in a div. * @returns {HTMLDivElement} @@ -1144,13 +1199,21 @@ class AnnotationEditor { * When the user disables the editing mode some editors can change some of * their properties. */ - disableEditing() {} + disableEditing() { + if (this.#altTextButton) { + this.#altTextButton.hidden = true; + } + } /** * When the user enables the editing mode some editors can change some of * their properties. */ - enableEditing() {} + enableEditing() { + if (this.#altTextButton) { + this.#altTextButton.hidden = false; + } + } /** * The editor is about to be edited. diff --git a/src/display/editor/freetext.js b/src/display/editor/freetext.js index 385492c0c..efad76948 100644 --- a/src/display/editor/freetext.js +++ b/src/display/editor/freetext.js @@ -56,8 +56,6 @@ class FreeTextEditor extends AnnotationEditor { static _freeTextDefaultContent = ""; - static _l10nPromise; - static _internalPadding = 0; static _defaultColor = null; @@ -145,13 +143,9 @@ class FreeTextEditor extends AnnotationEditor { /** @inheritdoc */ static initialize(l10n) { - super.initialize(l10n); - this._l10nPromise = new Map( - ["free_text2_default_content", "editor_free_text2_aria_label"].map( - str => [str, l10n.get(str)] - ) - ); - + AnnotationEditor.initialize(l10n, { + strings: ["free_text2_default_content", "editor_free_text2_aria_label"], + }); const style = getComputedStyle(document.documentElement); if (typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING")) { @@ -548,11 +542,11 @@ class FreeTextEditor extends AnnotationEditor { this.editorDiv.setAttribute("id", this.#editorDivId); this.enableEditing(); - FreeTextEditor._l10nPromise + AnnotationEditor._l10nPromise .get("editor_free_text2_aria_label") .then(msg => this.editorDiv?.setAttribute("aria-label", msg)); - FreeTextEditor._l10nPromise + AnnotationEditor._l10nPromise .get("free_text2_default_content") .then(msg => this.editorDiv?.setAttribute("default-content", msg)); this.editorDiv.contentEditable = true; diff --git a/src/display/editor/ink.js b/src/display/editor/ink.js index 7651b5451..f56929b05 100644 --- a/src/display/editor/ink.js +++ b/src/display/editor/ink.js @@ -62,8 +62,6 @@ class InkEditor extends AnnotationEditor { static _defaultThickness = 1; - static _l10nPromise; - static _type = "ink"; constructor(params) { @@ -84,13 +82,9 @@ class InkEditor extends AnnotationEditor { /** @inheritdoc */ static initialize(l10n) { - super.initialize(l10n); - this._l10nPromise = new Map( - ["editor_ink_canvas_aria_label", "editor_ink2_aria_label"].map(str => [ - str, - l10n.get(str), - ]) - ); + AnnotationEditor.initialize(l10n, { + strings: ["editor_ink_canvas_aria_label", "editor_ink2_aria_label"], + }); } /** @inheritdoc */ @@ -743,7 +737,7 @@ class InkEditor extends AnnotationEditor { this.canvas.width = this.canvas.height = 0; this.canvas.className = "inkEditorCanvas"; - InkEditor._l10nPromise + AnnotationEditor._l10nPromise .get("editor_ink_canvas_aria_label") .then(msg => this.canvas?.setAttribute("aria-label", msg)); this.div.append(this.canvas); @@ -782,7 +776,7 @@ class InkEditor extends AnnotationEditor { super.render(); - InkEditor._l10nPromise + AnnotationEditor._l10nPromise .get("editor_ink2_aria_label") .then(msg => this.div?.setAttribute("aria-label", msg)); diff --git a/src/display/editor/stamp.js b/src/display/editor/stamp.js index 61cb48fec..62651d51a 100644 --- a/src/display/editor/stamp.js +++ b/src/display/editor/stamp.js @@ -50,6 +50,11 @@ class StampEditor extends AnnotationEditor { this.#bitmapFile = params.bitmapFile; } + /** @inheritdoc */ + static initialize(l10n) { + AnnotationEditor.initialize(l10n); + } + static get supportedTypes() { // See https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types // to know which types are supported by the browser. @@ -306,6 +311,7 @@ class StampEditor extends AnnotationEditor { this.parent.addUndoableEditor(this); this.#hasBeenAddedInUndoStack = true; } + this.addAltTextButton(); } /** diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index f0177494d..47c26e88d 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -741,6 +741,14 @@ class AnnotationEditorUIManager { ); } + get direction() { + return shadow( + this, + "direction", + getComputedStyle(this.#container).direction + ); + } + onPageChanging({ pageNumber }) { this.#currentPageIndex = pageNumber - 1; } diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index abf1a2a11..2b7e3d621 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -37,6 +37,22 @@ /*#else*/ --editorInk-editing-cursor: url(images/cursor-editorInk.svg) 0 16, pointer; /*#endif*/ + + --alt-text-add-image: url(images/altText_add.svg); + --alt-text-done-image: url(images/altText_done.svg); + --alt-text-bg-color: #2b2a33; + --alt-text-fg-color: #fbfbfe; + --alt-text-border-color: var(--alt-text-bg-color); + --alt-text-hover-bg-color: #52525e; + --alt-text-hover-fg-color: var(--alt-text-fg-color); + --alt-text-hover-border-color: var(--alt-text-hover-bg-color); + --alt-text-active-bg-color: #5b5b66; + --alt-text-active-fg-color: var(--alt-text-fg-color); + --alt-text-active-border-color: var(--alt-text-hover-bg-color); + --alt-text-focus-outline-color: #0060df; + --alt-text-focus-border-color: #f0f0f4; + --alt-text-shadow: 0 2px 6px 0 rgba(28, 27, 34, 0.5); + --alt-text-opacity: 0.8; } @media (min-resolution: 1.1dppx) { @@ -53,6 +69,20 @@ --outline-color: CanvasText; --outline-around-color: ButtonFace; --resizer-bg-color: ButtonText; + + --alt-text-bg-color: Canvas; + --alt-text-fg-color: ButtonText; + --alt-text-border-color: ButtonText; + --alt-text-hover-bg-color: Canvas; + --alt-text-hover-fg-color: SelectedItem; + --alt-text-hover-border-color: SelectedItem; + --alt-text-active-bg-color: ButtonFace; + --alt-text-active-fg-color: SelectedItem; + --alt-text-active-border-color: ButtonText; + --alt-text-focus-outline-color: CanvasText; + --alt-text-focus-border-color: ButtonText; + --alt-text-shadow: none; + --alt-text-opacity: 1; } } @@ -331,4 +361,174 @@ } } } + + & + :is( + [data-main-rotation="0"] [data-editor-rotation="90"], + [data-main-rotation="90"] [data-editor-rotation="0"], + [data-main-rotation="180"] [data-editor-rotation="270"], + [data-main-rotation="270"] [data-editor-rotation="180"], + + ) { + & .altText { + rotate: 270deg; + + &:dir(ltr) { + inset-inline-start: calc(100% - 8px); + + &.small { + inset-inline-start: calc(100% + 8px); + inset-block-start: 100%; + } + } + + &:dir(rtl) { + inset-block-end: calc(100% - 8px); + + &.small { + inset-inline-start: -8px; + inset-block-start: 0; + } + } + } + } + + & + :is( + [data-main-rotation="0"] [data-editor-rotation="180"], + [data-main-rotation="90"] [data-editor-rotation="90"], + [data-main-rotation="180"] [data-editor-rotation="0"], + [data-main-rotation="270"] [data-editor-rotation="270"], + + ) { + & .altText { + rotate: 180deg; + + inset-block-end: calc(100% - 8px); + inset-inline-start: calc(100% - 8px); + + &.small { + inset-inline-start: 100%; + inset-block-start: -8px; + } + } + } + + & + :is( + [data-main-rotation="0"] [data-editor-rotation="270"], + [data-main-rotation="90"] [data-editor-rotation="180"], + [data-main-rotation="180"] [data-editor-rotation="90"], + [data-main-rotation="270"] [data-editor-rotation="0"], + + ) { + & .altText { + rotate: 90deg; + + &:dir(ltr) { + inset-block-end: calc(100% - 8px); + + &.small { + inset-inline-start: -8px; + inset-block-start: 0; + } + } + + &:dir(rtl) { + inset-inline-start: calc(100% - 8px); + + &.small { + inset-inline-start: calc(100% + 8px); + inset-block-start: 100%; + } + } + } + } +} + +.altText { + display: flex; + align-items: center; + justify-content: center; + width: auto; + height: 24px; + min-width: 88px; + z-index: 1; + pointer-events: all; + + color: var(--alt-text-fg-color); + font: menu; + font-size: 12px; + border-radius: 4px; + border: 1px solid var(--alt-text-border-color); + opacity: var(--alt-text-opacity); + background-color: var(--alt-text-bg-color); + box-shadow: var(--alt-text-shadow); + + position: absolute; + inset-block-end: 8px; + inset-inline-start: 8px; + + &:dir(ltr) { + transform-origin: 0 100%; + } + &:dir(rtl) { + transform-origin: 100% 100%; + } + + &.small { + &:dir(ltr) { + transform-origin: 0 0; + } + &:dir(rtl) { + transform-origin: 100% 0; + } + + inset-block-end: unset; + inset-inline-start: 0; + inset-block-start: calc(100% + 8px); + } + + &:hover { + background-color: var(--alt-text-hover-bg-color); + border-color: var(--alt-text-hover-border-color); + color: var(--alt-text-hover-fg-color); + cursor: pointer; + + &::before { + background-color: var(--alt-text-hover-fg-color); + } + } + + &:active { + background-color: var(--alt-text-active-bg-color); + border-color: var(--alt-text-active-border-color); + color: var(--alt-text-active-fg-color); + + &::before { + background-color: var(--alt-text-active-fg-color); + } + } + + &:focus-visible { + outline: 2px solid var(--alt-text-focus-outline-color); + border-color: var(--alt-text-focus-border-color); + } + + &::before { + content: ""; + mask-image: var(--alt-text-add-image); + mask-size: cover; + mask-repeat: no-repeat; + mask-position: center; + display: inline-block; + width: 12px; + height: 12px; + background-color: var(--alt-text-fg-color); + margin-inline-end: 4px; + } + + &.done::before { + mask-image: var(--alt-text-done-image); + } } diff --git a/web/annotation_editor_layer_builder.js b/web/annotation_editor_layer_builder.js index a15ffa79a..a6f1bbe08 100644 --- a/web/annotation_editor_layer_builder.js +++ b/web/annotation_editor_layer_builder.js @@ -82,6 +82,7 @@ class AnnotationEditorLayerBuilder { div.className = "annotationEditorLayer"; div.tabIndex = 0; div.hidden = true; + div.dir = this.#uiManager.direction; this.pageDiv.append(div); this.annotationEditorLayer = new AnnotationEditorLayer({ diff --git a/web/images/altText_add.svg b/web/images/altText_add.svg new file mode 100644 index 000000000..3451b536c --- /dev/null +++ b/web/images/altText_add.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/images/altText_done.svg b/web/images/altText_done.svg new file mode 100644 index 000000000..f54924ebf --- /dev/null +++ b/web/images/altText_done.svg @@ -0,0 +1,3 @@ + + + diff --git a/web/l10n_utils.js b/web/l10n_utils.js index 11663c22f..cc8b7d5c2 100644 --- a/web/l10n_utils.js +++ b/web/l10n_utils.js @@ -82,6 +82,7 @@ const DEFAULT_L10N_STRINGS = { editor_free_text2_aria_label: "Text Editor", editor_ink2_aria_label: "Draw Editor", editor_ink_canvas_aria_label: "User-created image", + alt_text_button_label: "Alt text", }; if (typeof PDFJSDev === "undefined" || !PDFJSDev.test("MOZCENTRAL")) { DEFAULT_L10N_STRINGS.print_progress_percent = "{{progress}}%";