@@ -181,7 +219,20 @@ class PageDemo extends LitElement {
#renderMainPanel() {
return html`
-
+
+ ${this.hasMainHeader ? html`
+
+
+
+
+
+
+
+
+
+ New Assignment
+ ` : nothing }
+
${this.#renderDemoMainControls()}
I'm in the default slot of the d2l-page component!
@@ -333,13 +384,29 @@ class PageDemo extends LitElement {
End of Content
-
+
+
+
+
+
+ Allow late submissions
+
+ Create
+ Cancel
+
+
`;
}
#renderSideNavPanel() {
return this.hasSideNavPanel ? html`
-
+
+ ${this.hasSideNavHeader ? html`
+
+
+
+ ` : nothing}
+ ${this.#renderDemoSideNavControls()}
I'm in the side-nav slot of the d2l-page component!
@@ -433,14 +500,19 @@ class PageDemo extends LitElement {
End of Content
-
+
` : nothing;
}
#renderSupportingPanel() {
return this.hasSupportingPanel ? html`
-
-
+
+ ${this.hasSupportingHeader ? html`
+
+
+
+ ` : nothing}
+ ${this.#renderDemoSupportingControls()}
I'm in the supporting slot of the d2l-page component!
@@ -489,7 +561,7 @@ class PageDemo extends LitElement {
End of Content
-
+
` : nothing;
}
}
diff --git a/components/page/page-main.js b/components/page/page-main.js
new file mode 100644
index 00000000000..341d3c423f5
--- /dev/null
+++ b/components/page/page-main.js
@@ -0,0 +1,24 @@
+import { css, html, LitElement } from 'lit';
+import { PagePanelMixin, pagePanelStyles } from './page-panel-mixin.js';
+
+/**
+ * Component to be placed in the main default slot of d2l-page, providing a panel with optional header
+ * @slot - The main content of the main page panel
+ * @slot header-start - Optional start content of the main page header
+ * @slot header-end - Optional end content of the main page header
+ */
+class PageMain extends PagePanelMixin(LitElement) {
+
+ static styles = [pagePanelStyles, css`
+ .panel-header {
+ top: var(--d2l-page-header-height, 0px);
+ }
+ `];
+
+ render() {
+ return this._renderPanel(html`
`);
+ }
+
+}
+
+customElements.define('d2l-page-main', PageMain);
diff --git a/components/page/page-panel-mixin.js b/components/page/page-panel-mixin.js
new file mode 100644
index 00000000000..c0a7cca5d15
--- /dev/null
+++ b/components/page/page-panel-mixin.js
@@ -0,0 +1,79 @@
+import '../colors/colors.js';
+import { css, html } from 'lit';
+import { classMap } from 'lit/directives/class-map.js';
+
+export const pagePanelStyles = css`
+ .panel-header:not([hidden]) {
+ align-items: center;
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+ }
+ .panel-header {
+ background-color: white;
+ border-bottom: 1px solid var(--d2l-color-mica);
+ height: 70px;
+ overflow: hidden;
+ padding: 0 30px;
+ position: sticky;
+ top: 0;
+ z-index: 14; /* To be over sticky content of our core components, but not over the header shadow */
+ }
+
+ .header-start,
+ .header-end {
+ display: flex;
+ gap: 6px;
+ }
+
+ .panel {
+ --d2l-page-panel-header-height: 0px;
+ padding: 30px;
+ }
+
+ .panel.header-sticky {
+ --d2l-page-panel-header-height: 70px;
+ }
+`;
+
+export const PagePanelMixin = superclass => class extends superclass {
+
+ static properties = {
+ _hasHeader: { state: true },
+ _slotVisibility: { state: true }
+ };
+
+ constructor() {
+ super();
+ this._hasHeader = false;
+ this._slotVisibility = {};
+ }
+
+ _handleSlotVisibilityChange(e) {
+ const key = e.target.name;
+ const nodes = e.target.assignedNodes();
+ this._slotVisibility = { ...this._slotVisibility, [key]: nodes.length !== 0 };
+
+ this._hasHeader = this._slotVisibility['header-start'] || this._slotVisibility['header-end'];
+ }
+
+ _renderPanel(content) {
+ const panelClasses = {
+ 'panel': true,
+ 'header-sticky': this._hasHeader
+ };
+ const header = html`
+ `;
+
+ return html`
+
+ ${header}
+
${content}
+
+ `;
+ }
+
+};
diff --git a/components/page/page-side-nav.js b/components/page/page-side-nav.js
new file mode 100644
index 00000000000..169745cacb4
--- /dev/null
+++ b/components/page/page-side-nav.js
@@ -0,0 +1,19 @@
+import { html, LitElement } from 'lit';
+import { PagePanelMixin, pagePanelStyles } from './page-panel-mixin.js';
+
+/**
+ * Component to be placed in the side-nav slot of d2l-page, providing a panel with optional header
+ * @slot - The main content of the side-nav page panel
+ * @slot header-start - Optional start content of the side-nav page header
+ * @slot header-end - Optional end content of the side-nav page header
+ */
+class PageSideNav extends PagePanelMixin(LitElement) {
+
+ static styles = [pagePanelStyles];
+
+ render() {
+ return this._renderPanel(html`
`);
+ }
+}
+
+customElements.define('d2l-page-side-nav', PageSideNav);
diff --git a/components/page/page-supporting.js b/components/page/page-supporting.js
new file mode 100644
index 00000000000..cb71f045119
--- /dev/null
+++ b/components/page/page-supporting.js
@@ -0,0 +1,19 @@
+import { html, LitElement } from 'lit';
+import { PagePanelMixin, pagePanelStyles } from './page-panel-mixin.js';
+
+/**
+ * Component to be placed in the supporting slot of d2l-page, providing a panel with optional header
+ * @slot - The main content of the supporting page panel
+ * @slot header-start - Optional start content of the supporting page header
+ * @slot header-end - Optional end content of the supporting page header
+ */
+class PageSupporting extends PagePanelMixin(LitElement) {
+
+ static styles = [pagePanelStyles];
+
+ render() {
+ return this._renderPanel(html`
`);
+ }
+}
+
+customElements.define('d2l-page-supporting', PageSupporting);
diff --git a/components/page/page.js b/components/page/page.js
index 1ad2ee4c90c..4b859523ac2 100644
--- a/components/page/page.js
+++ b/components/page/page.js
@@ -43,10 +43,14 @@ class Page extends LocalizeCoreElement(LitElement) {
--d2l-page-footer-max-width: 100%;
}
+ .header {
+ z-index: 15; /* To be over sticky content of our core components */
+ position: relative;
+ }
+
.page.header-sticky .header {
position: sticky;
top: 0;
- z-index: 15; /* To be over sticky content of our core components */
}
.content {
diff --git a/components/page/test/panels.axe.js b/components/page/test/panels.axe.js
new file mode 100644
index 00000000000..1a5f96d8bd4
--- /dev/null
+++ b/components/page/test/panels.axe.js
@@ -0,0 +1,74 @@
+import '../page-main.js';
+import '../page-side-nav.js';
+import '../page-supporting.js';
+import { expect, fixture, html } from '@brightspace-ui/testing';
+
+describe('panels', () => {
+
+ describe('page-main', () => {
+ it('no-header', async() => {
+ const elem = await fixture(html`
+
+ Content
+
+ `);
+ await expect(elem).to.be.accessible();
+ });
+
+ it('with-header', async() => {
+ const elem = await fixture(html`
+
+ Start
+ End
+ Content
+
+ `);
+ await expect(elem).to.be.accessible();
+ });
+ });
+
+ describe('page-side-nav', () => {
+ it('no-header', async() => {
+ const elem = await fixture(html`
+
+ Content
+
+ `);
+ await expect(elem).to.be.accessible();
+ });
+
+ it('with-header', async() => {
+ const elem = await fixture(html`
+
+ Start
+ End
+ Content
+
+ `);
+ await expect(elem).to.be.accessible();
+ });
+ });
+
+ describe('page-supporting', () => {
+ it('no-header', async() => {
+ const elem = await fixture(html`
+
+ Content
+
+ `);
+ await expect(elem).to.be.accessible();
+ });
+
+ it('with-header', async() => {
+ const elem = await fixture(html`
+
+ Start
+ End
+ Content
+
+ `);
+ await expect(elem).to.be.accessible();
+ });
+ });
+
+});
diff --git a/components/page/test/panels.test.js b/components/page/test/panels.test.js
new file mode 100644
index 00000000000..040c17e062f
--- /dev/null
+++ b/components/page/test/panels.test.js
@@ -0,0 +1,26 @@
+import '../page-main.js';
+import '../page-side-nav.js';
+import '../page-supporting.js';
+import { runConstructor } from '@brightspace-ui/testing';
+
+describe('panels', () => {
+
+ describe('page-main', () => {
+ it('should construct', () => {
+ runConstructor('d2l-page-main');
+ });
+ });
+
+ describe('page-side-nav', () => {
+ it('should construct', () => {
+ runConstructor('d2l-page-side-nav');
+ });
+ });
+
+ describe('page-supporting', () => {
+ it('should construct', () => {
+ runConstructor('d2l-page-supporting');
+ });
+ });
+
+});
From 4425eac0a2e65ed42e4352f513963ea0f946208b Mon Sep 17 00:00:00 2001
From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com>
Date: Wed, 6 May 2026 14:39:04 -0400
Subject: [PATCH 26/29] Save panel height properties for next PR
---
components/page/page-panel-mixin.js | 5 -----
1 file changed, 5 deletions(-)
diff --git a/components/page/page-panel-mixin.js b/components/page/page-panel-mixin.js
index c0a7cca5d15..69e9f3cad31 100644
--- a/components/page/page-panel-mixin.js
+++ b/components/page/page-panel-mixin.js
@@ -27,13 +27,8 @@ export const pagePanelStyles = css`
}
.panel {
- --d2l-page-panel-header-height: 0px;
padding: 30px;
}
-
- .panel.header-sticky {
- --d2l-page-panel-header-height: 70px;
- }
`;
export const PagePanelMixin = superclass => class extends superclass {
From e8d5fb4d28295b9db37200e164731192d7b1d62c Mon Sep 17 00:00:00 2001
From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com>
Date: Wed, 6 May 2026 14:44:24 -0400
Subject: [PATCH 27/29] Linting fixes
---
components/page/page-main.js | 2 +-
components/page/page.js | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/components/page/page-main.js b/components/page/page-main.js
index 341d3c423f5..ad82de676ea 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, 0px);
+ top: var(--d2l-page-header-height, 0);
}
`];
diff --git a/components/page/page.js b/components/page/page.js
index 1038ede979c..7c1aa97f0c0 100644
--- a/components/page/page.js
+++ b/components/page/page.js
@@ -45,8 +45,8 @@ class Page extends LocalizeCoreElement(LitElement) {
}
.header {
- z-index: 15; /* To be over sticky content of our core components */
position: relative;
+ z-index: 15; /* To be over sticky content of our core components */
}
.page.header-sticky .header {
From a35de3938851b8acaf7748724858084961c7ce93 Mon Sep 17 00:00:00 2001
From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com>
Date: Wed, 6 May 2026 15:44:57 -0400
Subject: [PATCH 28/29] No dom changes outside of render
---
components/page/demo/page-component.js | 12 +++++++++---
1 file changed, 9 insertions(+), 3 deletions(-)
diff --git a/components/page/demo/page-component.js b/components/page/demo/page-component.js
index 0d9c0966dac..c6c9d7e4428 100644
--- a/components/page/demo/page-component.js
+++ b/components/page/demo/page-component.js
@@ -47,7 +47,8 @@ class PageDemo extends LitElement {
hasSupportingPanel: { type: Boolean, attribute: 'has-supporting-panel' },
navType: { type: String, attribute: 'nav-type' },
widthType: { type: String, attribute: 'width-type' },
- _allowThreePanels: { state: true }
+ _allowThreePanels: { state: true },
+ _demoDialogOpened: { state: true }
};
static styles = [navStyles, selectStyles, tableStyles, css`
@@ -74,6 +75,7 @@ class PageDemo extends LitElement {
this.navType = 'full';
/** @type {'normal'|'wide'|'fullscreen'} */
this.widthType = 'normal';
+ this._demoDialogOpened = false;
}
render() {
@@ -95,8 +97,12 @@ class PageDemo extends LitElement {
}
}
+ #handleDialogClose() {
+ this._demoDialogOpened = false;
+ }
+
#handleDialogOpen() {
- this.shadowRoot.querySelector('#demo-dialog').opened = true;
+ this._demoDialogOpened = true;
}
#handleNavTypeChange(e) {
@@ -384,7 +390,7 @@ class PageDemo extends LitElement {
End of Content
-
+
From 16df33e85a5231346bec05c4595e5d1bdd27b7a9 Mon Sep 17 00:00:00 2001
From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com>
Date: Wed, 6 May 2026 15:46:48 -0400
Subject: [PATCH 29/29] Make function private
---
components/page/page-panel-mixin.js | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/components/page/page-panel-mixin.js b/components/page/page-panel-mixin.js
index 69e9f3cad31..5efa230b071 100644
--- a/components/page/page-panel-mixin.js
+++ b/components/page/page-panel-mixin.js
@@ -44,14 +44,6 @@ export const PagePanelMixin = superclass => class extends superclass {
this._slotVisibility = {};
}
- _handleSlotVisibilityChange(e) {
- const key = e.target.name;
- const nodes = e.target.assignedNodes();
- this._slotVisibility = { ...this._slotVisibility, [key]: nodes.length !== 0 };
-
- this._hasHeader = this._slotVisibility['header-start'] || this._slotVisibility['header-end'];
- }
-
_renderPanel(content) {
const panelClasses = {
'panel': true,
@@ -59,8 +51,8 @@ export const PagePanelMixin = superclass => class extends superclass {
};
const header = html`
`;
return html`
@@ -71,4 +63,12 @@ export const PagePanelMixin = superclass => class extends superclass {
`;
}
+ #handleSlotVisibilityChange(e) {
+ const key = e.target.name;
+ const nodes = e.target.assignedNodes();
+ this._slotVisibility = { ...this._slotVisibility, [key]: nodes.length !== 0 };
+
+ this._hasHeader = this._slotVisibility['header-start'] || this._slotVisibility['header-end'];
+ }
+
};