Skip to content

Shared full-height WP admin layout: replace per-plugin hard-coded calc(100dvh - 96px) heights (breaks under third-party admin banners) #487

Description

@OjasAT2105

Bug class (org-wide)

Every Sure-family admin SPA hard-codes the vertical overhead above its app and sizes itself with viewport math like calc(100dvh - 96px) (desktop) / calc(100dvh - 110px) (mobile). The constants assume exactly two things above the app: the WP admin bar (32px / 46px) and the plugin's own 64px topbar.

That assumption is false whenever ANY other plugin injects content into the admin above our app (in_admin_header, unclassed banners, promo bars). The math does not know the banner exists, so the app overflows the viewport and its bottom controls (Save button, sidebar items) are pushed off-screen.

Real-world impact: SureCookie support ticket 1507247 - MemberPress Courses' admin banner clipped SureCookie's Save button with no way to scroll to it; the customer's only workaround was browser zoom-out.

Confirmed occurrences of the same copy-forwarded pattern:

  • surecookie: src/admin/AdminApp.jsx - grid-rows-[64px_calc(100dvh_-_96px)] + h-[calc(100vh-64px)] (fixed height, worst symptom: content clipped, unreachable) - fixed via mechanism 2 below
  • surerank: src/apps/admin-components/layout/sidebar-layout.js:619 - min-h-[calc(100dvh_-_96px)] / 110px
  • suremails: src/App.js:37 - min-h-[calc(100dvh_-_96px)] / 110px
  • reported also in sureforms and surecontact (same template lineage)

Root cause: the admin layout was copied plugin-to-plugin instead of living in the shared library, so the wrong constant shipped everywhere. A build-time constant can never be correct here - other plugins' banner heights are unknowable.

Proper fix - two technically sound mechanisms

Mechanism 1 - flow-based, CSS-only (recommended for the shared component):
Scoped to the plugin's own admin screen (body class), turn the WP containers into a flex column and let the app take the remaining space:

body.toplevel_page_[slug] #wpcontent,
body.toplevel_page_[slug] #wpbody,
body.toplevel_page_[slug] #wpbody-content {
    display: flex;
    flex-direction: column;
    padding: 0;
}
body.toplevel_page_[slug] #wpcontent {
    min-height: calc(100vh - var(--wp-admin--admin-bar--height, 32px));
}
.[app-root] {
    flex: 1 1 auto;
    min-height: 0;
}

WP core owns --wp-admin--admin-bar--height (32px, 46px under 783px - wp-includes/css/admin-bar.css), so the only remaining constant is maintained by core itself. Injected banners become normal flex items: they take their natural height, the app takes the rest, and a dismissed banner reflows automatically with zero JS. Needs a QA pass for float-era markup (#wpfooter, screen-meta) inside the flex column, scoped per plugin page only.

Mechanism 2 - measured offset (JS, shipped in SureCookie as reference):
Measure the app container's real document offset into a CSS custom property and use it in the height calcs; re-measure via ResizeObserver on the parent + window resize (banners render late, resize, or get dismissed):

const compute = () => {
    const top = Math.max( 0, Math.round( node.getBoundingClientRect().top + window.scrollY ) );
    node.style.setProperty( '--admin-top', `${ top }px` );
};
// heights: calc(100dvh - <own topbar> - var(--admin-top, 32px))

Reference implementation: surecookie src/admin/AdminApp.jsx (ticket 1507247 fix). Verified against a simulated 72px in_admin_header banner.

Proposal: fix once in force-ui, adopt everywhere

  1. Ship an AdminPageLayout (topbar slot + optional sidebar slot + content slot) in force-ui that implements one of the mechanisms internally - so no consumer ever writes viewport math again. Minimally: export a documented useWpAdminOffset() hook + layout recipe.
  2. Migrate surecookie, surerank, sureforms, suremails, surecontact, suredash to the shared component and delete the per-plugin calc(100dvh - 96px) grids.

Acceptance criteria

  • With a 72px banner injected via in_admin_header, every plugin's admin app shows all controls (Save, full sidebar) without zooming.
  • Dismissing the banner reclaims the space without reload.
  • Mobile admin bar (46px at <=782px) handled without a separate hard-coded variant.

Repro harness (drop in mu-plugins):

add_action( 'in_admin_header', function () {
    echo '<div style="height:72px;background:#fde68a;">Simulated third-party banner</div>';
} );

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions