diff --git a/components/page/demo/page-component.js b/components/page/demo/page-component.js
index 0fd3c8907a9..c6c9d7e4428 100644
--- a/components/page/demo/page-component.js
+++ b/components/page/demo/page-component.js
@@ -1,8 +1,14 @@
+import '../../button/button-icon.js';
import '../../button/button-subtle.js';
+import '../../button/button.js';
import '../../collapsible-panel/collapsible-panel.js';
import '../../collapsible-panel/collapsible-panel-group.js';
import '../../collapsible-panel/collapsible-panel-summary-item.js';
import '../../demo/demo-page-settings.js';
+import '../../dialog/dialog.js';
+import '../../filter/filter.js';
+import '../../filter/filter-dimension-set.js';
+import '../../filter/filter-dimension-set-value.js';
import '../../icons/icon.js';
import '../../inputs/input-checkbox.js';
import '../../inputs/input-date.js';
@@ -14,8 +20,13 @@ import '../../list/list-item.js';
import '../../list/list-item-content.js';
import '../../list/list-item-nav.js';
import '../../selection/selection-action.js';
+import '../../switch/switch-visibility.js';
+import '../../switch/switch.js';
import '../../table/table-controls.js';
import '../page.js';
+import '../page-main.js';
+import '../page-side-nav.js';
+import '../page-supporting.js';
import { css, html, LitElement, nothing } from 'lit';
import { navStyles } from './temp-nav-styles.js';
import { selectStyles } from '../../inputs/input-select-styles.js';
@@ -29,11 +40,15 @@ class PageDemo extends LitElement {
static properties = {
demoMode: { type: Boolean, attribute: 'demo-mode' },
hasFooter: { type: Boolean, attribute: 'has-footer' },
+ hasMainHeader: { type: Boolean, attribute: 'has-main-header' },
+ hasSideNavHeader: { type: Boolean, attribute: 'has-side-nav-header' },
hasSideNavPanel: { type: Boolean, attribute: 'has-side-nav-panel' },
+ hasSupportingHeader: { type: Boolean, attribute: 'has-supporting-header' },
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`
@@ -42,6 +57,9 @@ class PageDemo extends LitElement {
flex-wrap: wrap;
gap: 0.75rem;
}
+ d2l-demo-page-settings {
+ margin-block: 0.6rem;
+ }
`];
constructor() {
@@ -49,11 +67,15 @@ class PageDemo extends LitElement {
this._allowThreePanels = false; // Temp for dev/testing
this.demoMode = false;
this.hasFooter = false;
+ this.hasMainHeader = false;
+ this.hasSideNavHeader = false;
this.hasSideNavPanel = false;
+ this.hasSupportingHeader = false;
this.hasSupportingPanel = false;
this.navType = 'full';
/** @type {'normal'|'wide'|'fullscreen'} */
this.widthType = 'normal';
+ this._demoDialogOpened = false;
}
render() {
@@ -75,6 +97,14 @@ class PageDemo extends LitElement {
}
}
+ #handleDialogClose() {
+ this._demoDialogOpened = false;
+ }
+
+ #handleDialogOpen() {
+ this._demoDialogOpened = true;
+ }
+
#handleNavTypeChange(e) {
this.navType = e.target.on ? 'immersive' : 'full';
}
@@ -107,6 +137,7 @@ class PageDemo extends LitElement {
+
@@ -114,6 +145,19 @@ class PageDemo extends LitElement {
` : nothing;
}
+ #renderDemoSideNavControls() {
+ return this.demoMode ? html`
+
+ ` : nothing;
+ }
+
+ #renderDemoSupportingControls() {
+ return this.demoMode ? html`
+
+
+ ` : nothing;
+ }
+
#renderFooter() {
return this.hasFooter ? html`
@@ -181,7 +225,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 +390,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 +506,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 +567,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..ad82de676ea
--- /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, 0);
+ }
+ `];
+
+ 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..5efa230b071
--- /dev/null
+++ b/components/page/page-panel-mixin.js
@@ -0,0 +1,74 @@
+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 {
+ padding: 30px;
+ }
+`;
+
+export const PagePanelMixin = superclass => class extends superclass {
+
+ static properties = {
+ _hasHeader: { state: true },
+ _slotVisibility: { state: true }
+ };
+
+ constructor() {
+ super();
+ this._hasHeader = false;
+ this._slotVisibility = {};
+ }
+
+ _renderPanel(content) {
+ const panelClasses = {
+ 'panel': true,
+ 'header-sticky': this._hasHeader
+ };
+ const header = html`
+ `;
+
+ return html`
+
+ ${header}
+
${content}
+
+ `;
+ }
+
+ #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'];
+ }
+
+};
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 9157c2a78af..7c1aa97f0c0 100644
--- a/components/page/page.js
+++ b/components/page/page.js
@@ -44,10 +44,14 @@ class Page extends LocalizeCoreElement(LitElement) {
--d2l-page-footer-max-width: 100%;
}
+ .header {
+ position: relative;
+ z-index: 15; /* To be over sticky content of our core components */
+ }
+
.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');
+ });
+ });
+
+});