Skip to content

Commit e309f3a

Browse files
authored
Fix listing page hash lost on a click right after a page change (#15233)
list.js rebuilds the pagination links on every update, and the handler that writes the page hash (and blocks the links' href="#") was only re-attached in a setTimeout after each update. A click on a rebuilt link before that timer fired changed the page through list.js but followed href="#", leaving the URL at index.html# with no page hash. This showed up as an intermittent firefox failure of the page hash Playwright test on the macOS nightly run. The deferral could not simply be dropped: list.js dispatches "updated" handlers in reverse registration order, so our handler runs before the pagination plugin rebuilds the links. Instead, attach one click listener per listing on its ul.pagination, which list.js keeps across updates, so there is no window without a handler. The new test clicks page 2 then page 3 in a single task, so no timer can run between the update and the second click; it failed in chromium, firefox and webkit before this change.
1 parent 32dc3b4 commit e309f3a

2 files changed

Lines changed: 37 additions & 16 deletions

File tree

‎src/resources/projects/website/listing/quarto-listing.js‎

Lines changed: 20 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -48,19 +48,16 @@ window["quarto-listing-loaded"] = () => {
4848
// The actual list
4949
const list = window["quarto-listings"][listingId];
5050

51-
// Update the handlers for pagination events
52-
refreshPaginationHandlers(listingId);
51+
// Record the page hash on pagination clicks
52+
attachPaginationHandler(listingId);
5353

5454
// Render any visible items that need it
5555
renderVisibleProgressiveImages(list);
5656

57-
// Whenever the list is updated, we also need to
58-
// attach handlers to the new pagination elements
59-
// and refresh any newly visible items.
57+
// Whenever the list is updated, refresh any newly visible items.
6058
list.on("updated", function () {
6159
renderVisibleProgressiveImages(list);
6260
window.quartoDecorateExternalLinks?.(list.list);
63-
setTimeout(() => refreshPaginationHandlers(listingId));
6461

6562
// Show or hide the no matching message
6663
toggleNoMatchingMessage(list);
@@ -130,19 +127,26 @@ function getListingPageKey(listingId) {
130127
return `${listingId}-page`;
131128
}
132129

133-
function refreshPaginationHandlers(listingId) {
130+
function attachPaginationHandler(listingId) {
131+
// list.js rebuilds the page links on every update but keeps their
132+
// container, so a single listener there covers every link.
134133
const listingEl = window.document.getElementById(listingId);
135-
const paginationEls = listingEl.querySelectorAll(
136-
".pagination li.page-item:not(.disabled) .page.page-link"
137-
);
138-
for (const paginationEl of paginationEls) {
134+
const paginationEl = listingEl.querySelector(".pagination");
135+
if (!paginationEl) {
136+
return;
137+
}
138+
paginationEl.addEventListener("click", (event) => {
139+
const pageLink = event.target.closest(
140+
"li.page-item:not(.disabled) .page.page-link"
141+
);
142+
if (!pageLink) {
143+
return;
144+
}
139145
// list.js's pagination plugin shows the page from its own click
140146
// listener on the pagination list; this handler only records the hash.
141-
paginationEl.onclick = (sender) => {
142-
setPageHash(listingId, sender.target.getAttribute("data-i"));
143-
return false;
144-
};
145-
}
147+
event.preventDefault();
148+
setPageHash(listingId, pageLink.getAttribute("data-i"));
149+
});
146150
}
147151

148152
function renderVisibleProgressiveImages(list) {

‎tests/integration/playwright/tests/listing-interactions.spec.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,23 @@ test.describe("Listing pagination", () => {
8080
await expect(page).toHaveURL(/#listing-second-listing-page=3$/);
8181
});
8282

83+
test("page link clicked right after an update writes the page hash", async ({ page }) => {
84+
await page.goto(fixture);
85+
86+
// Click page 2 then page 3 in one task, so no timer runs between the
87+
// update that rebuilds the links and the click on a rebuilt link.
88+
await page.evaluate(() => {
89+
const link = (n: string) =>
90+
[...document.querySelectorAll<HTMLElement>("#second-listing-pagination .page.page-link")]
91+
.find((el) => el.textContent?.trim() === n)!;
92+
link("2").click();
93+
link("3").click();
94+
});
95+
96+
await expect(titles(page, "second-listing")).toHaveText(["Post 3"]);
97+
await expect(page).toHaveURL(/#listing-second-listing-page=3$/);
98+
});
99+
83100
test("back restores the previous page", async ({ page }) => {
84101
await page.goto(fixture);
85102
await pageLink(page, "second-listing", "2").click();

0 commit comments

Comments
 (0)