Skip to content

Responsive grid layout: fixed TopBar/Sidebar with document scroll#3178

Merged
david-crespo merged 16 commits intomainfrom
bl/responsiveness-grid-layout
Apr 22, 2026
Merged

Responsive grid layout: fixed TopBar/Sidebar with document scroll#3178
david-crespo merged 16 commits intomainfrom
bl/responsiveness-grid-layout

Conversation

@benjaminleonard
Copy link
Copy Markdown
Contributor

@benjaminleonard benjaminleonard commented Apr 13, 2026

Refactor of #2087

Switch from a CSS grid layout (where TopBar and Sidebar occupied grid cells alongside ContentPane) to fixed-position TopBar and Sidebar with document-level scrolling. This is the foundation for mobile/tablet support and likely the place that has the potential to cause the most issues so I'm separating the two.

  • Viewport meta tag: proper responsive width=device-width instead of fixed width=1050
  • CSS: add --sidebar-width variable, responsive --content-gutter (smaller below 1000px), remove overflow-y-hidden on body, set height: 100% on html/body/#root
  • TopBar: single fixed element (was two Fragment children occupying grid cells)
  • Sidebar: fixed position with overflow scroll
  • ContentPane: margin-left offset for sidebar, document-level scroll via min-h-[calc(100vh-...)]
  • Scroll restoration: window.scrollY instead of container .scrollTop
  • PageSkeleton: matches new fixed layout structure
  • E2e: scroll-restore test updated for document scroll, expectScrollTop/scrollTo utils use window

This PR should produce a console that is visually identical to the current one.


Broader responsive changes will come in another PR, and are not a blocker for this.

Todo

  • Fix table dropdown overlapping header
  • Double check modals are not causing layout shift

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 13, 2026

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

Project Deployment Actions Updated (UTC)
console Ready Ready Preview Apr 22, 2026 1:46pm

Request Review

benjaminleonard and others added 2 commits April 15, 2026 10:54
Switch from a CSS grid layout (where TopBar and Sidebar occupied grid
cells alongside ContentPane) to fixed-position TopBar and Sidebar with
document-level scroll. This is the foundation for mobile/tablet support.

Key changes:
- viewport meta tag: proper responsive instead of fixed 1050px
- CSS: add --sidebar-width var, responsive --content-gutter, remove
  body overflow-y-hidden, set html/body/#root height: 100%
