Skip to content

Keep the settings dialog inside the window as accordion sections are opened - #398

Merged
lstein merged 2 commits into
masterfrom
lstein/fix/settings-accordion-overflow
Sep 17, 2026
Merged

lstein merged 2 commits into
masterfrom
lstein/fix/settings-accordion-overflow

Conversation

@lstein

@lstein lstein commented Sep 16, 2026

Copy link
Copy Markdown
Owner

What

Opening enough accordion sections in the Settings dialog made it taller than the browser window. It is centred in a fixed, non-scrolling overlay, so it spilled off both edges — and the part above the top edge, the title and the ✖ close button included, could not be reached at all.

Opening a section now measures the dialog and collapses the sections opened before it, oldest first, until it fits. A queue tracks expanded sections in the order they were opened, so the section you just clicked is always the one that survives, and the one you opened longest ago is the first to go.

How it decides what "fits" means

  • The budget comes from visibleViewportBottom() (panel-anchor.js) rather than innerHeight, so a stranded iPadOS layout viewport does not fool it.
  • #settingsOverlay is now centred within that same visible height. Limiting only the dialog's size is not enough: centred in a layout viewport taller than the visible one, a dialog of exactly the allowed height still hangs off the bottom of the screen by half the difference, and a fixed overlay does not scroll. Measured before the fix at layout 800 / visible 500: top 166, bottom 634 — 134px unreachable. After: top 16, bottom 484.
  • The check also runs when the modal opens, because the persisted open/closed state cannot be measured while the dialog is display:none and may have been saved on a larger window.
  • openSettingsModal() now awaits populateModalFields() before measuring. A reachable InvokeAI backend reveals the username, password and board rows a round trip later; the old fire-and-forget call measured a dialog ~170px shorter than the one the user ends up with, and nothing re-checked it.
  • A raised software keyboard shrinks the visible viewport exactly as a stranded layout viewport does, and tapping a header blurs the focused field first — so on iPad the collapse would have run on the keyboard's reading, and written it to localStorage where it outlives the keyboard. panel-anchor.js now exports visibleViewportSettled() (it already held this state for its own panel corrections) and the fit does nothing until the measurement is trustworthy.
  • The last open section is never collapsed: a section taller than the window on its own would otherwise slam shut the moment you opened it. That case falls through to a new max-height on .settings-modal, which scrolls instead. Its budget subtracts the margin in px and hands back .modal-content's padding in em, so the CSS and the JS agree on the allowed height at any root font size (verified at 12/16/20/24px).

Also: the accordion click handler is now a header.onclick property assignment instead of addEventListener, so a second setupAccordions() pass over the same dialog cannot stack handlers that toggle a section twice per click.

Testing

  • New tests/frontend/settings-accordion-fit.test.js, 12 cases: collapse order by age rather than document order, re-opening refreshing the queue, closing collapsing nothing, the single over-tall section being kept, restore-trimming on open, measuring only after the async fields land, the keyboard guard, and an exact-budget boundary case. Mutation-checked — flipping the margin constant, dropping the await, or dropping the keyboard guard each fail the suite.
  • Full suite: 54 suites / 855 tests pass; npm run lint and format:check clean.
  • Driven headless against the real app: at 900px three sections fit and opening API Integration collapses all three; at 420px with one oversized section open the dialog lands fully inside the window and scrolls internally.

Known, not addressed here

  • .modal-close is position: absolute inside what is now a scroll container, so in the single-over-tall-section fallback the ✖ scrolls out of view. The backdrop is still clickable, so it is a wart rather than a lockout.
  • The dialog is not re-fitted on window resize while it is already open — the max-height fallback catches it, but the invariant only holds at click and open time.
  • Unrelated but found while reviewing this: settingsUpdated is dispatched on window (state.js) but listened for on document (settings.js:846), so initializeSettings never actually re-runs. Several comments in the file assume it does. Left alone here — fixing it would change behaviour across the whole settings module.

🤖 Generated with Claude Code

lstein and others added 2 commits September 16, 2026 17:16
The settings modal grows with its content and is centred in a fixed,
non-scrolling overlay, so expanding a few accordion sections pushed it past
both edges of the window — and the half above the top edge, title and close
button included, could not be scrolled to.

Opening a section now measures the dialog and collapses the sections opened
before it, oldest first, until it fits again. A module-level queue tracks
expanded sections in the order they were opened, so the section just opened is
last in line and the one opened longest ago gives up its space first.

Details worth knowing:

- The budget comes from panel-anchor's visibleViewportBottom() rather than
  innerHeight, and #settingsOverlay is now centred within that same visible
  height. Limiting only the dialog's size is not enough: centred in a layout
  viewport taller than the visible one, a dialog of exactly the allowed height
  still hangs off the bottom of the screen by half the difference.

- A raised software keyboard shrinks the visible viewport exactly as a
  stranded layout viewport does, and tapping a header blurs the field first,
  so the collapse would have run on the keyboard's reading — and persisted it.
  panel-anchor now exports visibleViewportSettled(), and the fit does nothing
  until the measurement is trustworthy again.

- openSettingsModal() awaits populateModalFields() before measuring. A
  reachable InvokeAI backend reveals the username, password and board rows a
  round trip later, and the old fire-and-forget call measured a modal ~170px
  shorter than the one the user ended up with.

- .settings-modal gains a max-height and scrolls as a last resort, for the one
  case the collapsing cannot help: a single section taller than the window,
  which is never collapsed because shutting it would make it unopenable. Its
  budget subtracts the margin in px and hands back .modal-content's padding in
  em, so it agrees with the JS at any root font size.

- The accordion click handler is a header.onclick property assignment rather
  than addEventListener, so running setupAccordions twice over the same dialog
  cannot stack handlers that toggle a section twice per click.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@lstein
lstein enabled auto-merge (squash) September 17, 2026 00:26
@lstein
lstein merged commit c4b4060 into master Sep 17, 2026
10 checks passed
@lstein
lstein deleted the lstein/fix/settings-accordion-overflow branch September 17, 2026 00:32
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