Fix document row a11y and PDF page sync

Expose pending access status to assistive tech on
mobile rows, and recompute the visible PDF page
after fit-to-width reflow without writing refs
during render.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-07-20 17:07:51 +02:00
parent d047f1b323
commit 07645e6251
3 changed files with 74 additions and 15 deletions

View File

@@ -74,6 +74,19 @@ export function DocumentAccessAction({
const { t } = useTranslation("documents");
if (!interactive) {
// Pending rows have no mobile hit overlay — expose status for assistive tech.
if (requested) {
return (
<span
className="flex size-8 items-center justify-center"
role="status"
aria-label={t("actions.requested")}
>
<ClockIcon className="size-4 text-sand-11" aria-hidden />
</span>
);
}
return (
<span className="flex size-8 items-center justify-center" aria-hidden>
<StatusIcon

View File

@@ -91,8 +91,12 @@ export function DocumentEntry({
/>
</div>
{/* Mobile: status icon only; the row overlay handles activation. */}
<div className="hidden shrink-0 max-sm:block" aria-hidden={mobileHitLabel != null}>
{/* Mobile: status icon only; the row overlay handles activation.
Pending (`requested`) rows keep the icon in the a11y tree as status. */}
<div
className="hidden shrink-0 max-sm:block"
aria-hidden={mobileHitLabel != null ? true : undefined}
>
<DocumentAccessAction
isAuthorized={isAuthorized}
requested={requested}

View File

@@ -40,6 +40,24 @@ pdfjs.GlobalWorkerOptions.workerSrc = workerSrc;
// Horizontal inset so pages don't kiss the viewport edge on phones.
const PAGE_GUTTER_PX = 32;
function findCenteredPage(
wrapper: HTMLDivElement,
pages: ArrayLike<HTMLElement | null | undefined>,
): number | null {
const middle = wrapper.getBoundingClientRect().top + wrapper.clientHeight / 2;
for (let index = 0; index < pages.length; index += 1) {
const page = pages[index];
if (page == null) {
continue;
}
const rect = page.getBoundingClientRect();
if (rect.top <= middle && rect.bottom >= middle) {
return index + 1;
}
}
return null;
}
export interface PdfPreviewHandle {
scrollToPage: (page: number) => void;
}
@@ -73,6 +91,18 @@ export function PdfPreview({ file, scale, ref, onNumPages, onVisiblePageChange }
},
}), []);
const resolveVisiblePage = () => {
const wrapper = wrapperRef.current;
const pages = documentRef.current?.pages.current;
if (!wrapper || !pages?.length) {
return;
}
const page = findCenteredPage(wrapper, pages);
if (page != null) {
onVisiblePageChange(page);
}
};
useEffect(() => {
const wrapper = wrapperRef.current;
if (!wrapper) {
@@ -89,21 +119,33 @@ export function PdfPreview({ file, scale, ref, onNumPages, onVisiblePageChange }
return () => observer.disconnect();
}, []);
const resolveVisiblePage = () => {
const wrapper = wrapperRef.current;
const pages = documentRef.current?.pages.current;
if (!wrapper || !pages?.length) {
// After fit-to-width / zoom reflow, the same scroll offset can center a
// different page — recompute once the resized pages have painted.
useEffect(() => {
if (pageWidth == null || numPages === 0) {
return;
}
const middle = wrapper.getBoundingClientRect().top + wrapper.clientHeight / 2;
for (let index = 0; index < pages.length; index += 1) {
const rect = pages[index].getBoundingClientRect();
if (rect.top <= middle && rect.bottom >= middle) {
onVisiblePageChange(index + 1);
return;
}
}
};
let secondFrame = 0;
const firstFrame = requestAnimationFrame(() => {
secondFrame = requestAnimationFrame(() => {
const wrapper = wrapperRef.current;
const pages = documentRef.current?.pages.current;
if (!wrapper || !pages?.length) {
return;
}
const page = findCenteredPage(wrapper, pages);
if (page != null) {
onVisiblePageChange(page);
}
});
});
return () => {
cancelAnimationFrame(firstFrame);
cancelAnimationFrame(secondFrame);
};
}, [pageWidth, scale, numPages, onVisiblePageChange]);
const slots = pdfPreview();