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')) {