Skip to content

Count only rendered steps, and keep a data page's paginator in frame - #127

Merged
woksin merged 5 commits into
mainfrom
fix/datapage-paginator-within-layout-height
Aug 11, 2026
Merged

Count only rendered steps, and keep a data page's paginator in frame#127
woksin merged 5 commits into
mainfrom
fix/datapage-paginator-within-layout-height

Conversation

@woksin

@woksin woksin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

Summary

Two layout defects that only appear once there is enough content to expose them.

Fixed

  • A stepper counts only the steps it actually renders. A conditionally rendered panel still reaches it as a child, so a hidden step was counted: Submit never appeared on the real last step, a dead Next took its place, and clicking it landed on a step with nothing in it. Each hidden step cost another click, and the per-step validation gate read past the end of its own array and stopped blocking (StepperCommandDialog miscounts conditional steps #120)
  • A step that disappears after the user has already passed it no longer strands the wizard. The index is kept within the steps that still exist, so Submit stays reachable, Previous moves rather than absorbing a click per vanished step, and hiding every step no longer leaves a dead Next behind (StepperCommandDialog miscounts conditional steps #120)
  • A data page keeps its paginator inside the height it was given. The table grew to its content instead of filling the pane, so a full page of rows pushed the paging controls past the bottom of the frame and clipped them, leaving no way to reach the next page. A short page fit and looked correct (DataPage clips its paginator in a bounded-height container when actions are present #126)
  • A data page with a details component keeps its paginator in frame too. The split view sizes its panes from a stylesheet the package never imported, so the pane grew to content and clipped the paginator exactly as before, and the two panes stacked instead of siding. The stylesheet now ships with the component, so there is nothing for a consumer to import (DataPage clips its paginator in a bounded-height container when actions are present #126)

Changed

woksin and others added 2 commits August 10, 2026 22:25
A conditionally rendered panel still reaches the stepper as a child, so
counting children counted the ones React had already thrown away. With a
hidden last step the count ran one past the end: Submit never appeared,
a dead Next took its place, and clicking it landed on a step with nothing
in it. Two hidden steps cost two clicks. The per-step validation gate
went along with it, reading past the end of its own array and finding
nothing to block on.

Derive the panels once and let the count, the per-step field names and
the rendered children all come from that one list, so they cannot drift
apart again. The validation array was already built this way; only the
count was not, which is why the error indicators were right while the
navigation was wrong.

Submit stays gated on whole-form validity throughout, so no invalid
command could be submitted - this was navigation, not integrity.

Fragments are unchanged: a fragment still counts as one step and still
pools its panels' fields. Supporting them properly is a different change.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
The page asked its table to fill the height, but nothing above the table
had a height to fill: the split-view pane it sits in gets its size from a
stylesheet the library never imports, so the pane was an ordinary block
that grew to fit its content. The table grew with it and the page clipped
whatever ran past the bottom - which is where the paginator lives. A
short page fit and looked correct; a full one lost its paging controls,
and with them any way to reach the next page.

The pane now allocates what it was given: the action bar keeps its own
height, the table takes the rest and scrolls inside it, and the paginator
sits outside that scrolling region so it cannot leave the frame. When
there is no details pane to split, the split view is not used at all,
which removes the dependency on that unimported stylesheet for the
common case.

A data page still needs an ancestor with a real height - it cannot
invent one - so that is now written down with an example of each, and a
minimum height keeps a page that violates it usable rather than empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
@woksin woksin added the minor label Aug 10, 2026
@woksin

woksin commented Aug 10, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer context, kept out of the description because it is not release notes.

Verification. npx tsc -b no errors; yarn lint clean; yarn test 375 passed (baseline 333, so +42 across 6 new spec files); yarn build and yarn build-storybook both clean. Each item verified by mutation — reverting the fix must turn a named spec red — and restored by hand before the next step.

DataPage, DataTableForQuery and CommandStepper had no specs at all before this. That absence is why both defects shipped: every existing spec imports source and every DataPage story renders a single short page, so neither defect could appear.

Environment note that matters for anyone reproducing this. yarn.lock is gitignored and no lockfile is tracked, so installs are not reproducible and a branch switch silently carries dependency state across. This tree had drifted to PrimeReact 11 with jsdom missing, producing 125 phantom tsc errors that have nothing to do with either change. yarn install restored what main pins (PrimeReact 10.9.8, jsdom 30.0.1) and the baseline above is measured against that. Worth tracking separately — it is plausibly why #118's ESM defect reached publish.

One deliberate deviation. The single-pane branch keys on props.detailsComponent alone rather than detailsComponent && selectedItem. With the latter, the element flips between a plain div and the split view the moment a row is clicked, so React unmounts the subtree and the table loses its filter state and refetches on every select. Branching on the stable prop keeps the subtree mounted and still removes the split view from the single-pane case.

Deliberately out of scope.

Sequencing. #121 (footer Cancel) touches the same footer block as #120 and should be rebased onto this rather than developed in parallel. Open PR #107 (PrimeReact 11) rewrites the stepper wrappers; if it lands first this needs a rebase onto it.

woksin and others added 2 commits August 11, 2026 02:33
Counting only rendered steps made the step count change while a wizard is
open, and the index walking it was never clamped. A step hidden after the
user had already passed it left the index above the last one that still
exists: the comparison for the final step is an equality, so it stopped
matching, submit never appeared, and every further click pushed the index
further out. Hiding every step did the same thing from the other end.

The index is now clamped to the steps that exist and compared with
inequalities, so an index stranded above the end lands on the last real
step rather than past it. Previous moves the wizard instead of absorbing
one click per step that vanished.

Both siblings get the same treatment. They share the body that renders the
steps and each owns the same index state, so fixing one and not the other
would leave the second asking for a step index that no longer exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
Keeping the paginator in frame worked only on the path that stopped using
a split view. A page given a details component still mounts one, and the
split view positions its panes from a stylesheet this package never
imported - so the pane was an ordinary block that grew to content, the
height it was given resolved to auto, and the paginator was clipped exactly
as before. The two panes stacked instead of siding, too.

The component imports the stylesheet itself, the way every other component
here imports the one it needs. Allotment is already a dependency and
exports the file, so a consumer has nothing to add.

The specs could not have caught any of this. They asserted style
declarations, and jsdom applies no stylesheets - it cannot tell a height
that resolves from one that is inert, which is why putting the common path
back inside a split view left every test passing. The runner now processes
that one stylesheet, so the specs read computed values and a pane that is
not positioned reads as static and auto. The containment claims also assert
the paginator exists first: a claim that it sits outside the scrolling
region was satisfied by it not being there at all.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
@woksin woksin added patch and removed minor labels Aug 11, 2026
@woksin

woksin commented Aug 11, 2026

Copy link
Copy Markdown
Contributor Author

Reviewer context for 2c91e93 and 643ac2b, kept out of the description.

Where these came from. Independent code and security review. The security review found no concerns in the diff — and noted the stepper change actually closes a small disclosure, since React.Children.count counted false/null slots while toArray dropped them, so {isAdmin && <StepperPanel/>} gave a non-admin an inflated count, a live Next onto an empty panel, and a stepErrors[phantom] ?? false that read as valid. The code review required changes, and both of its merge-blocking findings are fixed here.

The specs could not fail on the DataPage defect, and that is now fixed at the root. Reverting the primary production change of c5ee3e1 previously left all 375 tests green: every assertion was on an inline style declaration, and jsdom applies no stylesheets, so it cannot tell an honored height: 100% from an inert one. The runner now processes exactly one stylesheet (css.include scoped to allotment/dist/style.css), so the specs read computed values and an unpositioned pane reads as static / auto. That same mutation now fails.

The containment claims also assert the paginator exists before claiming where it sits — scrollRegion.contains(paginator) was satisfied by the paginator being absent, since Node.contains(null) is false. Forcing it never to render previously left for_DataPage 40/40 green; it now fails four specs.

Route taken for the details path. The stylesheet is imported by the component, which is this package's actual convention — every stylesheet here is imported by the component that needs it, rollup.config.mjs externalises /\.css$/ so the import survives verbatim, package.json declares sideEffects: ["**/*.css"], and allotment is already a hard dependency exporting ./dist/*. Verified in the built output (dist/esm/DataPage/DataPage.js and the cjs twin) and in the Storybook bundle. Drag-to-resize is preserved; replacing Allotment was not needed.

Six mutations, each apply → red → capture → restore → green in one trap … EXIT command, every capture ending identical to pre-mutation. The two that matter most: putting the common path back inside the split view now fails (it was the previously-undetectable one), and commenting out the stylesheet import yields ['static / auto', 'static / auto'] against an expected ['absolute / 100%', …] — precisely the diagnosed failure.

One mutation came back green and was worth more than the five that went red. The first attempt at the dialog's clamp passed, because the clamp added to the shared body module masked it — an assertion guarding an invariant that a different module establishes. The behaviour the dialog's own clamp uniquely owns is that Previous moves the wizard rather than absorbing one click per vanished step; a spec for that is what kills it.

Browser spec: assessed and not built. Issue #126 asked for one. There is no browser-mode capability in this repo — no @vitest/browser, playwright, puppeteer, @storybook/test-runner or webdriverio, and vite.config.ts pins environment: 'node'. Adding it means a new test stack, a second Vitest project (the current config is single-project with fileParallelism: false), a playwright install --with-deps CI step and browser-binary caching, for one assertion. What the computed-style change buys instead is the ability to distinguish honored from inert; what remains genuinely uncovered is layout itself — "the paginator is within the visible frame and clickable" still needs a real browser.

Gate. tsc -b exit 0; eslint 0 errors and 0 warnings; root build and full rollup clean; Storybook builds; markdownlint clean. 394 specs pass, 0 fail (375 + 19). One pre-existing dead eslint-disable comment in DataPage.tsx was removed to reach zero warnings — confirmed against git show HEAD: that it warns there too, so it is not from this PR.

Found, not fixed — both outside this PR.

  • useCommandFormContext() identity is load-bearing and unguarded. CommandStepperContent's error effect memoizes on [stepFieldNames, getFieldError]; if the context object is recreated per render, getFieldError's identity changes every render, which fires the effect, which sets state, forever. A spec run hung on exactly this. Every existing stepper spec uses renderToStaticMarkup, which never runs effects — so no spec in this repo can see it. Arc presumably memoises the context, but nothing here asserts it, and a consumer that does not gets an infinite render loop.
  • scripts/copy-css.sh copies storybook-static/assets/*.css into dist/ when a local Storybook build exists — its find excludes node_modules, dist and .storybook, but not storybook-static. Thirteen Storybook artifacts landed in dist/{esm,cjs} on a local build. CI never hits it; a local yarn build && npm publish would ship them.

Deliberately left. minHeight: '20rem' is an unconditional floor rather than the documented fallback, with no opt-out — a page inside a bounded 10rem ancestor is forced to 20rem and overflows. The fix wants a minHeight prop defaulting to '20rem', which is a public-API addition and belongs in its own minor PR. Fragments still count as one step (honestly documented and pinned by a spec); #120 should be closed noting that.

Label corrected from minor to patch — both commits are bug fixes, no public surface is added or removed, and .ai/rules/pull-requests.md maps bug fixes to patch.

The stepper body compares against the last step with an inequality, and
that only differs from equality when nothing renders at all - so putting
the equality back left every spec passing while reinstating the defect
this branch exists to fix. The dialog's own spec cannot see it: the dialog
hides the body's footer, so the buttons only exist when the body is used
on its own, which is a published entry point.

The clamp against a negative index was unmeasured for the same reason, and
is now pinned where it is load-bearing.

The runner processes one stylesheet, so a computed value governed by any
other one reads as a browser default and asserts nothing. Said so where
the list is declared, since the next spec to read a computed value is
where that bites.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UWugkmN6NmoemKeTvSKNDg
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant