From 5d88ce6dad7d6cab3afc7d65acb07ebb14f9f6b9 Mon Sep 17 00:00:00 2001
From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com>
Date: Fri, 17 Jul 2026 17:45:39 -0400
Subject: [PATCH 1/2] GAUD-10314 - Add legacy browser mode that creates scroll
containers for browsers that don't support popover
---
components/page/demo/page-component.js | 15 +++++-
components/page/page.js | 64 +++++++++++++++++++++++---
2 files changed, 71 insertions(+), 8 deletions(-)
diff --git a/components/page/demo/page-component.js b/components/page/demo/page-component.js
index 19527902733..a5a628093d8 100644
--- a/components/page/demo/page-component.js
+++ b/components/page/demo/page-component.js
@@ -27,13 +27,13 @@ import '../../selection/selection-action.js';
import '../../switch/switch-visibility.js';
import '../../switch/switch.js';
import '../../table/table-controls.js';
-import '../page.js';
import '../page-footer.js';
import '../page-main.js';
import '../page-side-nav.js';
import '../page-supporting.js';
import './page-header-full.js';
import { css, html, LitElement, nothing } from 'lit';
+import { _forceLegacyBrowserMode } from '../page.js';
import { ifDefined } from 'lit/directives/if-defined.js';
import { inputLabelStyles } from '../../inputs/input-label-styles.js';
import { pageHeaderImmersiveActionsDemo } from '../test/page-header-immersive-fixtures.js';
@@ -52,6 +52,7 @@ class PageDemo extends LitElement {
header: { type: String, attribute: 'header' },
immersiveHeaderTitleType: { type: String, attribute: 'immersive-header-title-type' },
layout: { type: String, attribute: 'layout' },
+ legacyBrowserMode: { type: String, attribute: 'legacy-browser-mode' },
widthType: { type: String, attribute: 'width-type' },
_mainDialogOpened: { state: true },
_mainToastOpened: { state: true },
@@ -86,6 +87,8 @@ class PageDemo extends LitElement {
this.header = urlParams.get('header') || 'full';
this.immersiveHeaderTitleType = urlParams.get('immersiveHeaderTitleType') || 'title-subtitle';
this.layout = urlParams.get('layout') || 'main-only';
+ this.legacyBrowserMode = urlParams.has('legacyBrowserMode');
+ if (this.legacyBrowserMode) _forceLegacyBrowserMode(true);
this.widthType = urlParams.get('widthType') || 'normal';
this._mainDialogOpened = false;
this._mainToastOpened = false;
@@ -124,6 +127,13 @@ class PageDemo extends LitElement {
this.#updateUrlParam('layout', this.layout);
}
+ #handleLegacyBrowserModeChange(e) {
+ this.legacyBrowserMode = e.target.on;
+ _forceLegacyBrowserMode(this.legacyBrowserMode);
+ this.#updateUrlParamBool('legacyBrowserMode', this.legacyBrowserMode);
+ this.shadowRoot.querySelector('d2l-page')?.requestUpdate();
+ }
+
#handleMainDialogClose() {
this._mainDialogOpened = false;
this._mainToastOpened = true;
@@ -251,6 +261,9 @@ class PageDemo extends LitElement {
${this.layout === 'side-nav' ? html`` : nothing}
${this.layout === 'supporting' ? html`` : nothing}
+
+
+
${this.#renderDemoHeaderControls()}
diff --git a/components/page/page.js b/components/page/page.js
index 6791db83bb1..35c2ad43587 100644
--- a/components/page/page.js
+++ b/components/page/page.js
@@ -11,6 +11,22 @@ const DRAWER_MIN_HEIGHT = 200; // TO DO: Confirm
const MAIN_MIN_WIDTH = 600; // TO DO: Confirm
const PANEL_MIN_WIDTH = 320;
+/*
+* Legacy Browser Mode can be deleted once we have hard-blocked all browsers that do not support the Popover api,
+* and dialog will be able to escape stacking contexts. It creates a "scrolling container" layout for pages with panels,
+* to avoid the stacking context created by position: sticky.
+*/
+let forceLegacyBrowserMode = false;
+const isPopoverSupported = ('popover' in HTMLElement.prototype);
+function isLegacyBrowserMode() {
+ return forceLegacyBrowserMode || !isPopoverSupported;
+}
+// Back door for the demo page and unit tests to force legacy browser mode, bypassing popover feature detection.
+// DO NOT use in production.
+export function _forceLegacyBrowserMode(on) {
+ forceLegacyBrowserMode = on;
+}
+
class PanelStateController {
constructor(host, panelConfigs) {
this.#host = host;
@@ -117,7 +133,7 @@ class Page extends ProviderMixin(LocalizeCoreElement(LitElement)) {
max-width: var(--d2l-page-content-max-width, 100%);
padding-bottom: var(--d2l-page-footer-height, 0); /* Reserve space for fixed footer */
}
- .content.has-panels {
+ .page.has-panels .content {
min-height: calc(100vh - var(--d2l-page-header-height-measured, 0px));
}
@@ -160,6 +176,42 @@ class Page extends ProviderMixin(LocalizeCoreElement(LitElement)) {
margin-inline: var(--d2l-page-margin-inline, 0);
max-width: var(--d2l-page-footer-max-width, 100%);
}
+
+ /* Legacy Browser Mode */
+ .page.legacy-browser-mode.has-panels {
+ display: flex;
+ flex-direction: column;
+ height: 100vh;
+ }
+ .page.legacy-browser-mode.has-panels .header,
+ .page.legacy-browser-mode.header-sticky.has-panels .header {
+ flex: 0 0 auto;
+ position: static;
+ }
+ .page.legacy-browser-mode.has-panels .content {
+ flex: 1 1 auto;
+ min-height: 0;
+ overflow: hidden;
+ padding-bottom: 0;
+ width: 100%;
+ }
+ .page.legacy-browser-mode.has-panels main {
+ min-height: 0;
+ overflow: auto;
+ }
+ .page.legacy-browser-mode.has-panels .side-nav-panel,
+ .page.legacy-browser-mode.has-panels .supporting-panel,
+ .page.legacy-browser-mode.has-panels .divider {
+ max-height: none;
+ min-height: 0;
+ position: static;
+ }
+ .page.legacy-browser-mode.has-panels .footer {
+ flex: 0 0 auto;
+ }
+ .page.legacy-browser-mode.has-panels .fixed-footer {
+ position: static;
+ }
`;
constructor() {
@@ -182,7 +234,7 @@ class Page extends ProviderMixin(LocalizeCoreElement(LitElement)) {
this._headerHeight = entry.target.offsetHeight;
this.style.setProperty('--d2l-page-header-height-measured', `${this._headerHeight}px`);
- const height = this._headerIsSticky ? this._headerHeight : 0;
+ const height = this._headerIsSticky && !isLegacyBrowserMode() ? this._headerHeight : 0;
this.style.setProperty('--d2l-page-header-height', `${height}px`);
this._panelState.updateMaxSize('supporting-mobile', this.#getMaxDrawerHeight());
} else if (entry.target.classList.contains('footer')) {
@@ -229,17 +281,15 @@ class Page extends ProviderMixin(LocalizeCoreElement(LitElement)) {
render() {
const pageClasses = {
'page': true,
- 'header-sticky': this._headerIsSticky
- };
- const contentClasses = {
- 'content': true,
+ 'header-sticky': this._headerIsSticky,
+ 'legacy-browser-mode': isLegacyBrowserMode(),
'has-panels': this._slotVisibility['side-nav'] || this._slotVisibility['supporting']
};
return html`
${this.#renderHeader()}
-
+
${this.#renderSideNavPanel()}
${this.#renderSupportingPanel()}
From 9f76e7c4f6ee82aaa3661a5e9ff161981424dc25 Mon Sep 17 00:00:00 2001
From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com>
Date: Fri, 17 Jul 2026 18:55:42 -0400
Subject: [PATCH 2/2] Fix main header sticky position for immersive nav
---
components/page/page-main.js | 2 +-
components/page/page.js | 4 +++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/components/page/page-main.js b/components/page/page-main.js
index ad82de676ea..614a5fcf535 100644
--- a/components/page/page-main.js
+++ b/components/page/page-main.js
@@ -11,7 +11,7 @@ class PageMain extends PagePanelMixin(LitElement) {
static styles = [pagePanelStyles, css`
.panel-header {
- top: var(--d2l-page-header-height, 0);
+ top: var(--d2l-page-main-sticky-top, 0);
}
`];
diff --git a/components/page/page.js b/components/page/page.js
index 35c2ad43587..4b9d13708c2 100644
--- a/components/page/page.js
+++ b/components/page/page.js
@@ -138,6 +138,7 @@ class Page extends ProviderMixin(LocalizeCoreElement(LitElement)) {
}
main {
+ --d2l-page-main-sticky-top: var(--d2l-page-header-height, 0);
flex: 1;
min-width: min(${MAIN_MIN_WIDTH}px, 100%);
}
@@ -196,6 +197,7 @@ class Page extends ProviderMixin(LocalizeCoreElement(LitElement)) {
width: 100%;
}
.page.legacy-browser-mode.has-panels main {
+ --d2l-page-main-sticky-top: 0; /* Header is static and content scrolls within main, so panel headers stick to the top of the scroll container */
min-height: 0;
overflow: auto;
}
@@ -234,7 +236,7 @@ class Page extends ProviderMixin(LocalizeCoreElement(LitElement)) {
this._headerHeight = entry.target.offsetHeight;
this.style.setProperty('--d2l-page-header-height-measured', `${this._headerHeight}px`);
- const height = this._headerIsSticky && !isLegacyBrowserMode() ? this._headerHeight : 0;
+ const height = this._headerIsSticky ? this._headerHeight : 0;
this.style.setProperty('--d2l-page-header-height', `${height}px`);
this._panelState.updateMaxSize('supporting-mobile', this.#getMaxDrawerHeight());
} else if (entry.target.classList.contains('footer')) {