Skip to content

Commit daf2cbb

Browse files
Merge pull request #20096 from calixteman/bug1708041
Make the link annotations correctly announced by screen readers (bug 1708041)
2 parents bfc7fc4 + f695e0c commit daf2cbb

7 files changed

Lines changed: 111 additions & 14 deletions

File tree

src/core/annotation.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3833,6 +3833,10 @@ class LinkAnnotation extends Annotation {
38333833
docAttachments: annotationGlobals.attachments,
38343834
});
38353835
}
3836+
3837+
get overlaysTextContent() {
3838+
return true;
3839+
}
38363840
}
38373841

38383842
class PopupAnnotation extends Annotation {

src/core/intersector.js

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ class SingleIntersector {
2424

2525
#maxY = -Infinity;
2626

27-
#quadPoints;
27+
#quadPoints = null;
2828

2929
#text = [];
3030

@@ -36,14 +36,23 @@ class SingleIntersector {
3636

3737
constructor(annotation) {
3838
this.#annotation = annotation;
39-
const quadPoints = (this.#quadPoints = annotation.data.quadPoints);
39+
const quadPoints = annotation.data.quadPoints;
40+
if (!quadPoints) {
41+
// If there are no quad points, we use the rectangle to determine the
42+
// bounds of the annotation.
43+
[this.#minX, this.#minY, this.#maxX, this.#maxY] = annotation.data.rect;
44+
return;
45+
}
4046

4147
for (let i = 0, ii = quadPoints.length; i < ii; i += 8) {
4248
this.#minX = Math.min(this.#minX, quadPoints[i]);
4349
this.#maxX = Math.max(this.#maxX, quadPoints[i + 2]);
4450
this.#minY = Math.min(this.#minY, quadPoints[i + 5]);
4551
this.#maxY = Math.max(this.#maxY, quadPoints[i + 1]);
4652
}
53+
if (quadPoints.length > 8) {
54+
this.#quadPoints = quadPoints;
55+
}
4756
}
4857

4958
overlaps(other) {
@@ -73,7 +82,7 @@ class SingleIntersector {
7382
}
7483

7584
const quadPoints = this.#quadPoints;
76-
if (quadPoints.length === 8) {
85+
if (!quadPoints) {
7786
// We've only one quad, so if we intersect min/max bounds then we
7887
// intersect the quad.
7988
return true;
@@ -150,7 +159,7 @@ class Intersector {
150159

151160
constructor(annotations) {
152161
for (const annotation of annotations) {
153-
if (!annotation.data.quadPoints) {
162+
if (!annotation.data.quadPoints && !annotation.data.rect) {
154163
continue;
155164
}
156165
const intersector = new SingleIntersector(annotation);

src/display/annotation_layer.js

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -276,7 +276,10 @@ class AnnotationElement {
276276

277277
const container = document.createElement("section");
278278
container.setAttribute("data-annotation-id", data.id);
279-
if (!(this instanceof WidgetAnnotationElement)) {
279+
if (
280+
!(this instanceof WidgetAnnotationElement) &&
281+
!(this instanceof LinkAnnotationElement)
282+
) {
280283
container.tabIndex = 0;
281284
}
282285
const { style } = container;
@@ -797,16 +800,21 @@ class LinkAnnotationElement extends AnnotationElement {
797800
linkService.addLinkAttributes(link, data.url, data.newWindow);
798801
isBound = true;
799802
} else if (data.action) {
800-
this._bindNamedAction(link, data.action);
803+
this._bindNamedAction(link, data.action, data.overlaidText);
801804
isBound = true;
802805
} else if (data.attachment) {
803-
this.#bindAttachment(link, data.attachment, data.attachmentDest);
806+
this.#bindAttachment(
807+
link,
808+
data.attachment,
809+
data.overlaidText,
810+
data.attachmentDest
811+
);
804812
isBound = true;
805813
} else if (data.setOCGState) {
806-
this.#bindSetOCGState(link, data.setOCGState);
814+
this.#bindSetOCGState(link, data.setOCGState, data.overlaidText);
807815
isBound = true;
808816
} else if (data.dest) {
809-
this._bindLink(link, data.dest);
817+
this._bindLink(link, data.dest, data.overlaidText);
810818
isBound = true;
811819
} else {
812820
if (
@@ -848,9 +856,10 @@ class LinkAnnotationElement extends AnnotationElement {
848856
* @private
849857
* @param {Object} link
850858
* @param {Object} destination
859+
* @param {string} [overlaidText]
851860
* @memberof LinkAnnotationElement
852861
*/
853-
_bindLink(link, destination) {
862+
_bindLink(link, destination, overlaidText = "") {
854863
link.href = this.linkService.getDestinationHash(destination);
855864
link.onclick = () => {
856865
if (destination) {
@@ -861,6 +870,9 @@ class LinkAnnotationElement extends AnnotationElement {
861870
if (destination || destination === /* isTooltipOnly = */ "") {
862871
this.#setInternalLink();
863872
}
873+
if (overlaidText) {
874+
link.title = overlaidText;
875+
}
864876
}
865877

866878
/**
@@ -869,27 +881,34 @@ class LinkAnnotationElement extends AnnotationElement {
869881
* @private
870882
* @param {Object} link
871883
* @param {Object} action
884+
* @param {string} [overlaidText]
872885
* @memberof LinkAnnotationElement
873886
*/
874-
_bindNamedAction(link, action) {
887+
_bindNamedAction(link, action, overlaidText = "") {
875888
link.href = this.linkService.getAnchorUrl("");
876889
link.onclick = () => {
877890
this.linkService.executeNamedAction(action);
878891
return false;
879892
};
893+
if (overlaidText) {
894+
link.title = overlaidText;
895+
}
880896
this.#setInternalLink();
881897
}
882898

883899
/**
884900
* Bind attachments to the link element.
885901
* @param {Object} link
886902
* @param {Object} attachment
887-
* @param {str} [dest]
903+
* @param {string} [overlaidText]
904+
* @param {string} [dest]
888905
*/
889-
#bindAttachment(link, attachment, dest = null) {
906+
#bindAttachment(link, attachment, overlaidText = "", dest = null) {
890907
link.href = this.linkService.getAnchorUrl("");
891908
if (attachment.description) {
892909
link.title = attachment.description;
910+
} else if (overlaidText) {
911+
link.title = overlaidText;
893912
}
894913
link.onclick = () => {
895914
this.downloadManager?.openOrDownloadData(
@@ -906,13 +925,17 @@ class LinkAnnotationElement extends AnnotationElement {
906925
* Bind SetOCGState actions to the link element.
907926
* @param {Object} link
908927
* @param {Object} action
928+
* @param {string} [overlaidText]
909929
*/
910-
#bindSetOCGState(link, action) {
930+
#bindSetOCGState(link, action, overlaidText = "") {
911931
link.href = this.linkService.getAnchorUrl("");
912932
link.onclick = () => {
913933
this.linkService.executeSetOCGState(action);
914934
return false;
915935
};
936+
if (overlaidText) {
937+
link.title = overlaidText;
938+
}
916939
this.#setInternalLink();
917940
}
918941

@@ -947,6 +970,9 @@ class LinkAnnotationElement extends AnnotationElement {
947970
return false;
948971
};
949972
}
973+
if (data.overlaidText) {
974+
link.title = data.overlaidText;
975+
}
950976

951977
if (!link.onclick) {
952978
link.onclick = () => false;

test/integration/annotation_spec.mjs

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,51 @@ describe("Text widget", () => {
247247
});
248248
});
249249

250+
describe("Link annotations with internal destinations", () => {
251+
describe("bug1708041.pdf", () => {
252+
let pages;
253+
254+
beforeEach(async () => {
255+
pages = await loadAndWait(
256+
"bug1708041.pdf",
257+
".page[data-page-number='1'] .annotationLayer"
258+
);
259+
});
260+
261+
afterEach(async () => {
262+
await closePages(pages);
263+
});
264+
265+
it("must click on a link and check if it navigates to the correct page", async () => {
266+
await Promise.all(
267+
pages.map(async ([browserName, page]) => {
268+
const pageOneSelector = ".page[data-page-number='1']";
269+
const linkSelector = `${pageOneSelector} #pdfjs_internal_id_42R`;
270+
await page.waitForSelector(linkSelector);
271+
const linkTitle = await page.$eval(linkSelector, el => el.title);
272+
expect(linkTitle)
273+
.withContext(`In ${browserName}`)
274+
.toEqual("Go to the last page");
275+
await page.click(linkSelector);
276+
const pageSixTextLayerSelector =
277+
".page[data-page-number='6'] .textLayer";
278+
await page.waitForSelector(pageSixTextLayerSelector, {
279+
visible: true,
280+
});
281+
await page.waitForFunction(
282+
sel => {
283+
const textLayer = document.querySelector(sel);
284+
return document.activeElement === textLayer;
285+
},
286+
{},
287+
pageSixTextLayerSelector
288+
);
289+
})
290+
);
291+
});
292+
});
293+
});
294+
250295
describe("Annotation and storage", () => {
251296
describe("issue14023.pdf", () => {
252297
let pages;

test/pdfs/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,3 +734,4 @@
734734
!issue20062.pdf
735735
!issue20102.pdf
736736
!issue20065.pdf
737+
!bug1708041.pdf

test/pdfs/bug1708041.pdf

37.3 KB
Binary file not shown.

web/pdf_link_service.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,18 @@ class PDFLinkService {
192192
destArray: explicitDest,
193193
ignoreDestinationZoom: this._ignoreDestinationZoom,
194194
});
195+
196+
const ac = new AbortController();
197+
this.eventBus._on(
198+
"textlayerrendered",
199+
evt => {
200+
if (evt.pageNumber === pageNumber) {
201+
evt.source.textLayer.div.focus();
202+
ac.abort();
203+
}
204+
},
205+
{ signal: ac.signal }
206+
);
195207
}
196208

197209
/**

0 commit comments

Comments
 (0)