Skip to content

feat(Dialog): pin DialogForm actions in a footer - #1424

Open
tenphi wants to merge 2 commits into
mainfrom
andrew/cub-4920-dialogform-footer
Open

tenphi wants to merge 2 commits into
mainfrom
andrew/cub-4920-dialogform-footer

Conversation

@tenphi

@tenphi tenphi commented Sep 22, 2026

Copy link
Copy Markdown
Member

Closes CUB-4920.

The gap

DialogForm rendered Dialog > Header + Content > Form > [children, ButtonGroup]. The actions lived inside the scrolling body, so they rode the scroll away — and a dialog that needed pinned actions could not use DialogForm at all.

Consumers dropped it, hand-composed Dialog > Form > Content + Footer, and then had to carry the dialog's flex context through the intervening <form> themselves. The same four declarations arrived verbatim in two unrelated Cloud features, each rediscovering height: 'min 0' by debugging a footer pushed off the bottom of the dialog.

The change

1. DialogForm pins its actions. The form now wraps both slots — Dialog > Header + Form > [Content, Footer > ButtonGroup] — so the body scrolls while Submit and Cancel stay put. The buttons are still inside the <form>, which is why the form is the outer of the two and native submit is unchanged. Both the legacy and the modern branch move.

2. Dialog hands a direct-child <form> its flex context, so the hand-composed shape needs no plumbing:

<Dialog>
  <Header><Title>Edit</Title></Header>
  <Form>                        {/* no styles needed */}
    <Content>{/* scrolls */}</Content>
    <Footer><Button htmlType="submit">Save</Button></Footer>
  </Form>
</Dialog>

A form nested deeper is the consumer's own layout and is left alone ($: '> form' is a raw tag selector — direct child only, no data-element on Form).

3. The footer draws a top line only while the body scrolls. A permanent line looked wrong on short forms: nothing continues beneath the actions, so the rule is clutter. Once Content overflows, the footer sits over content that carries on underneath it, and the line is what says so. Footer carries a content-overflow modifier while the body overflows, and its border keys off it. border in a footer's own styles still overrides it both ways ('top' for always, false for never).

Dialog tracks this itself (use-content-overflow.ts), so plain dialogs and hand-composed forms get the line too. It finds its body structurally: the Content that is a direct child of the dialog or of a direct-child <form>, which is the same pair of shapes the flex pass-through supports. Content now renders data-id="Content", like Footer already does, so a consumer qa cannot hide it. The body's own box isn't enough to watch: it stops growing at max height and never changes in a fixed-height dialog (fullscreen, panel). So the hook also observes the body's children for resizes, and the child lists of the dialog, a direct-child form and the body for insertions. It never observes the subtree, because a picker's popover is a Dialog too and a virtualized list scrolling inside it mutates every frame. Pure-CSS routes were ruled out: scroll-driven animations don't cover our Firefox target, scroll-state() queries are Chrome-only and can't style a sibling, and a background-painted line would be covered by every input scrolling past it.

⚠️ This changes how every dialog with actions looks

Actions now sit below Content's bottom padding plus Footer's top padding, rather than a single content gap. Measured: 17px → 41px between the last field and the buttons.

That +24px is exactly the chrome any hand-composed dialog footer already has, so this brings DialogForm into line with the rest of the library rather than giving it a look of its own. But it is a visible change to every dialog in the product and wants a Chromatic review pass.

Verification

Gate Result
pnpm test 2827 passed
pnpm test:browser 24 files, 235 passed
pnpm build (emits .d.ts — the broad typecheck) exit 0
pnpm test:types / test:types:consumer exit 0
pnpm audit-docs (Dialog, DialogForm) 0 issues
pnpm size 590.8 kB vs 590.48 kB on main, budget 592 kB (fresh builds)
pnpm chromatic:check budget passed

