MotirBuilding in public
MOTIR · moooon
onMotir
You’re viewing a public project. Anyone can view it — no account needed. Sign in to submit, upvote, or comment on requests.View-only — you can’t edit work items
MOTIR-2488

Create API token modal: the Create/Cancel footer is clipped OUTSIDE the dialog — the form bypasses `Modal.Body`, so on a tall variant there is no submit button and no scrollbar

Done
Description

Type · code (UI layout defect) · Parent · root sibling (discovered during MOTIR-2219, whose story 11.5 and epic MOTIR-1850 are both done) · Discovered in · manual dogfooding — minting a PAT for the cli-v0.2.0 anonymous-install verification (MOTIR-2219 step 4).

Symptom

Settings → Account → API tokens → Create opens the modal, and there is no Create token button and no Cancel button. Nothing scrolls: the panel has no scrollbar, so the footer cannot be reached by any means. The form is unusable — a PAT cannot be minted from the UI.

Root cause (verified against origin/main @ 0d5279da)

Modal's panel caps its own height and clips:

packages/design-system/src/components/ui/Modal.tsx:45
'flex max-h-[90vh] flex-col overflow-hidden'

The scroll recipe that makes that survivable lives in Modal.Body (Modal.tsx:238-256) — flex min-h-0 flex-1 flex-col overflow-y-auto. Its own doc comment names this exact failure: "instead of the dialog growing off-screen (e.g. the create modal's expandable Explanation pushing the Create button out of view)."

CreateTokenModal never uses Modal.Body (zero occurrences in the file). Its whole form phase is one bare <form className="flex flex-col gap-4"> (app/(authed)/settings/account/_components/CreateTokenModal.tsx:301) with Modal.Footer as its last child (:419-431). A flex item defaults to min-height: auto, so the form refuses to shrink below its content height; the panel clips the overflow; and because no ancestor sets overflow-y: auto, no scrollbar appears anywhere. The footer — Cancel and Create token — is simply painted outside the clip box.

Same class as the sticky-under-a-clipping-ancestor trap: the element is in the DOM and passes every unit assertion, but is unreachable on screen.

Why it is a TALL modal specifically

This is the tallest form in the app. It is deliberately widened to max-w-[42rem] so all six permission scopes show at once — "width, not scroll" (:261-265) — which trades width for a lot of height. It then grows further, conditionally:

  • multiOrg (account spans ≥2 organizations) adds a full-width label + Combobox row — :322-338
  • multiWorkspace (≥2 workspaces in the selected org) swaps the flat readonly field for a Combobox — :343-357
  • an empty permission-scope selection adds an error row — :409-417

Why no test caught it

Every test in tests/e2e/api-tokens.spec.ts runs against a fresh signup — one org, one workspace — at Playwright's default 1280×720. That is the shortest variant of the form, and it fits. The multi-org / multi-workspace variant, which is what a real dogfooding account renders, has no coverage at any viewport, and no test constrains viewport height.

Fix direction

Adopt the canonical pattern already used two files over in app/(authed)/_components/CreateIssueModal.tsx:222-227 — the form becomes the flex column, the fields go in Modal.Body, the footer stays a sibling INSIDE the form so type="submit" keeps working:

<form className="flex min-h-0 flex-1 flex-col" onSubmit={…}>
  <Modal.Body className="gap-4">…fields…</Modal.Body>
  <Modal.Footer className="shrink-0"></Modal.Footer>
</form>

RECURRENCE — this is the second instance of the class

MOTIR-462 (bug-sprint-report-modal-clipped-burndown) was the same defect — "SprintReport bypasses Modal.Body, so the burndown section is clipped off the bottom with no scroll affordance" — fixed at ONE site with no sweep of the others. A sweep of the remaining <Modal> call sites that never wrap their fields in Modal.Body found ~40 files; most are short confirm dialogs that fit, but BoardConfigEditor, WorkflowEditor, WidgetConfigModal and AutomationSettings are tall enough to be worth measuring. That sweep is NOT in this card's scope — it wants its own card.

Acceptance criteria

  • Opening Settings → Account → API tokens → Create on an account with ≥2 organizations, at a viewport 700px tall or shorter, shows the Create token button inside the viewport, and the fields scroll.
  • The fields area scrolls (Modal.Body) while the footer stays pinned; the panel itself never grows past 90vh.
  • Submitting still works via the form's onSubmit — the primary button keeps type="submit" and stays inside the <form>.
  • A Playwright regression test opens this modal with a MULTI-ORG fixture at a SHORT viewport and asserts the submit button is in the viewport and clickable — it must FAIL against the current code before the fix lands.
  • The existing tests/e2e/api-tokens.spec.ts cases still pass unchanged.

Resolution

FIXED — PR #1960, squash-merged as e67536bc (2026-08-08), full CI green.

The fields moved into Modal.Body (which owns the flex min-h-0 flex-1 overflow-y-auto scroll recipe) and the footer is pinned beside it with shrink-0, both still inside the <form> so type="submit" keeps working — the CreateIssueModal pattern named in the fix direction above.

The regression test breaks both blind axes at once, because either alone still passes: a ≥2-org account created server-side (a single-org account has no UI path to org #2), at a 1280×700 viewport, asserted with toBeInViewport() rather than toBeVisible() — the clipped button has a bounding box and answers every role query, which is precisely why unit and E2E both stayed green while the surface was unusable. Verified failing before the fix ("viewport ratio 0") and passing after; the four existing cases are untouched.

The class-level sweep is MOTIR-2491.