Merge pull request #20096 from calixteman/bug1708041
Make the link annotations correctly announced by screen readers (bug 1708041)
This commit is contained in:
commit
daf2cbbfe9
@ -3833,6 +3833,10 @@ class LinkAnnotation extends Annotation {
|
|||||||
docAttachments: annotationGlobals.attachments,
|
docAttachments: annotationGlobals.attachments,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get overlaysTextContent() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PopupAnnotation extends Annotation {
|
class PopupAnnotation extends Annotation {
|
||||||
|
|||||||
@ -24,7 +24,7 @@ class SingleIntersector {
|
|||||||
|
|
||||||
#maxY = -Infinity;
|
#maxY = -Infinity;
|
||||||
|
|
||||||
#quadPoints;
|
#quadPoints = null;
|
||||||
|
|
||||||
#text = [];
|
#text = [];
|
||||||
|
|
||||||
@ -36,7 +36,13 @@ class SingleIntersector {
|
|||||||
|
|
||||||
constructor(annotation) {
|
constructor(annotation) {
|
||||||
this.#annotation = annotation;
|
this.#annotation = annotation;
|
||||||
const quadPoints = (this.#quadPoints = annotation.data.quadPoints);
|
const quadPoints = annotation.data.quadPoints;
|
||||||
|
if (!quadPoints) {
|
||||||
|
// If there are no quad points, we use the rectangle to determine the
|
||||||
|
// bounds of the annotation.
|
||||||
|
[this.#minX, this.#minY, this.#maxX, this.#maxY] = annotation.data.rect;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = 0, ii = quadPoints.length; i < ii; i += 8) {
|
for (let i = 0, ii = quadPoints.length; i < ii; i += 8) {
|
||||||
this.#minX = Math.min(this.#minX, quadPoints[i]);
|
this.#minX = Math.min(this.#minX, quadPoints[i]);
|
||||||
@ -44,6 +50,9 @@ class SingleIntersector {
|
|||||||
this.#minY = Math.min(this.#minY, quadPoints[i + 5]);
|
this.#minY = Math.min(this.#minY, quadPoints[i + 5]);
|
||||||
this.#maxY = Math.max(this.#maxY, quadPoints[i + 1]);
|
this.#maxY = Math.max(this.#maxY, quadPoints[i + 1]);
|
||||||
}
|
}
|
||||||
|
if (quadPoints.length > 8) {
|
||||||
|
this.#quadPoints = quadPoints;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
overlaps(other) {
|
overlaps(other) {
|
||||||
@ -73,7 +82,7 @@ class SingleIntersector {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const quadPoints = this.#quadPoints;
|
const quadPoints = this.#quadPoints;
|
||||||
if (quadPoints.length === 8) {
|
if (!quadPoints) {
|
||||||
// We've only one quad, so if we intersect min/max bounds then we
|
// We've only one quad, so if we intersect min/max bounds then we
|
||||||
// intersect the quad.
|
// intersect the quad.
|
||||||
return true;
|
return true;
|
||||||
@ -150,7 +159,7 @@ class Intersector {
|
|||||||
|
|
||||||
constructor(annotations) {
|
constructor(annotations) {
|
||||||
for (const annotation of annotations) {
|
for (const annotation of annotations) {
|
||||||
if (!annotation.data.quadPoints) {
|
if (!annotation.data.quadPoints && !annotation.data.rect) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const intersector = new SingleIntersector(annotation);
|
const intersector = new SingleIntersector(annotation);
|
||||||
|
|||||||
@ -276,7 +276,10 @@ class AnnotationElement {
|
|||||||
|
|
||||||
const container = document.createElement("section");
|
const container = document.createElement("section");
|
||||||
container.setAttribute("data-annotation-id", data.id);
|
container.setAttribute("data-annotation-id", data.id);
|
||||||
if (!(this instanceof WidgetAnnotationElement)) {
|
if (
|
||||||
|
!(this instanceof WidgetAnnotationElement) &&
|
||||||
|
!(this instanceof LinkAnnotationElement)
|
||||||
|
) {
|
||||||
container.tabIndex = 0;
|
container.tabIndex = 0;
|
||||||
}
|
}
|
||||||
const { style } = container;
|
const { style } = container;
|
||||||
@ -797,16 +800,21 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
linkService.addLinkAttributes(link, data.url, data.newWindow);
|
linkService.addLinkAttributes(link, data.url, data.newWindow);
|
||||||
isBound = true;
|
isBound = true;
|
||||||
} else if (data.action) {
|
} else if (data.action) {
|
||||||
this._bindNamedAction(link, data.action);
|
this._bindNamedAction(link, data.action, data.overlaidText);
|
||||||
isBound = true;
|
isBound = true;
|
||||||
} else if (data.attachment) {
|
} else if (data.attachment) {
|
||||||
this.#bindAttachment(link, data.attachment, data.attachmentDest);
|
this.#bindAttachment(
|
||||||
|
link,
|
||||||
|
data.attachment,
|
||||||
|
data.overlaidText,
|
||||||
|
data.attachmentDest
|
||||||
|
);
|
||||||
isBound = true;
|
isBound = true;
|
||||||
} else if (data.setOCGState) {
|
} else if (data.setOCGState) {
|
||||||
this.#bindSetOCGState(link, data.setOCGState);
|
this.#bindSetOCGState(link, data.setOCGState, data.overlaidText);
|
||||||
isBound = true;
|
isBound = true;
|
||||||
} else if (data.dest) {
|
} else if (data.dest) {
|
||||||
this._bindLink(link, data.dest);
|
this._bindLink(link, data.dest, data.overlaidText);
|
||||||
isBound = true;
|
isBound = true;
|
||||||
} else {
|
} else {
|
||||||
if (
|
if (
|
||||||
@ -848,9 +856,10 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
* @private
|
* @private
|
||||||
* @param {Object} link
|
* @param {Object} link
|
||||||
* @param {Object} destination
|
* @param {Object} destination
|
||||||
|
* @param {string} [overlaidText]
|
||||||
* @memberof LinkAnnotationElement
|
* @memberof LinkAnnotationElement
|
||||||
*/
|
*/
|
||||||
_bindLink(link, destination) {
|
_bindLink(link, destination, overlaidText = "") {
|
||||||
link.href = this.linkService.getDestinationHash(destination);
|
link.href = this.linkService.getDestinationHash(destination);
|
||||||
link.onclick = () => {
|
link.onclick = () => {
|
||||||
if (destination) {
|
if (destination) {
|
||||||
@ -861,6 +870,9 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
if (destination || destination === /* isTooltipOnly = */ "") {
|
if (destination || destination === /* isTooltipOnly = */ "") {
|
||||||
this.#setInternalLink();
|
this.#setInternalLink();
|
||||||
}
|
}
|
||||||
|
if (overlaidText) {
|
||||||
|
link.title = overlaidText;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -869,14 +881,18 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
* @private
|
* @private
|
||||||
* @param {Object} link
|
* @param {Object} link
|
||||||
* @param {Object} action
|
* @param {Object} action
|
||||||
|
* @param {string} [overlaidText]
|
||||||
* @memberof LinkAnnotationElement
|
* @memberof LinkAnnotationElement
|
||||||
*/
|
*/
|
||||||
_bindNamedAction(link, action) {
|
_bindNamedAction(link, action, overlaidText = "") {
|
||||||
link.href = this.linkService.getAnchorUrl("");
|
link.href = this.linkService.getAnchorUrl("");
|
||||||
link.onclick = () => {
|
link.onclick = () => {
|
||||||
this.linkService.executeNamedAction(action);
|
this.linkService.executeNamedAction(action);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
if (overlaidText) {
|
||||||
|
link.title = overlaidText;
|
||||||
|
}
|
||||||
this.#setInternalLink();
|
this.#setInternalLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -884,12 +900,15 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
* Bind attachments to the link element.
|
* Bind attachments to the link element.
|
||||||
* @param {Object} link
|
* @param {Object} link
|
||||||
* @param {Object} attachment
|
* @param {Object} attachment
|
||||||
* @param {str} [dest]
|
* @param {string} [overlaidText]
|
||||||
|
* @param {string} [dest]
|
||||||
*/
|
*/
|
||||||
#bindAttachment(link, attachment, dest = null) {
|
#bindAttachment(link, attachment, overlaidText = "", dest = null) {
|
||||||
link.href = this.linkService.getAnchorUrl("");
|
link.href = this.linkService.getAnchorUrl("");
|
||||||
if (attachment.description) {
|
if (attachment.description) {
|
||||||
link.title = attachment.description;
|
link.title = attachment.description;
|
||||||
|
} else if (overlaidText) {
|
||||||
|
link.title = overlaidText;
|
||||||
}
|
}
|
||||||
link.onclick = () => {
|
link.onclick = () => {
|
||||||
this.downloadManager?.openOrDownloadData(
|
this.downloadManager?.openOrDownloadData(
|
||||||
@ -906,13 +925,17 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
* Bind SetOCGState actions to the link element.
|
* Bind SetOCGState actions to the link element.
|
||||||
* @param {Object} link
|
* @param {Object} link
|
||||||
* @param {Object} action
|
* @param {Object} action
|
||||||
|
* @param {string} [overlaidText]
|
||||||
*/
|
*/
|
||||||
#bindSetOCGState(link, action) {
|
#bindSetOCGState(link, action, overlaidText = "") {
|
||||||
link.href = this.linkService.getAnchorUrl("");
|
link.href = this.linkService.getAnchorUrl("");
|
||||||
link.onclick = () => {
|
link.onclick = () => {
|
||||||
this.linkService.executeSetOCGState(action);
|
this.linkService.executeSetOCGState(action);
|
||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
|
if (overlaidText) {
|
||||||
|
link.title = overlaidText;
|
||||||
|
}
|
||||||
this.#setInternalLink();
|
this.#setInternalLink();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -947,6 +970,9 @@ class LinkAnnotationElement extends AnnotationElement {
|
|||||||
return false;
|
return false;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
if (data.overlaidText) {
|
||||||
|
link.title = data.overlaidText;
|
||||||
|
}
|
||||||
|
|
||||||
if (!link.onclick) {
|
if (!link.onclick) {
|
||||||
link.onclick = () => false;
|
link.onclick = () => false;
|
||||||
|
|||||||
@ -247,6 +247,51 @@ describe("Text widget", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Link annotations with internal destinations", () => {
|
||||||
|
describe("bug1708041.pdf", () => {
|
||||||
|
let pages;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
pages = await loadAndWait(
|
||||||
|
"bug1708041.pdf",
|
||||||
|
".page[data-page-number='1'] .annotationLayer"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await closePages(pages);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("must click on a link and check if it navigates to the correct page", async () => {
|
||||||
|
await Promise.all(
|
||||||
|
pages.map(async ([browserName, page]) => {
|
||||||
|
const pageOneSelector = ".page[data-page-number='1']";
|
||||||
|
const linkSelector = `${pageOneSelector} #pdfjs_internal_id_42R`;
|
||||||
|
await page.waitForSelector(linkSelector);
|
||||||
|
const linkTitle = await page.$eval(linkSelector, el => el.title);
|
||||||
|
expect(linkTitle)
|
||||||
|
.withContext(`In ${browserName}`)
|
||||||
|
.toEqual("Go to the last page");
|
||||||
|
await page.click(linkSelector);
|
||||||
|
const pageSixTextLayerSelector =
|
||||||
|
".page[data-page-number='6'] .textLayer";
|
||||||
|
await page.waitForSelector(pageSixTextLayerSelector, {
|
||||||
|
visible: true,
|
||||||
|
});
|
||||||
|
await page.waitForFunction(
|
||||||
|
sel => {
|
||||||
|
const textLayer = document.querySelector(sel);
|
||||||
|
return document.activeElement === textLayer;
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
pageSixTextLayerSelector
|
||||||
|
);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("Annotation and storage", () => {
|
describe("Annotation and storage", () => {
|
||||||
describe("issue14023.pdf", () => {
|
describe("issue14023.pdf", () => {
|
||||||
let pages;
|
let pages;
|
||||||
|
|||||||
1
test/pdfs/.gitignore
vendored
1
test/pdfs/.gitignore
vendored
@ -734,3 +734,4 @@
|
|||||||
!issue20062.pdf
|
!issue20062.pdf
|
||||||
!issue20102.pdf
|
!issue20102.pdf
|
||||||
!issue20065.pdf
|
!issue20065.pdf
|
||||||
|
!bug1708041.pdf
|
||||||
|
|||||||
BIN
test/pdfs/bug1708041.pdf
Normal file
BIN
test/pdfs/bug1708041.pdf
Normal file
Binary file not shown.
@ -192,6 +192,18 @@ class PDFLinkService {
|
|||||||
destArray: explicitDest,
|
destArray: explicitDest,
|
||||||
ignoreDestinationZoom: this._ignoreDestinationZoom,
|
ignoreDestinationZoom: this._ignoreDestinationZoom,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const ac = new AbortController();
|
||||||
|
this.eventBus._on(
|
||||||
|
"textlayerrendered",
|
||||||
|
evt => {
|
||||||
|
if (evt.pageNumber === pageNumber) {
|
||||||
|
evt.source.textLayer.div.focus();
|
||||||
|
ac.abort();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ signal: ac.signal }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user