The new browser test was falsified, not just run. Pinning and scrolling are both layout, and jsdom lays nothing out — every getBoundingClientRect() is 0 × 0 and scrollHeight === clientHeight, so a build with the footer pushed off the bottom would pass a jsdom spec that looked identical.

Removing only height: 'min 0' — the single declaration the ticket says nobody guesses — fails all four cases. Restored, all four pass. The suite covers the legacy branch, the modern branch (ModernDialogForm is separate JSX and was initially untested), and the hand-composed shape with no styles on the form.

The footer-line cases were falsified piece by piece. There are four: no line under a body that fits; a line once it scrolls, gone again when it fits; children added to a fixed-height body; one child growing in place inside it. Dropping the footer modifier fails three of them. Dropping the > form half of the body selector fails the DialogForm case. Dropping the child resize observation fails only the grow-in-place case. Ignoring mutations fails only the added-children case. Two things had to be fixed before the last two breaks failed at all. The tests now wait for Modal's data-entered plus two frames, because the open transition's own resizes re-measured the body and detected the change by accident. The fixed-height fixture also reserves a stable scrollbar gutter, because a scrollbar that takes room narrows the body when it appears and fires its observer the same way.

The frozen legacy-contract/ DialogForm cases (§7.1 #34) pass unchanged — they assert submit, delayed-reset and preserve behaviour, none of which this touches.

Note on flow

The ticket flags that Form's base styles already set flow: column, so the explicit flow: 'column' in BudgetModalForm.tsx is a restated default. That is Cloud-side and not touched here.

Follow-up

Both Cloud call sites can drop their FORM_STYLES block and move back onto DialogForm once console-ui bumps its pin.

🤖 Generated with Claude Code

`DialogForm` rendered `Dialog > Header + Content > Form > [children,
ButtonGroup]`, so its actions lived inside the scrolling body and rode
the scroll away. A dialog that needed pinned actions could not use
`DialogForm` at all.

The form now wraps both slots — `Dialog > Header + Form > [Content,
Footer > ButtonGroup]` — so the body scrolls while Submit and Cancel stay
put. The buttons are still inside the `<form>`, which is why the form is
the outer of the two and native submit is unchanged. Both the legacy and
the modern branch move.

`Dialog` also hands a DIRECT-CHILD `<form>` its flex context, so the same
shape composed by hand needs no plumbing. Consumers were restating
`display:flex / flexGrow:1 / height:'min 0' / gap:0` verbatim across
unrelated features, each rediscovering the `min-height: 0` by debugging a
footer pushed off the dialog. A form nested deeper is the consumer's own
layout and is left alone.

Actions now sit below `Content`'s bottom padding plus `Footer`'s top
padding rather than one content gap, so a dialog with actions gains 24px
between the last field and the buttons — measured 17px to 41px. That is
the same spacing every hand-composed dialog footer already has.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@changeset-bot

changeset-bot Bot commented Sep 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 836c2e4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cube-dev/ui-kit Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel

vercel Bot commented Sep 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cube-ui-kit Ready Ready Preview Sep 22, 2026 6:32pm UTC

Request Review

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

📦 NPM canary release

Deployed canary version 0.0.0-canary-50ea57a.

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

🧪 Storybook is successfully deployed!

@github-actions

github-actions Bot commented Sep 22, 2026

Copy link
Copy Markdown
Contributor

🏋️ Size limit report

Name Size Passed?
All 577.35 KB (+0.06% 🔺) Yes 🎉
Tree shaking (just a Button) 128.29 KB (0% 🟰) Yes 🎉

Compared against main at 565dc93run 35764087638, 2026-09-22T17:58:13Z.

To see which modules changed, download the size-limit-statoscope-report artifact from this run and open report.html.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

This branch was successfully deployed

2 active deployments
Preview 836c2e42 Deployed Sep 22, 2026 by vercel[bot]
Chromatic staging 836c2e42 Deployed Sep 22, 2026 by tenphi via Prepare Storybook for review & tests #3857
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant