From 059f93e9774708812c80f0061c3548cb0a33d21d Mon Sep 17 00:00:00 2001 From: Calixte Denizet Date: Mon, 11 Mar 2024 17:03:44 +0100 Subject: [PATCH] [Editor] Make the text layer focusable before the editors (bug 1881746) Keep the different layers in a constant order to avoid the use of a z-index and a tab-index. --- src/display/editor/annotation_editor_layer.js | 4 ++ src/display/editor/editor.js | 10 +++- src/display/editor/tools.js | 6 +++ test/integration/highlight_editor_spec.mjs | 50 +++++++++++++++++++ test/integration/test_utils.mjs | 20 ++++++++ web/annotation_editor_layer_builder.css | 1 - web/annotation_editor_layer_builder.js | 7 +-- web/annotation_layer_builder.css | 1 - web/annotation_layer_builder.js | 7 +-- web/pdf_page_view.js | 38 ++++++++++++-- web/pdf_viewer.css | 1 - web/text_layer_builder.css | 1 - web/text_layer_builder.js | 1 + 13 files changed, 127 insertions(+), 20 deletions(-) diff --git a/src/display/editor/annotation_editor_layer.js b/src/display/editor/annotation_editor_layer.js index 367236947c8110..2ad0e67790900f 100644 --- a/src/display/editor/annotation_editor_layer.js +++ b/src/display/editor/annotation_editor_layer.js @@ -237,6 +237,7 @@ class AnnotationEditorLayer { * editor creation. */ enable() { + this.div.tabIndex = 0; this.togglePointerEvents(true); const annotationElementIds = new Set(); for (const editor of this.#editors.values()) { @@ -274,6 +275,7 @@ class AnnotationEditorLayer { */ disable() { this.#isDisabling = true; + this.div.tabIndex = -1; this.togglePointerEvents(false); const hiddenAnnotationIds = new Set(); for (const editor of this.#editors.values()) { @@ -333,6 +335,7 @@ class AnnotationEditorLayer { } enableTextSelection() { + this.div.tabIndex = -1; if (this.#textLayer?.div) { this.#textLayer.div.addEventListener( "pointerdown", @@ -343,6 +346,7 @@ class AnnotationEditorLayer { } disableTextSelection() { + this.div.tabIndex = 0; if (this.#textLayer?.div) { this.#textLayer.div.removeEventListener( "pointerdown", diff --git a/src/display/editor/editor.js b/src/display/editor/editor.js index cf3ae887f24367..d0b0d1830d2df0 100644 --- a/src/display/editor/editor.js +++ b/src/display/editor/editor.js @@ -1002,7 +1002,7 @@ class AnnotationEditor { this.div.setAttribute("data-editor-rotation", (360 - this.rotation) % 360); this.div.className = this.name; this.div.setAttribute("id", this.id); - this.div.setAttribute("tabIndex", 0); + this.div.tabIndex = 0; if (!this._isVisible) { this.div.classList.add("hidden"); } @@ -1680,6 +1680,14 @@ class AnnotationEditor { this.div.classList.toggle("hidden", !visible); this._isVisible = visible; } + + enable() { + this.div.tabIndex = 0; + } + + disable() { + this.div.tabIndex = -1; + } } // This class is used to fake an editor which has been deleted. diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index 81ccd16e926812..d24415c95ae171 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -1549,6 +1549,9 @@ class AnnotationEditorUIManager { for (const layer of this.#allLayers.values()) { layer.enable(); } + for (const editor of this.#allEditors.values()) { + editor.enable(); + } } } @@ -1562,6 +1565,9 @@ class AnnotationEditorUIManager { for (const layer of this.#allLayers.values()) { layer.disable(); } + for (const editor of this.#allEditors.values()) { + editor.disable(); + } } } diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index 78be7bdd819834..0c192388da65ad 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -22,6 +22,8 @@ import { getSerialized, kbBigMoveLeft, kbBigMoveUp, + kbFocusNext, + kbFocusPrevious, kbSelectAll, loadAndWait, scrollIntoView, @@ -1510,4 +1512,52 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Text layer must have the focus before highlights", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterAll(async () => { + await closePages(pages); + }); + + it("must check the focus order", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.click("#editorHighlight"); + await page.waitForSelector(".annotationEditorLayer.highlightEditing"); + + let rect = await getSpanRectFromText(page, 1, "Abstract"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + + rect = await getSpanRectFromText(page, 1, "Languages"); + x = rect.x + rect.width / 2; + y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(1)); + await page.focus(getEditorSelector(1)); + + await kbFocusPrevious(page); + await page.waitForFunction( + sel => document.querySelector(sel) === document.activeElement, + {}, + `.page[data-page-number="1"] > .textLayer` + ); + + await kbFocusNext(page); + await page.waitForFunction( + sel => document.querySelector(sel) === document.activeElement, + {}, + getEditorSelector(1) + ); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 037b547a1d7e5f..d903e014249ac6 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -461,6 +461,24 @@ async function kbDeleteLastWord(page) { } } +async function kbFocusNext(page) { + const handle = await createPromise(page, resolve => { + window.addEventListener("focusin", resolve, { once: true }); + }); + await page.keyboard.press("Tab"); + await awaitPromise(handle); +} + +async function kbFocusPrevious(page) { + const handle = await createPromise(page, resolve => { + window.addEventListener("focusin", resolve, { once: true }); + }); + await page.keyboard.down("Shift"); + await page.keyboard.press("Tab"); + await page.keyboard.up("Shift"); + await awaitPromise(handle); +} + export { awaitPromise, clearInput, @@ -484,6 +502,8 @@ export { kbBigMoveUp, kbCopy, kbDeleteLastWord, + kbFocusNext, + kbFocusPrevious, kbGoToBegin, kbGoToEnd, kbModifierDown, diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 733af85d7e1c6f..a926d81e2dbd57 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -109,7 +109,6 @@ font-size: calc(100px * var(--scale-factor)); transform-origin: 0 0; cursor: auto; - z-index: 4; } .annotationEditorLayer.waiting { diff --git a/web/annotation_editor_layer_builder.js b/web/annotation_editor_layer_builder.js index c841de6ac0f98b..79ded0b41b708c 100644 --- a/web/annotation_editor_layer_builder.js +++ b/web/annotation_editor_layer_builder.js @@ -30,7 +30,6 @@ import { GenericL10n } from "web-null_l10n"; /** * @typedef {Object} AnnotationEditorLayerBuilderOptions * @property {AnnotationEditorUIManager} [uiManager] - * @property {HTMLDivElement} pageDiv * @property {PDFPageProxy} pdfPage * @property {IL10n} [l10n] * @property {TextAccessibilityManager} [accessibilityManager] @@ -52,7 +51,6 @@ class AnnotationEditorLayerBuilder { * @param {AnnotationEditorLayerBuilderOptions} options */ constructor(options) { - this.pageDiv = options.pageDiv; this.pdfPage = options.pdfPage; this.accessibilityManager = options.accessibilityManager; this.l10n = options.l10n; @@ -72,7 +70,7 @@ class AnnotationEditorLayerBuilder { * @param {PageViewport} viewport * @param {string} intent (default value is 'display') */ - async render(viewport, intent = "display") { + async render(viewport, view, intent = "display") { if (intent !== "display") { return; } @@ -91,10 +89,9 @@ class AnnotationEditorLayerBuilder { // Create an AnnotationEditor layer div const div = (this.div = document.createElement("div")); div.className = "annotationEditorLayer"; - div.tabIndex = 0; div.hidden = true; div.dir = this.#uiManager.direction; - this.pageDiv.append(div); + view.addLayer(div, "annotationEditorLayer"); this.annotationEditorLayer = new AnnotationEditorLayer({ uiManager: this.#uiManager, diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index 74add91ca01796..20baa7abc95c38 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -76,7 +76,6 @@ left: 0; pointer-events: none; transform-origin: 0 0; - z-index: 3; &[data-main-rotation="90"] .norotate { transform: rotate(270deg) translateX(-100%); diff --git a/web/annotation_layer_builder.js b/web/annotation_layer_builder.js index ca0c824d378e9d..c53fc15ce2ee6c 100644 --- a/web/annotation_layer_builder.js +++ b/web/annotation_layer_builder.js @@ -28,7 +28,6 @@ import { PresentationModeState } from "./ui_utils.js"; /** * @typedef {Object} AnnotationLayerBuilderOptions - * @property {HTMLDivElement} pageDiv * @property {PDFPageProxy} pdfPage * @property {AnnotationStorage} [annotationStorage] * @property {string} [imageResourcesPath] - Path for image resources, mainly @@ -51,7 +50,6 @@ class AnnotationLayerBuilder { * @param {AnnotationLayerBuilderOptions} options */ constructor({ - pageDiv, pdfPage, linkService, downloadManager, @@ -64,7 +62,6 @@ class AnnotationLayerBuilder { annotationCanvasMap = null, accessibilityManager = null, }) { - this.pageDiv = pageDiv; this.pdfPage = pdfPage; this.linkService = linkService; this.downloadManager = downloadManager; @@ -89,7 +86,7 @@ class AnnotationLayerBuilder { * @returns {Promise} A promise that is resolved when rendering of the * annotations is complete. */ - async render(viewport, intent = "display") { + async render(viewport, view, intent = "display") { if (this.div) { if (this._cancelled || !this.annotationLayer) { return; @@ -115,7 +112,7 @@ class AnnotationLayerBuilder { // if there is at least one annotation. const div = (this.div = document.createElement("div")); div.className = "annotationLayer"; - this.pageDiv.append(div); + view.addLayer(div, "annotationLayer"); if (annotations.length === 0) { this.hide(); diff --git a/web/pdf_page_view.js b/web/pdf_page_view.js index 4e97897b5ca2ea..41b1e7b91e25ff 100644 --- a/web/pdf_page_view.js +++ b/web/pdf_page_view.js @@ -129,6 +129,16 @@ class PDFPageView { #viewportMap = new WeakMap(); + static #layerOrder = new Map([ + ["canvasWrapper", 0], + ["textLayer", 1], + ["annotationLayer", 2], + ["annotationEditorLayer", 3], + ["xfaLayer", 2], + ]); + + #layers = [null, null, null, null]; + /** * @param {PDFPageViewOptions} options */ @@ -223,6 +233,19 @@ class PDFPageView { } } + addLayer(div, name) { + const pos = PDFPageView.#layerOrder.get(name); + this.#layers[pos] = div; + for (let i = pos - 1; i >= 0; i--) { + const layer = this.#layers[i]; + if (layer) { + layer.after(div); + return; + } + } + this.div.prepend(div); + } + get renderingState() { return this.#renderingState; } @@ -337,7 +360,7 @@ class PDFPageView { async #renderAnnotationLayer() { let error = null; try { - await this.annotationLayer.render(this.viewport, "display"); + await this.annotationLayer.render(this.viewport, this, "display"); } catch (ex) { console.error(`#renderAnnotationLayer: "${ex}".`); error = ex; @@ -353,7 +376,7 @@ class PDFPageView { async #renderAnnotationEditorLayer() { let error = null; try { - await this.annotationEditorLayer.render(this.viewport, "display"); + await this.annotationEditorLayer.render(this.viewport, this, "display"); } catch (ex) { console.error(`#renderAnnotationEditorLayer: "${ex}".`); error = ex; @@ -392,7 +415,7 @@ class PDFPageView { if (this.xfaLayer?.div) { // Pause translation when inserting the xfaLayer in the DOM. this.l10n.pause(); - this.div.append(this.xfaLayer.div); + this.addLayer(this.xfaLayer.div, "xfaLayer"); this.l10n.resume(); } @@ -531,6 +554,10 @@ class PDFPageView { continue; } node.remove(); + const indexLayer = this.#layers.indexOf(node); + if (indexLayer >= 0) { + this.#layers[indexLayer] = null; + } } div.removeAttribute("data-loaded"); @@ -877,7 +904,8 @@ class PDFPageView { // overflow will be hidden in Firefox. const canvasWrapper = document.createElement("div"); canvasWrapper.classList.add("canvasWrapper"); - div.append(canvasWrapper); + canvasWrapper.setAttribute("aria-hidden", true); + this.addLayer(canvasWrapper, "canvasWrapper"); if ( !this.textLayer && @@ -895,7 +923,7 @@ class PDFPageView { this.textLayer.onAppend = textLayerDiv => { // Pause translation when inserting the textLayer in the DOM. this.l10n.pause(); - this.div.append(textLayerDiv); + this.addLayer(textLayerDiv, "textLayer"); this.l10n.resume(); }; } diff --git a/web/pdf_viewer.css b/web/pdf_viewer.css index 0abf0b5e2272f5..bbd6ca97b7a51d 100644 --- a/web/pdf_viewer.css +++ b/web/pdf_viewer.css @@ -75,7 +75,6 @@ overflow: hidden; width: 100%; height: 100%; - z-index: 1; } .pdfViewer .page { diff --git a/web/text_layer_builder.css b/web/text_layer_builder.css index 5d4faa6019dfc3..254bcbe45e8d6b 100644 --- a/web/text_layer_builder.css +++ b/web/text_layer_builder.css @@ -23,7 +23,6 @@ text-size-adjust: none; forced-color-adjust: none; transform-origin: 0 0; - z-index: 2; caret-color: CanvasText; &.highlighting { diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index dc676bf6ddc1f3..cf17bfea5460ae 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -65,6 +65,7 @@ class TextLayerBuilder { this.onAppend = null; this.div = document.createElement("div"); + this.div.tabIndex = 0; this.div.className = "textLayer"; }