diff --git a/src/display/editor/tools.js b/src/display/editor/tools.js index af98c6940ae81..a6fc40677a8c1 100644 --- a/src/display/editor/tools.js +++ b/src/display/editor/tools.js @@ -1480,8 +1480,12 @@ class AnnotationEditorUIManager { if (!boxes) { return; } + const parent = textLayer.parentElement; + if (!parent) { + return; + } this.#floatingToolbar ||= new FloatingToolbar(this); - this.#floatingToolbar.show(textLayer, boxes, this.direction === "ltr"); + this.#floatingToolbar.show(parent, boxes, this.direction === "ltr"); } /** diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 35aaa2e72bd9b..614da115ed59d 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -17,10 +17,10 @@ import { awaitPromise, clearEditors, closePages, + commit, copy, copyToClipboard, - countSerialized, - countStorageEntries, + createFreeTextEditor, createPromise, dragAndDrop, firstPageOnTop, @@ -28,7 +28,6 @@ import { getEditors, getEditorSelector, getFirstSerialized, - getNextEditorId, getRect, getSerialized, isCanvasMonochrome, @@ -67,55 +66,8 @@ const selectAll = selectEditors.bind(null, "freeText"); const clearAll = clearEditors.bind(null, "freeText"); -const commit = async page => { - await page.keyboard.press("Escape"); - await page.waitForSelector(".freeTextEditor.selectedEditor .overlay.enabled"); -}; - const switchToFreeText = switchToEditor.bind(null, "FreeText"); -const cancelFocusIn = async (page, selector) => { - page.evaluate(sel => { - const el = document.querySelector(sel); - el.addEventListener( - "focusin", - evt => { - evt.preventDefault(); - evt.stopPropagation(); - }, - { capture: true, once: true } - ); - }, selector); -}; - -const createFreeTextEditor = async ({ - page, - x, - y, - data = null, - noFocusIn = false, -}) => { - const editorSelector = getEditorSelector(await getNextEditorId(page)); - const serializedCount = await countSerialized(page); - const storageEntriesCount = await countStorageEntries(page); - - await page.mouse.click(x, y); - await page.waitForSelector(editorSelector, { visible: true }); - if (data) { - await page.type(`${editorSelector} .internal`, data); - } - if (noFocusIn) { - await cancelFocusIn(page, editorSelector); - } - await commit(page); - - await waitForSelectedEditor(page, editorSelector); - await waitForStorageEntries(page, storageEntriesCount + 1); - await waitForSerialized(page, serializedCount + 1); - - return editorSelector; -}; - describe("FreeText Editor", () => { describe("FreeText", () => { let pages; diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index f13ad2d8ced8e..8600b904b1234 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -16,6 +16,7 @@ import { awaitPromise, closePages, + createFreeTextEditor, getAnnotationSelector, getEditorSelector, getFirstSerialized, @@ -53,6 +54,8 @@ const selectAll = selectEditors.bind(null, "highlight"); const switchToHighlight = switchToEditor.bind(null, "Highlight"); +const switchToFreeText = switchToEditor.bind(null, "FreeText"); + describe("Highlight Editor", () => { describe("Editor must be removed without exception", () => { let pages; @@ -2931,4 +2934,131 @@ describe("Highlight Editor", () => { }); }); }); + + describe("editToolbar is rendering over annotations", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "toolbar-overlap-with-annotations.pdf", + ".annotationEditorLayer", + 120 + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that the edit toolbar is rendered above link annotations", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await page.waitForSelector( + `.page[data-page-number = "1"] .textLayer .endOfContent` + ); + + const linkSelector = `a[href="https://github.com/mozilla/pdf.js"]`; + const linkRect = await getRect(page, linkSelector); + + const topElementId = await page.evaluate( + (px, py) => document.elementFromPoint(px, py)?.id || null, + linkRect.x + linkRect.width / 2, + linkRect.y + linkRect.height / 2 + ); + expect(topElementId) + .withContext(`In ${browserName}`) + .toEqual("pdfjs_internal_id_14R"); + + // Select some text to show the floating toolbar. + const firstRect = await getSpanRectFromText(page, 1, "A:"); + await page.mouse.click(firstRect.x, firstRect.y, { + count: 2, + delay: 100, + }); + + const highlightButtonSelector = `.page[data-page-number = "1"] .editToolbar button.highlightButton`; + await page.waitForSelector(highlightButtonSelector, { + visible: true, + }); + + const topElementClass = await page.evaluate( + (px, py) => document.elementFromPoint(px, py)?.className || null, + linkRect.x, + linkRect.y + ); + expect(topElementClass) + .withContext(`In ${browserName}`) + .toContain("highlightButton"); + }) + ); + }); + + it("must check that the edit toolbar is rendered above text annotations", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToFreeText(page); + + const myText = await getSpanRectFromText(page, 1, "My text"); + // "CHECK" is roughly 50px wide at the default font size and + // "CHECK" roughly aligned with the end of "Languages". + const editorSelector = await createFreeTextEditor({ + page, + x: myText.x + myText.width / 2, + y: myText.y + myText.height + 20, + data: "CHECK", + }); + + const freeTextRect = await getRect(page, editorSelector); + const freeTextTopElement = await page.evaluate( + (px, py) => + document.elementFromPoint(px, py)?.getAttribute("data-l10n-id") || + null, + freeTextRect.x + freeTextRect.width / 2, + freeTextRect.y + freeTextRect.height / 2 + ); + expect(freeTextTopElement) + .withContext(`In ${browserName}`) + .toEqual("pdfjs-free-text2"); + + // Close the text editor. + await switchToFreeText(page, /* disable */ true); + + // Double click on "myText" to show the floating toolbar. + await page.mouse.click( + myText.x + myText.width / 2, + myText.y + myText.height / 2, + { count: 2, delay: 100 } + ); + + const toolbarSelector = `.page[data-page-number = "1"] .editToolbar:has(button.highlightButton)`; + const highlightButtonSelector = `${toolbarSelector} button.highlightButton`; + await page.waitForSelector(highlightButtonSelector, { + visible: true, + }); + + let topElement = await page.evaluate( + (px, py) => document.elementFromPoint(px, py)?.className || null, + freeTextRect.x + freeTextRect.width / 2, + freeTextRect.y + freeTextRect.height / 2 + ); + expect(topElement) + .withContext(`In ${browserName}`) + .toContain("highlightButton"); + + // Re-open the text editor without dismissing the floating toolbar, + // so both are present on the page at the same time. + await switchToFreeText(page); + + topElement = await page.evaluate( + (px, py) => document.elementFromPoint(px, py)?.className || null, + freeTextRect.x + freeTextRect.width / 2, + freeTextRect.y + freeTextRect.height / 2 + ); + expect(topElement) + .withContext(`In ${browserName}`) + .toContain("highlightButton"); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 5999c202d2ef8..52211b43b334e 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -432,6 +432,53 @@ async function waitForUnselectedEditor(page, selector) { return page.waitForSelector(`${selector}:not(.selectedEditor)`); } +async function commit(page) { + await page.keyboard.press("Escape"); + await page.waitForSelector(".freeTextEditor.selectedEditor .overlay.enabled"); +} + +async function cancelFocusIn(page, selector) { + page.evaluate(sel => { + const el = document.querySelector(sel); + el.addEventListener( + "focusin", + evt => { + evt.preventDefault(); + evt.stopPropagation(); + }, + { capture: true, once: true } + ); + }, selector); +} + +async function createFreeTextEditor({ + page, + x, + y, + data = null, + noFocusIn = false, +}) { + const editorSelector = getEditorSelector(await getNextEditorId(page)); + const serializedCount = await countSerialized(page); + const storageEntriesCount = await countStorageEntries(page); + + await page.mouse.click(x, y); + await page.waitForSelector(editorSelector, { visible: true }); + if (data) { + await page.type(`${editorSelector} .internal`, data); + } + if (noFocusIn) { + await cancelFocusIn(page, editorSelector); + } + await commit(page); + + await waitForSelectedEditor(page, editorSelector); + await waitForStorageEntries(page, storageEntriesCount + 1); + await waitForSerialized(page, serializedCount + 1); + + return editorSelector; +} + async function mockClipboard(pages) { return Promise.all( pages.map(async ([_, page]) => { @@ -1189,10 +1236,12 @@ export { clearInput, closePages, closeSinglePage, + commit, copy, copyToClipboard, countSerialized, countStorageEntries, + createFreeTextEditor, createPromise, createPromiseWithArgs, dragAndDrop, diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index 8148a5d3202a4..afc428856435c 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -492,6 +492,7 @@ !issue11473.pdf !bug1001080.pdf !issue15716.pdf +!toolbar-overlap-with-annotations.pdf !bug1671312_reduced.pdf !bug1671312_ArialNarrow.pdf !issue17848.pdf diff --git a/test/pdfs/toolbar-overlap-with-annotations.pdf b/test/pdfs/toolbar-overlap-with-annotations.pdf new file mode 100644 index 0000000000000..4c2f9776a8c9d Binary files /dev/null and b/test/pdfs/toolbar-overlap-with-annotations.pdf differ diff --git a/test/test_manifest.json b/test/test_manifest.json index 05dea0a73d430..12809a59387c5 100644 --- a/test/test_manifest.json +++ b/test/test_manifest.json @@ -1387,6 +1387,13 @@ "rounds": 1, "type": "eq" }, + { + "id": "toolbar-overlap-with-annotations", + "file": "pdfs/toolbar-overlap-with-annotations.pdf", + "md5": "33f76794ba75dfcf0938267c25b8df14", + "rounds": 1, + "type": "eq" + }, { "id": "unix01-partial", "file": "pdfs/unix01.pdf", diff --git a/web/annotation_editor_layer_builder.css b/web/annotation_editor_layer_builder.css index 13f4768c4d334..7801fd64ace5e 100644 --- a/web/annotation_editor_layer_builder.css +++ b/web/annotation_editor_layer_builder.css @@ -122,6 +122,7 @@ font-size: calc(100px * var(--total-scale-factor)); transform-origin: 0 0; cursor: auto; + isolation: isolate; .selectedEditor { z-index: 100000 !important; @@ -241,7 +242,7 @@ .highlightEditor, .signatureEditor ), -.textLayer { +.page { .editToolbar { --editor-toolbar-delete-image: url(images/editor-toolbar-delete.svg); --editor-toolbar-bg-color: light-dark(#f0f0f4, #2b2a33); @@ -329,6 +330,8 @@ position: absolute; inset-inline-end: 0; inset-block-start: calc(100% + var(--editor-toolbar-vert-offset)); + z-index: 2; + isolation: isolate; border-radius: 6px; background-color: var(--editor-toolbar-bg-color); diff --git a/web/annotation_layer_builder.css b/web/annotation_layer_builder.css index af3a2af245a5d..9737b7e322e43 100644 --- a/web/annotation_layer_builder.css +++ b/web/annotation_layer_builder.css @@ -79,6 +79,8 @@ left: 0; pointer-events: none; transform-origin: 0 0; + z-index: 1; + isolation: isolate; &[data-main-rotation="90"] .norotate { transform: rotate(270deg) translateX(-100%);