fix(ui): the page's end scrolls out from under Back to top

The button floats over the bottom-right corner, and at the end of a long table it covered the closing
pager's '100' and 'All'. While it is up, PageScroller ends the page a button's height later (only
then, so a full-height layout gets no scrollbar). Its shown state is a shared store read through a
use… wrapper: a store's .use() called straight from a component is memoized by the React Compiler
and never re-renders (contract: store.contract.test.ts).
This commit is contained in:
2026-09-27 16:28:18 +02:00
parent 51f881681b
commit 86884e368e
5 changed files with 73 additions and 6 deletions
@@ -2,6 +2,7 @@ import { useCallback, useLayoutEffect, useRef, useState, type ReactNode } from "
import { useScrollFade } from "../../lib/useScrollFade";
import { FOCUS_REST_ATTRS, useFocusKeeper } from "../../lib/useFocusKeeper";
import { StickyTopProvider } from "./stickyTop";
import { useBackToTopShown } from "./backToTopState";
import { CONTENT_ID } from "./landmarks";
import { ensureNavId, peekNavId, subscribeNav } from "../../lib/history";
@@ -93,6 +94,7 @@ export default function PageScroller({
return () => setStickyTops((n) => n - 1);
}, []);
const fade = useScrollFade(20, stickyTops === 0);
const fabUp = useBackToTopShown();
const fadeRef = fade.ref;
const elRef = useRef<HTMLElement | null>(null);
// Stable identity: an inline ref callback changes every render, which makes React detach and
@@ -169,6 +171,10 @@ export default function PageScroller({
className="flex-1 min-w-0 overflow-y-auto scrollbar-gutter-stable focus:outline-none"
>
<StickyTopProvider value={registerStickyTop}>{children}</StickyTopProvider>
{/* While Back to top floats over the corner, the page ends a button's height (3rem + its 1.25rem
inset) later, so its last row or pager can scroll out from under it. Only then: a spacer
always there would give a full-height layout a scrollbar it does not need. */}
{fabUp && <div aria-hidden="true" className="h-18" />}
</main>
);
}