- TopBar: single fixed element (was two grid cell fragments)
- Sidebar: fixed position with overflow scroll
- ContentPane: margin-left for sidebar on desktop, document scroll
- Scroll restoration: window.scrollY instead of container scrollTop
- PageSkeleton: match new fixed layout structure

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Comment thread app/components/PageSkeleton.tsx Outdated
* different points in the history stack will not share the same scroll position.
*/
export function useScrollRestoration(container: React.RefObject<HTMLElement | null>) {
export function useScrollRestoration() {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we even need this hook now? Since we're using a regular window scroll, perhaps we can use the built-in react-router one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to #2450 if I remove it I was unable to get react-router ScrollRestoration to work

Comment thread app/layouts/helpers.tsx
import { classed } from '~/util/classed'

export const PageContainer = classed.div`grid h-screen grid-cols-[14.25rem_1fr] grid-rows-[var(--top-bar-height)_1fr]`
export const PageContainer = classed.div`min-h-full pt-(--top-bar-height)`
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Top bar is fixed, this leaves space for it. Alternatively we use sticky for the top bar but the sidebar would still be fixed and this feels marginally cleaner.

@benjaminleonard
Copy link
Copy Markdown
Contributor Author

PR is visually identical to main with expanded visual regression tests. Haven't figured out an easy way to test it scrolled since the baseline would require a different scroll pattern to this new PR.

I'd suggest we err on merging sooner so it can be tested for longer.

@david-crespo
Copy link
Copy Markdown
Collaborator

david-crespo commented Apr 21, 2026

No visual changes — love it. Found one issue: double scroll bar on side modal when height is small enough. Probably worth updating the regression test if it didn't catch this.

https://console-git-bl-responsiveness-grid-layout-oxidecomputer.vercel.app/projects/mock-project/disks-new

image

@david-crespo
Copy link
Copy Markdown
Collaborator

david-crespo commented Apr 21, 2026

4d4e03f fixes the side modal scroll bar thing by turning on modal=true in the base UI dialog we use for both side model and regular middle modal. modal={false} a workaround for a Radix issue covered in #1745, where a native confirm() dialog stealing and returning focus triggered Radix's focus-outside handler and caused a dismiss loop.

In base-ui, this covers it and the modal=false bit is unnecessary.

<BaseDialog.Root
open={isOpen}
onOpenChange={(open, { reason }) => {
// Ignore focus-out to prevent a dismiss loop when a native confirm()
// dialog steals and returns focus (same role as the old Radix
// onFocusOutside preventDefault). See oxidecomputer/console#1745.
if (!open && reason !== 'focus-out') onDismiss()
}}

modal={false} has downsides — it disables scroll lock, focus trap, backdrop registration, and outside-content inerting — so it's nice to get rid of it.

In order to make this work I also added a scrim for the modal on top of a side modal situation with a special z-index that makes sure it's between the modal and the side modal and blocks interaction with the side modal. Right now on main you can interact with the image upload side modal while the main upload modal is going.

@david-crespo
Copy link
Copy Markdown
Collaborator

I have fixes for the e2e failures but I want to merge #3192 into main and main into this first because it affects at least one of them.

david-crespo added a commit that referenced this pull request Apr 22, 2026
Found this while working on #3178. Instance resize, attach floating IP,
and attach ephemeral IP all had `onError`s that triggered toasts on
server error. The first two also displayed the error in the modal.
Ephemeral IP did not. This PR gets rid of the toasts and puts an error
display into attach ephemeral IP. It doesn't look very good (maybe we
should move the error and give it a little box or something), but it's
basically the same as it already was, minus the extra toasts.

(Screenshot is from playwright, hence the weird missing submit button —
I think it's right in the middle of switching between the button text
and the loading spinner.)

<img width="930" height="528" alt="Screenshot 2026-04-21 at 5 20 23 PM"
src="https://github.com/user-attachments/assets/0c6db6a6-913f-40aa-8ccc-245be9dfc1d0"
/>
@david-crespo david-crespo merged commit 93f206c into main Apr 22, 2026
11 of 12 checks passed
@david-crespo david-crespo deleted the bl/responsiveness-grid-layout branch April 22, 2026 14:50
david-crespo added a commit that referenced this pull request Apr 23, 2026
Test failure in #3195 repros locally no problem:

<img width="747" height="409" alt="image"
src="https://github.com/user-attachments/assets/d1fed181-f26c-455b-a0ab-c8f8e962c1ae"
/>

Claude's speculation on why it suddenly became a problem after #3178 is
good enough for me. This is the second or third time lately we've run
into small issues because we're not awaiting
`queryClient.invalidateEndpoint` in the mutation handlers. Something to
keep an eye on.

> The race: `NetworkingTab` gates the Delete row action on `nic.primary
&& multipleNics`, where `multipleNics = nics.length > 1`. When the
`my-nic` delete mutation succeeds, `onSuccess` synchronously calls
`queryClient.invalidateEndpoint` (not awaited) and closes the confirm
modal. If the test opens nic-3's row-actions menu before the refetch
lands, `nics.length` is still 2, so Delete renders disabled and wrapped
in a `<Wrap with={<Tooltip/>}>`. When the refetch then lands mid-click,
the `<Wrap>` unwraps, the `Menu.Item` remounts (old DOM detached), and
on Safari/Firefox base-ui's Menu treats that focus/DOM churn as a close
— so Playwright's retry never resolves a clickable Delete and hits the
60s timeout.
> 
> When it was introduced: the disable rule landed April 2025 in #2806
("Disable primary NIC delete when multiple NICs present"). The
`DropdownMenu.Item` `<Wrap>`-with-Tooltip pattern has been in place
since ~March 2025. So the race has been latent for about a year.
> 
> Why it's surfacing now: first CI appearance was on the #3178
"Responsive grid layout" PR (2026-04-21), which landed on trunk
2026-04-22; every workflow run after that hits it. #3178 moved
TopBar/Sidebar from CSS-grid cells to fixed-position elements, switched
to document-level scroll, removed `overflow-y-hidden` on body, and set
`height: 100%` on html/body/#root. None of that touches the NIC flow,
but it changed paint/layout timing around modal open/close and table
reflow. That shifted the window such that `expect(my-nic
cell).toBeHidden()` now reliably resolves while the page is briefly
inert during the modal's close transition — before the refetch has
actually landed — so the test races into opening nic-3's menu in the
stale state. The layout PR didn't introduce the bug; it just removed the
timing cushion that had been hiding it.
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.

2 participants