ui: centre the pagination strip

It sat hard against the left edge of its container despite three call
sites wrapping it in `<center>` or `text-align: center`.

Mantine builds a Pagination as a BLOCK root -- full width, so those
wrappers had nothing to centre -- holding a flex Group that carries the
controls and lays them out at `flex-start`.  So `justify-content` on the
root does nothing at all; it has to reach the Group.

Setting `--group-justify` on the root does not work either, which cost a
detour: Group defaults that prop and writes the variable INLINE on its own
element, where an inherited value cannot reach it.  The property itself is
fair game, since Mantine only ever reads the variable from a hashed class,
so `.wrolpi-pagination .mantine-Group-root { justify-content: center }` at
(0,2,0) is what lands.

The three wrappers are now plain divs keeping their margin.  `<center>` is
a deprecated element that was doing nothing here.

Two tests: the gap either side is equal -- measured, not read off
`justify-content` -- with an assertion that the container really is wider
than the strip, since otherwise the equality holds trivially at 0 and 0.
And an inverse: at 240px nothing hangs off the LEFT edge.  That one is the
risk centring introduces -- overflow in a flex-start row spills right,
where it can be scrolled to, but centred it spills both ways and the first
page becomes unreachable.  It passed before this change and must keep
passing after.

Verified red first (0px left against 230px right), and the wrong-element
fix -- centring the root rather than the Group -- reddens it again.

1080 jest, 160 in ui-layout.cy.js, clean build.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Roland 2026-08-02 13:00:56 -06:00
parent 32f73cc3c9
commit c643f90c19
No known key found for this signature in database
GPG key ID: 1BA620CE6C9A4680
5 changed files with 79 additions and 6 deletions

View file

@ -333,9 +333,9 @@ export function FilesView(
) {
const {view} = useSearchView();
const paginator = <center style={{marginTop: '2em'}}>
const paginator = <div style={{marginTop: '2em'}}>
<Paginator activePage={activePage} totalPages={totalPages} onPageChange={setPage}/>
</center>;
</div>;
let body;
if (view === 'list') {

View file

@ -124,7 +124,7 @@ export function MapSearchView() {
<MapPlaceCard key={`${place.name}-${place.lat}-${place.lon}-${i}`} place={place} index={i}/>
)}
</CardGroup>
<div style={{textAlign: 'center', marginTop: '2em'}}>
<div style={{marginTop: '2em'}}>
<Paginator activePage={activePage} totalPages={totalPages} onPageChange={setPage}/>
</div>
</React.Fragment>;

View file

@ -223,9 +223,9 @@ const ZimAccordionItem = ({value, active, data, searchStr, activeTags}) => {
}
}
const paginator = <center style={{marginTop: '2em'}}>
const paginator = <div style={{marginTop: '2em'}}>
<Paginator activePage={pages.activePage} totalPages={pages.totalPages} onPageChange={pages.setPage}/>
</center>;
</div>;
const label = <Label color={estimate > 0 ? 'violet' : undefined}>{normalizeEstimate(estimate)}</Label>;

View file

@ -1,7 +1,7 @@
import React from 'react';
import {
ActionInput, Button, Card, Header, Icon, IconButton, IconStack, Loading, MultiSelect, Panel, PathInput,
Message, Placeholder, Progress, SearchBox, Statistic, StatisticGroup, Status, TabBar, Table,
Message, Pagination, Placeholder, Progress, SearchBox, Statistic, StatisticGroup, Status, TabBar, Table,
tabClassName, TextInput,
} from './index';
import {MemoryRouter} from 'react-router';
@ -2020,3 +2020,54 @@ describe('the search clear button matches the field it clears', () => {
});
});
});
describe('pagination sits in the middle of its container', () => {
/*
* Three call sites wrap the pager in `<center>` or `text-align: center` and it still sat
* hard against the left edge. Both of those centre INLINE content, and Mantine's
* Pagination root is a flex container that fills its parent's width -- so the buttons
* were laid out at `flex-start` inside a box that was already the full width, and no
* amount of centring outside it could move them.
*
* Measured as the gap on each side rather than by reading `justify-content`, so the
* claim is about where the control actually lands.
*/
it('leaves the same gap either side', () => {
cy.mountUI(<div style={{width: 600}}>
<Pagination activePage={3} totalPages={12} onPageChange={() => {}}/>
</div>);
cy.get('.wrolpi-pagination').should(($pager) => {
const container = $pager[0].getBoundingClientRect();
const buttons = [...$pager[0].querySelectorAll('button')]
.map(button => button.getBoundingClientRect());
expect(buttons.length, 'the pager rendered its controls').to.be.greaterThan(3);
const left = Math.min(...buttons.map(b => b.left)) - container.left;
const right = container.right - Math.max(...buttons.map(b => b.right));
// The premise: the container really is wider than the strip, so there is slack
// to distribute. Without this the equality holds trivially at 0 and 0.
expect(left + right, 'the container is wider than the strip').to.be.greaterThan(40);
expect(left, 'gap left vs gap right').to.be.closeTo(right, 1);
});
});
it('does not push its first page off the left edge when space is tight', () => {
/*
* The risk centring introduces. Overflow in a `flex-start` row spills to the right,
* where it can at least be scrolled to; centred, it spills BOTH ways and the first
* page number is unreachable. 240px is narrower than any real layout gives it.
*/
cy.mountUI(<div style={{width: 240}}>
<Pagination activePage={6} totalPages={99} onPageChange={() => {}} showFirstAndLast/>
</div>);
cy.get('.wrolpi-pagination').should(($pager) => {
const container = $pager[0].getBoundingClientRect();
const buttons = [...$pager[0].querySelectorAll('button')]
.map(button => button.getBoundingClientRect());
expect(Math.min(...buttons.map(b => b.left)), 'nothing hangs off the left')
.to.be.at.least(container.left - 0.5);
});
});
});

View file

@ -190,6 +190,28 @@ html[data-theme="amber"] .wrolpi-theme-option-active {
/* ------------------------------------------------------------ Pagination */
/*
* The pager is centred here rather than by whoever renders it. Three call sites wrapped it
* in `<center>` or `text-align: center` and it still sat hard against the left edge.
*
* Mantine builds a Pagination as a BLOCK root -- full width, so those wrappers had nothing
* to centre -- holding a flex Group that carries the controls and lays them out at
* `flex-start`. `justify-content` on the root therefore does nothing at all; it has to
* reach the Group.
*
* Setting `--group-justify` on the root does not work either: Group defaults that prop and
* writes the variable INLINE on its own element, where an inherited value cannot reach it.
* The property itself is fair game, though -- Mantine only ever reads the variable from a
* hashed class -- so this rule at (0,2,0) is what actually lands.
*
* The Group already wraps by default (`--group-wrap` falls back to `wrap`), which matters
* once the row is centred: centred overflow spills off BOTH edges, and the first page would
* be unreachable rather than merely off-screen to the right.
*/
.wrolpi-pagination .mantine-Group-root {
justify-content: center;
}
.wrolpi-pagination button {
background: var(--panel);
color: var(--text);