From 7f3dfc8738adfa8cf230a86ced750abbc023dd3c Mon Sep 17 00:00:00 2001 From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com> Date: Fri, 1 May 2026 17:58:16 -0400 Subject: [PATCH 1/6] GAUD-9165 - Implement the d2l-page skeleton --- components/page/README.md | 5 + components/page/demo/page-component.js | 191 ++++++++++++++++++++ components/page/demo/page.html | 15 ++ components/page/demo/temp-nav-styles.js | 217 +++++++++++++++++++++++ components/page/page.js | 223 ++++++++++++++++++++++++ components/page/test/page.axe.js | 40 +++++ components/page/test/page.test.js | 10 ++ index.html | 1 + lang/ar.js | 2 + lang/ca.js | 2 + lang/cy.js | 2 + lang/da.js | 2 + lang/de.js | 2 + lang/en-gb.js | 2 + lang/en.js | 2 + lang/es-es.js | 2 + lang/es.js | 2 + lang/fr-fr.js | 2 + lang/fr.js | 2 + lang/haw.js | 2 + lang/hi.js | 2 + lang/ja.js | 2 + lang/ko.js | 2 + lang/mi.js | 2 + lang/nl.js | 2 + lang/pt.js | 2 + lang/sv.js | 2 + lang/th.js | 2 + lang/tr.js | 2 + lang/vi.js | 2 + lang/zh-cn.js | 2 + lang/zh-tw.js | 2 + 32 files changed, 750 insertions(+) create mode 100644 components/page/README.md create mode 100644 components/page/demo/page-component.js create mode 100644 components/page/demo/page.html create mode 100644 components/page/demo/temp-nav-styles.js create mode 100644 components/page/page.js create mode 100644 components/page/test/page.axe.js create mode 100644 components/page/test/page.test.js diff --git a/components/page/README.md b/components/page/README.md new file mode 100644 index 00000000000..11d630c5402 --- /dev/null +++ b/components/page/README.md @@ -0,0 +1,5 @@ +# Page + +The `d2l-page` and page layout components are in progress. + +Please reach out before using them! diff --git a/components/page/demo/page-component.js b/components/page/demo/page-component.js new file mode 100644 index 00000000000..5aa62dc03e5 --- /dev/null +++ b/components/page/demo/page-component.js @@ -0,0 +1,191 @@ +import '../../collapsible-panel/collapsible-panel.js'; +import '../page.js'; +import { css, html, LitElement, nothing } from 'lit'; +import { navStyles } from './temp-nav-styles.js'; +import { selectStyles } from '../../inputs/input-select-styles.js'; + +/** + * Component for d2l-page demos and tests + */ +class PageDemo extends LitElement { + + static properties = { + demoMode: { type: Boolean, attribute: 'demo-mode' }, + hasFooter: { type: Boolean, attribute: 'has-footer' }, + hasSideNavPanel: { type: Boolean, attribute: 'has-side-nav-panel' }, + hasSupportingPanel: { type: Boolean, attribute: 'has-supporting-panel' }, + navType: { type: String, attribute: 'nav-type' }, + widthType: { type: String, attribute: 'width-type' }, + _allowThreePanels: { state: true } + }; + + static styles = [navStyles, selectStyles, css` + .demo-controls { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + } + `]; + + constructor() { + super(); + this._allowThreePanels = false; // Temp for dev/testing + this.demoMode = false; + this.hasFooter = false; + this.hasSideNavPanel = false; + this.hasSupportingPanel = false; + this.navType = 'full'; + /** @type {'normal'|'wide'|'fullscreen'} */ + this.widthType = 'normal'; + } + + render() { + return html` + + ${this.navType === 'full' ? this.#renderFullNav() : this.#renderImmersiveNav()} + ${this.#renderSideNavPanel()} + ${this.#renderMainPanel()} + ${this.#renderSupportingPanel()} + ${this.#renderFooter()} + + `; + } + + #handleAllowThreePanelsChange(e) { + this._allowThreePanels = e.target.on; + if (!this._allowThreePanels && this.hasSideNavPanel && this.hasSupportingPanel) { + this.shadowRoot.querySelector('#switch-supporting-panel').on = false; + this.hasSupportingPanel = false; + } + } + + #handleNavTypeChange(e) { + this.navType = e.target.on ? 'immersive' : 'full'; + } + + #handleVisibilityChange(e) { + const key = e.target.dataset.key; + this[key] = e.target.on; + + if (this._allowThreePanels) return; + if (e.target.on && key === 'hasSideNavPanel' && this.hasSupportingPanel) { + this.shadowRoot.querySelector('#switch-supporting-panel').on = false; + this.hasSupportingPanel = false; + } else if (e.target.on && key === 'hasSupportingPanel' && this.hasSideNavPanel) { + this.shadowRoot.querySelector('#switch-side-nav-panel').on = false; + this.hasSideNavPanel = false; + } + } + + #handleWidthTypeChange(e) { + this.widthType = e.target.value; + } + + #renderFooter() { + return this.hasFooter ? html` +
+ I'm in the footer slot of the d2l-page component! +
+ ` : nothing; + } + + #renderFullNav() { + // Update with navigation components once available + return html` +
+ +
+
+ +
+ +
+
+
+ + + +
+
+ + +
+ `; + } + + #renderImmersiveNav() { + // Update with navigation components once available + return html` +
+ +
+ +
+ Assignment 1 - Introduction to Economics +
+
+ + +
+
+ +
+ `; + } + + #renderMainPanel() { + return html` +
+ ${this.demoMode ? html` + +
+ + + + + + +
+
+ ` : nothing} +

I'm in the default slot of the d2l-page component!

+
+
End of Content
+ `; + } + + #renderSideNavPanel() { + return this.hasSideNavPanel ? html` +
+ I'm in the side-nav slot of the d2l-page component! +
+ ` : nothing; + } + + #renderSupportingPanel() { + return this.hasSupportingPanel ? html` +
+ I'm in the supporting slot of the d2l-page component! +
+ ` : nothing; + } +} + +customElements.define('d2l-page-demo', PageDemo); diff --git a/components/page/demo/page.html b/components/page/demo/page.html new file mode 100644 index 00000000000..f31638807b9 --- /dev/null +++ b/components/page/demo/page.html @@ -0,0 +1,15 @@ + + + + + + + + + + + + diff --git a/components/page/demo/temp-nav-styles.js b/components/page/demo/temp-nav-styles.js new file mode 100644 index 00000000000..98d89148ef5 --- /dev/null +++ b/components/page/demo/temp-nav-styles.js @@ -0,0 +1,217 @@ +/** + * Temporary navigation demo styles + * These can be removed once we have official navigation components to use + */ + +import { css } from 'lit'; + +export const navStyles = css` + + /* Shared Styles */ + .nav-shadow { + background-color: rgba(0, 0, 0, 0.02); + bottom: -4px; + display: block; + height: 4px; + pointer-events: none; + position: absolute; + width: 100%; + z-index: 1; + } + .nav-band { + background: linear-gradient(180deg, var(--d2l-color-celestine) 1.5rem, #ffffff 0%); + min-height: 4px; + } + .nav-icon-btn { + align-items: center; + background: none; + border: none; + border-radius: 4px; + color: var(--d2l-color-ferrite); + cursor: pointer; + display: inline-flex; + font-size: 0.8rem; + gap: 4px; + justify-content: center; + min-height: 42px; + min-width: 42px; + padding: 6px; + } + .nav-icon-btn:hover, + .nav-icon-btn:focus-visible { + background-color: var(--d2l-color-gypsum); + color: var(--d2l-color-celestine); + } + + /* Full Nav Styles */ + .full-nav-wrapper { + position: relative; + } + .full-nav-header { + align-items: center; + display: flex; + height: 90px; + margin-inline: var(--d2l-page-margin-inline); + max-width: var(--d2l-page-header-max-width); + padding: 0 30px; + } + @media (max-width: 767px) { + .full-nav-header { + height: 72px; + } + } + @media (max-width: 615px) { + .full-nav-header { + padding: 0 15px; + } + } + .full-nav-header-left { + align-items: center; + display: flex; + flex: 0 1 auto; + gap: 12px; + height: 100%; + } + .full-nav-header-spacer { + flex: 1 1 auto; + min-width: 30px; + } + @media (max-width: 615px) { + .full-nav-header-spacer { + min-width: 15px; + } + } + .full-nav-header-right { + align-items: center; + display: flex; + flex: 0 0 auto; + gap: 6px; + height: 100%; + } + .full-nav-logo { + background-color: var(--d2l-color-celestine); + border-radius: 4px; + color: white; + font-size: 0.8rem; + font-weight: 700; + padding: 8px 14px; + } + .full-nav-separator { + background-color: var(--d2l-color-mica); + height: 26px; + margin: 0 6px; + width: 1px; + } + .full-nav-footer { + border-bottom: 1px solid rgba(124, 134, 149, 0.18); + border-top: 1px solid rgba(124, 134, 149, 0.18); + } + .full-nav-footer-inner { + align-items: center; + display: flex; + gap: 4px; + margin-inline: var(--d2l-page-margin-inline); + max-width: var(--d2l-page-header-max-width); + padding: 0 30px; + } + @media (max-width: 615px) { + .full-nav-footer-inner { + padding: 0 15px; + } + } + .full-nav-footer-link { + border-bottom: 4px solid transparent; + color: var(--d2l-color-ferrite); + display: inline-block; + font-size: 0.7rem; + padding: 8px 12px; + text-decoration: none; + } + .full-nav-footer-link:hover, + .full-nav-footer-link:focus-visible { + border-bottom-color: var(--d2l-color-celestine); + color: var(--d2l-color-celestine); + } + + /* Immersive Nav Styles */ + .immersive-wrapper { + background-color: white; + border-bottom: 1px solid var(--d2l-color-mica); + position: relative; + } + .immersive-container { + align-items: center; + display: flex; + height: 3.1rem; + justify-content: space-between; + margin-inline: var(--d2l-page-margin-inline); + max-width: var(--d2l-page-header-max-width); + overflow: hidden; + padding: 0 30px; + } + @media (max-width: 929px) { + .immersive-container { + padding: 0 24px; + } + } + @media (max-width: 767px) { + .immersive-container { + padding: 0 18px; + } + } + @media (max-width: 615px) { + .immersive-container { + height: 2.8rem; + } + } + .immersive-left { + align-items: center; + color: var(--d2l-color-tungsten); + display: flex; + flex: 0 0 auto; + font-size: 0.8rem; + letter-spacing: 0.2px; + } + .immersive-back-link { + align-items: center; + color: var(--d2l-color-tungsten); + display: inline-flex; + gap: 4px; + text-decoration: none; + } + .immersive-back-link:hover, + .immersive-back-link:focus-visible { + color: var(--d2l-color-celestine); + } + .immersive-back-icon { + font-size: 1rem; + } + .immersive-middle { + border-inline-end: 1px solid var(--d2l-color-gypsum); + border-inline-start: 1px solid var(--d2l-color-gypsum); + flex: 0 1 auto; + font-size: 0.8rem; + margin: 0 24px; + min-width: 0; + overflow: hidden; + padding: 0 24px; + text-overflow: ellipsis; + white-space: nowrap; + width: 100%; + } + @media (max-width: 615px) { + .immersive-middle { + margin: 0 18px; + padding: 0 18px; + } + } + .immersive-right { + align-items: center; + display: flex; + flex: 0 0 auto; + gap: 8px; + } + .immersive-right .nav-icon-btn { + font-size: 0.7rem; + } +`; diff --git a/components/page/page.js b/components/page/page.js new file mode 100644 index 00000000000..8e73b05a63e --- /dev/null +++ b/components/page/page.js @@ -0,0 +1,223 @@ +import '../button/floating-buttons.js'; +import { css, html, LitElement, nothing } from 'lit'; +import { classMap } from 'lit/directives/class-map.js'; +import { LocalizeCoreElement } from '../../helpers/localize-core-element.js'; + +/** + * Page template with header, optional footer and optional navigation panel or supporting panel + * @slot - The main content of the page (expecting d2l-page-main) + * @slot header - The header content of the page (expecting d2l-page-header-*) + * @slot side-nav - The side navigation content of the page (expecting d2l-page-side-nav) + * @slot supporting - The supporting content of the page (expecting d2l-page-supporting) + * @slot footer - The footer content of the page (expecting d2l-page-footer) + */ +class Page extends LocalizeCoreElement(LitElement) { + + static properties = { + /** + * Width type of the page and its underlying pieces + * @type {'normal'|'wide'|'fullscreen'} + */ + widthType: { type: String, attribute: 'width-type' }, + _headerIsSticky: { state: true }, + _slotVisibility: { state: true } + }; + + static styles = css` + :host { + --d2l-page-header-max-width: 1230px; + --d2l-page-content-max-width: 1230px; + --d2l-page-footer-max-width: 1230px; + --d2l-page-margin-inline: auto; + } + + :host([width-type="wide"]) { + --d2l-page-header-max-width: 1440px; + --d2l-page-content-max-width: 1440px; + --d2l-page-footer-max-width: 1440px; + } + + :host([width-type="fullscreen"]) { + --d2l-page-header-max-width: 100%; + --d2l-page-content-max-width: 100%; + --d2l-page-footer-max-width: 100%; + } + + .header { + background-color: white; + z-index: 2; /* To be over divider and main contents */ + } + .page.header-sticky .header { + position: sticky; + top: 0; + } + + .content { + display: flex; + margin-inline: var(--d2l-page-margin-inline, 0); + max-width: var(--d2l-page-content-max-width, 100%); + padding-bottom: var(--d2l-page-footer-height, 0px); /* Reserve space for fixed footer */ + } + + main { + flex: 1; + min-width: 400px; /* TBD */ + } + + .side-nav-panel, + .supporting-panel { + overflow-y: auto; + position: sticky; + top: 0; + height: calc(100vh - var(--d2l-page-footer-height, 0px)); + } + .page.header-sticky .side-nav-panel, + .page.header-sticky .supporting-panel { + top: var(--d2l-page-header-height, 0px); + height: calc(100vh - var(--d2l-page-header-height, 0px) - var(--d2l-page-footer-height, 0px)); + } + + .divider { + background-color: var(--d2l-color-gypsum); + flex: none; + width: 4px; + z-index: 1; + } + + .footer:not([hidden]), + .floating-buttons-container { + display: inline; + } + .fixed-footer { + background-color: white; + box-shadow: 0 -2px 4px rgba(32, 33, 34, 0.2); /* ferrite */ + inset: auto 0 0; + padding: 0.75rem 0; + position: fixed; + z-index: 1; /* To be over divider and main contents */ + } + .floating-footer { + padding-block-end: 0.75rem; + } + .footer-contents { + margin-inline: var(--d2l-page-margin-inline, 0); + max-width: var(--d2l-page-footer-max-width, 100%); + } + `; + + constructor() { + super(); + + this.widthType = 'normal'; + this._headerIsSticky = false; + this._slotVisibility = {}; + this.#resizeObserver = new ResizeObserver(entries => { + for (const entry of entries) { + if (entry.target.classList.contains('header')) { + const height = entry.target.offsetHeight; + this.style.setProperty('--d2l-page-header-height', `${height}px`); + } else if (entry.target.classList.contains('footer')) { + const height = entry.target.classList.contains('fixed-footer') ? entry.target.offsetHeight : 0; + this.style.setProperty('--d2l-page-footer-height', `${height}px`); + } + } + }); + } + + connectedCallback() { + super.connectedCallback(); + } + + disconnectedCallback() { + super.disconnectedCallback(); + this.#resizeObserver.disconnect(); + } + + firstUpdated() { + const header = this.shadowRoot.querySelector('.header'); + const footer = this.shadowRoot.querySelector('.footer'); + if (header) this.#resizeObserver.observe(header); + if (footer) this.#resizeObserver.observe(footer); + } + + render() { + const pageClasses = { + 'page': true, + 'header-sticky': this._headerIsSticky + }; + + const header = html` +
+ +
`; + + const mainContent = html` +
+ +
`; + + const sideNavPanel = html` + + ${this._slotVisibility['side-nav'] ? html`
` : nothing} + `; + + const supportingPanel = html` + ${this._slotVisibility['supporting'] ? html`
` : nothing} + `; + + const fixedFooter = this._slotVisibility['side-nav'] || this._slotVisibility['supporting']; + const footerContainerClasses = { 'footer': true, 'fixed-footer': fixedFooter }; + const footerContents = html``; + const footer = html` +
+ ${fixedFooter ? footerContents : this.#renderFloatingButtons(footerContents)} +
`; + + return html` +
+ ${header} +
+ ${sideNavPanel} + ${mainContent} + ${supportingPanel} +
+ ${footer} +
+ `; + } + + #resizeObserver; + + #handleHeaderSlotChange(e) { + const nodes = e.target.assignedNodes(); + //this._headerIsSticky = nodes.some(node => node.tagName.toLowerCase() === 'd2l-page-header-immersive'); + this._headerIsSticky = nodes.some(node => node.id === 'immersive-nav'); // temp until the official component exists + } + + #handleSlotVisibilityChange(e) { + const key = e.target.name; + const nodes = e.target.assignedNodes(); + this._slotVisibility = { ...this._slotVisibility, [key]: nodes.length !== 0 }; + this.requestUpdate(); + } + + #renderFloatingButtons(footerContents) { + // Floating buttons needs to be wrapped as it spawns a sibling element that should be cleaned up as one by Lit + return html` +
+ + + +
+ `; + } + +} + +customElements.define('d2l-page', Page); diff --git a/components/page/test/page.axe.js b/components/page/test/page.axe.js new file mode 100644 index 00000000000..0cb23386687 --- /dev/null +++ b/components/page/test/page.axe.js @@ -0,0 +1,40 @@ +import '../page.js'; +import { expect, fixture, html } from '@brightspace-ui/testing'; + +describe('page', () => { + + it('single panel', async() => { + const elem = await fixture(html` + +
Header
+
Content
+
Footer
+
+ `); + await expect(elem).to.be.accessible(); + }); + + it('with side-nav panel', async() => { + const elem = await fixture(html` + +
Header
+
Content
+
Side Nav
+
Footer
+
+ `); + await expect(elem).to.be.accessible(); + }); + + it('with supporting panel', async() => { + const elem = await fixture(html` + +
Header
+
Content
+
Supporting
+
Footer
+
+ `); + await expect(elem).to.be.accessible(); + }); +}); diff --git a/components/page/test/page.test.js b/components/page/test/page.test.js new file mode 100644 index 00000000000..2b8bce6f0e7 --- /dev/null +++ b/components/page/test/page.test.js @@ -0,0 +1,10 @@ +import '../page.js'; +import { runConstructor } from '@brightspace-ui/testing'; + +describe('page', () => { + + it('should construct', () => { + runConstructor('d2l-page'); + }); + +}); diff --git a/index.html b/index.html index fdc60242b71..517691d84f9 100644 --- a/index.html +++ b/index.html @@ -153,6 +153,7 @@

Components

  • d2l-more-less
  • d2l-object-property-list
  • d2l-offscreen
  • +
  • d2l-page
  • d2l-progress
  • d2l-overflow-group
  • d2l-pager-load-more
  • diff --git a/lang/ar.js b/lang/ar.js index e76318e0c36..fe4a4c0d3a6 100644 --- a/lang/ar.js +++ b/lang/ar.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "المزيد", "components.object-property-list.item-placeholder-text": "عنصر نائب", "components.overflow-group.moreActions": "مزيد من الإجراءات", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} مادة واحد} diff --git a/lang/ca.js b/lang/ca.js index 569d4558a69..7b465df428a 100644 --- a/lang/ca.js +++ b/lang/ca.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "more", "components.object-property-list.item-placeholder-text": "Placeholder Item", "components.overflow-group.moreActions": "More Actions", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} item} diff --git a/lang/cy.js b/lang/cy.js index 4a2343d3c01..80f1dedb897 100644 --- a/lang/cy.js +++ b/lang/cy.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "mwy", "components.object-property-list.item-placeholder-text": "Eitem Dalfan", "components.overflow-group.moreActions": "Rhagor o Gamau Gweithredu", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} eitem} diff --git a/lang/da.js b/lang/da.js index 5d1b1c5e6df..37c60f9326d 100644 --- a/lang/da.js +++ b/lang/da.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "flere", "components.object-property-list.item-placeholder-text": "Pladsholder-element", "components.overflow-group.moreActions": "Flere handlinger", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} element} diff --git a/lang/de.js b/lang/de.js index 89d29dc533b..80e362ab3a0 100644 --- a/lang/de.js +++ b/lang/de.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "mehr", "components.object-property-list.item-placeholder-text": "Platzhalterelement", "components.overflow-group.moreActions": "Weitere Aktionen", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} Element} diff --git a/lang/en-gb.js b/lang/en-gb.js index 4830e2e5336..26ca60cb2f9 100644 --- a/lang/en-gb.js +++ b/lang/en-gb.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "more", "components.object-property-list.item-placeholder-text": "Placeholder Item", "components.overflow-group.moreActions": "More Actions", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} item} diff --git a/lang/en.js b/lang/en.js index 569d4558a69..7b465df428a 100644 --- a/lang/en.js +++ b/lang/en.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "more", "components.object-property-list.item-placeholder-text": "Placeholder Item", "components.overflow-group.moreActions": "More Actions", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} item} diff --git a/lang/es-es.js b/lang/es-es.js index fff3d714103..cfb60447b5a 100644 --- a/lang/es-es.js +++ b/lang/es-es.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "más", "components.object-property-list.item-placeholder-text": "Elemento de marcador de posición", "components.overflow-group.moreActions": "Más acciones", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} elemento} diff --git a/lang/es.js b/lang/es.js index 22356ee0d91..8a848f05bb4 100644 --- a/lang/es.js +++ b/lang/es.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "más", "components.object-property-list.item-placeholder-text": "Elemento de marcador de posición", "components.overflow-group.moreActions": "Más acciones", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} elemento} diff --git a/lang/fr-fr.js b/lang/fr-fr.js index 6b15ef59e1e..a87745a48b3 100644 --- a/lang/fr-fr.js +++ b/lang/fr-fr.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "plus", "components.object-property-list.item-placeholder-text": "Élément d’espace réservé", "components.overflow-group.moreActions": "Plus d’actions", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} élément} diff --git a/lang/fr.js b/lang/fr.js index 0f3cc881a93..f8979e193f2 100644 --- a/lang/fr.js +++ b/lang/fr.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "plus", "components.object-property-list.item-placeholder-text": "Élément de paramètre fictif", "components.overflow-group.moreActions": "Plus d’actions", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} élément} diff --git a/lang/haw.js b/lang/haw.js index de2d20a99b9..846b1a0ee09 100644 --- a/lang/haw.js +++ b/lang/haw.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "hou aku", "components.object-property-list.item-placeholder-text": "Mea Paʻa Wahi", "components.overflow-group.moreActions": "Nā Hana Hou", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} mea} diff --git a/lang/hi.js b/lang/hi.js index d4a5947e1f7..16303fc1082 100644 --- a/lang/hi.js +++ b/lang/hi.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "अधिक", "components.object-property-list.item-placeholder-text": "प्लेसहोल्डर आइटम", "components.overflow-group.moreActions": "अधिक क्रियाएँ", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} आइटम} diff --git a/lang/ja.js b/lang/ja.js index c8195d72ca8..57ee858f7fb 100644 --- a/lang/ja.js +++ b/lang/ja.js @@ -147,6 +147,8 @@ export default { "components.more-less.more": "増やす", "components.object-property-list.item-placeholder-text": "プレースホルダの項目", "components.overflow-group.moreActions": "その他のアクション", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, other {{countFormatted} 個の項目} diff --git a/lang/ko.js b/lang/ko.js index f4bcf45798a..a32f2ebbc33 100644 --- a/lang/ko.js +++ b/lang/ko.js @@ -147,6 +147,8 @@ export default { "components.more-less.more": "더 보기", "components.object-property-list.item-placeholder-text": "자리표시자 항목", "components.overflow-group.moreActions": "추가 작업", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, other {해당 항목 수 {countFormatted}개} diff --git a/lang/mi.js b/lang/mi.js index 322c2cb3d8a..7dbfbef6103 100644 --- a/lang/mi.js +++ b/lang/mi.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "ētahi atu", "components.object-property-list.item-placeholder-text": "Tūemi Puriwāhi", "components.overflow-group.moreActions": "Ētahi atu Hohenga", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} Tūemi} diff --git a/lang/nl.js b/lang/nl.js index 3d3e9401311..35ca1a3fe25 100644 --- a/lang/nl.js +++ b/lang/nl.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "meer", "components.object-property-list.item-placeholder-text": "Item tijdelijke aanduiding", "components.overflow-group.moreActions": "Meer acties", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} item} diff --git a/lang/pt.js b/lang/pt.js index 9da99f9b552..26ede4348b6 100644 --- a/lang/pt.js +++ b/lang/pt.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "mais", "components.object-property-list.item-placeholder-text": "Item de espaço reservado", "components.overflow-group.moreActions": "Mais ações", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} item} diff --git a/lang/sv.js b/lang/sv.js index 7d107505040..1e9f65dd9b1 100644 --- a/lang/sv.js +++ b/lang/sv.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "mer", "components.object-property-list.item-placeholder-text": "Platshållarobjekt", "components.overflow-group.moreActions": "Fler åtgärder", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} objekt} diff --git a/lang/th.js b/lang/th.js index 69e39a595ce..899f502c5a6 100644 --- a/lang/th.js +++ b/lang/th.js @@ -148,6 +148,8 @@ export default { "components.more-less.more": "เพิ่มเติม", "components.object-property-list.item-placeholder-text": "รายการตัวแทน", "components.overflow-group.moreActions": "การดำเนินการเพิ่มเติม", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, other {{countFormatted} รายการ} diff --git a/lang/tr.js b/lang/tr.js index 9dfe17ae411..e090ada5e4d 100644 --- a/lang/tr.js +++ b/lang/tr.js @@ -152,6 +152,8 @@ export default { "components.more-less.more": "daha fazla", "components.object-property-list.item-placeholder-text": "Yer Tutucu Öğesi", "components.overflow-group.moreActions": "Daha Fazla Eylem", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, one {{countFormatted} öğe} diff --git a/lang/vi.js b/lang/vi.js index 4d038ec17f0..587270ad2de 100644 --- a/lang/vi.js +++ b/lang/vi.js @@ -146,6 +146,8 @@ export default { "components.more-less.more": "thêm", "components.object-property-list.item-placeholder-text": "Mục giữ chỗ", "components.overflow-group.moreActions": "Thêm các Tác vụ", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, other {{countFormatted} mục} diff --git a/lang/zh-cn.js b/lang/zh-cn.js index 3658b14a471..0129e96669a 100644 --- a/lang/zh-cn.js +++ b/lang/zh-cn.js @@ -147,6 +147,8 @@ export default { "components.more-less.more": "更多", "components.object-property-list.item-placeholder-text": "占位符项目", "components.overflow-group.moreActions": "更多操作", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, other {{countFormatted} 项} diff --git a/lang/zh-tw.js b/lang/zh-tw.js index ac27fda0215..54d8b207006 100644 --- a/lang/zh-tw.js +++ b/lang/zh-tw.js @@ -148,6 +148,8 @@ export default { "components.more-less.more": "較多", "components.object-property-list.item-placeholder-text": "預留位置項目", "components.overflow-group.moreActions": "其他動作", + "components.page.header-nav-label": "Main", + "components.page.side-nav-label": "Side", "components.pageable.info": `{count, plural, other {{countFormatted} 個項目} From 5994bb77d5aaa848098cd52378bc3bc6d99a8a68 Mon Sep 17 00:00:00 2001 From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com> Date: Fri, 1 May 2026 18:33:34 -0400 Subject: [PATCH 2/6] Linting fixes --- components/page/page.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/components/page/page.js b/components/page/page.js index 8e73b05a63e..a7efbf969b2 100644 --- a/components/page/page.js +++ b/components/page/page.js @@ -56,7 +56,7 @@ class Page extends LocalizeCoreElement(LitElement) { display: flex; margin-inline: var(--d2l-page-margin-inline, 0); max-width: var(--d2l-page-content-max-width, 100%); - padding-bottom: var(--d2l-page-footer-height, 0px); /* Reserve space for fixed footer */ + padding-bottom: var(--d2l-page-footer-height, 0); /* Reserve space for fixed footer */ } main { @@ -66,15 +66,15 @@ class Page extends LocalizeCoreElement(LitElement) { .side-nav-panel, .supporting-panel { + height: calc(100vh - var(--d2l-page-footer-height, 0)); overflow-y: auto; position: sticky; top: 0; - height: calc(100vh - var(--d2l-page-footer-height, 0px)); } .page.header-sticky .side-nav-panel, .page.header-sticky .supporting-panel { - top: var(--d2l-page-header-height, 0px); - height: calc(100vh - var(--d2l-page-header-height, 0px) - var(--d2l-page-footer-height, 0px)); + height: calc(100vh - var(--d2l-page-header-height, 0) - var(--d2l-page-footer-height, 0)); + top: var(--d2l-page-header-height, 0); } .divider { From 4c09b8bae2bcedaba8d4324544b94083053b4b4d Mon Sep 17 00:00:00 2001 From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com> Date: Mon, 4 May 2026 14:45:13 -0400 Subject: [PATCH 3/6] Stacking context adjustments --- components/page/page.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/components/page/page.js b/components/page/page.js index a7efbf969b2..85618b4b849 100644 --- a/components/page/page.js +++ b/components/page/page.js @@ -62,14 +62,18 @@ class Page extends LocalizeCoreElement(LitElement) { main { flex: 1; min-width: 400px; /* TBD */ + overflow: clip; + position: relative; + z-index: 0; } .side-nav-panel, .supporting-panel { height: calc(100vh - var(--d2l-page-footer-height, 0)); - overflow-y: auto; + overflow: clip auto; position: sticky; top: 0; + z-index: 0; } .page.header-sticky .side-nav-panel, .page.header-sticky .supporting-panel { @@ -94,7 +98,7 @@ class Page extends LocalizeCoreElement(LitElement) { inset: auto 0 0; padding: 0.75rem 0; position: fixed; - z-index: 1; /* To be over divider and main contents */ + z-index: 1; /* To be over divider */ } .floating-footer { padding-block-end: 0.75rem; From 67d42fdc1eaebf59598cba6213ea789ef2485df9 Mon Sep 17 00:00:00 2001 From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com> Date: Mon, 4 May 2026 18:04:11 -0400 Subject: [PATCH 4/6] GAUD-9165 - Flesh out the demo page to be more realistic and include real, sticky componets with hardcoded z-indexes --- components/page/demo/page-component.js | 320 ++++++++++++++++++++++++- 1 file changed, 312 insertions(+), 8 deletions(-) diff --git a/components/page/demo/page-component.js b/components/page/demo/page-component.js index 5aa62dc03e5..54c0633c761 100644 --- a/components/page/demo/page-component.js +++ b/components/page/demo/page-component.js @@ -1,8 +1,24 @@ +import '../../button/button-subtle.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 '../../inputs/input-checkbox.js'; +import '../../inputs/input-date.js'; +import '../../inputs/input-number.js'; +import '../../inputs/input-text.js'; +import '../../list/list.js'; +import '../../list/list-controls.js'; +import '../../list/list-item.js'; +import '../../list/list-item-content.js'; +import '../../list/list-item-nav.js'; +import '../../selection/selection-action.js'; +import '../../table/table-controls.js'; import '../page.js'; import { css, html, LitElement, nothing } from 'lit'; import { navStyles } from './temp-nav-styles.js'; import { selectStyles } from '../../inputs/input-select-styles.js'; +import { tableStyles } from '../../table/table-wrapper.js'; /** * Component for d2l-page demos and tests @@ -19,7 +35,7 @@ class PageDemo extends LitElement { _allowThreePanels: { state: true } }; - static styles = [navStyles, selectStyles, css` + static styles = [navStyles, selectStyles, tableStyles, css` .demo-controls { display: flex; flex-wrap: wrap; @@ -148,7 +164,8 @@ class PageDemo extends LitElement { #renderMainPanel() { return html` -
    +
    +

    I'm in the default slot of the d2l-page component!

    ${this.demoMode ? html`
    @@ -165,24 +182,311 @@ class PageDemo extends LitElement {
    ` : nothing} -

    I'm in the default slot of the d2l-page component!

    + +

    List with Sticky Controls (extend-separators)

    + + + + + + + +
    Introduction to Economics
    +
    Chapter 1 — Fundamentals
    +
    Due: May 15, 2026
    +
    +
    + + +
    Supply and Demand
    +
    Chapter 2 — Market Forces
    +
    Due: May 22, 2026
    +
    +
    + + +
    Market Equilibrium
    +
    Chapter 3 — Price Discovery
    +
    Due: May 29, 2026
    +
    +
    +
    + +

    List with Sticky Controls (no extend-separators)

    + + + + + + + +
    Assignment 1: Research Proposal
    +
    Weight: 20%
    +
    Submissions: 14/30
    +
    +
    + + +
    Assignment 2: Literature Review
    +
    Weight: 30%
    +
    Submissions: 8/30
    +
    +
    + + +
    Assignment 3: Final Paper
    +
    Weight: 50%
    +
    Submissions: 0/30
    +
    +
    +
    + +

    Table with Sticky Controls

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    CourseEnrolledCompletion RateAvg Grade
    Introduction to Biology14587%B+
    Advanced Chemistry6279%B
    World History9892%A-
    +
    + +

    Sticky Table

    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    StudentAssignment 1Assignment 2Final ExamTotal
    Alice Johnson92889591.7
    Bob Smith85798282.0
    Carol Davis78918785.3
    David Lee95939092.7
    Emily Chen88849187.7
    +
    -
    End of Content
    +
    End of Content
    `; } #renderSideNavPanel() { return this.hasSideNavPanel ? html` -
    - I'm in the side-nav slot of the d2l-page component! +
    +

    I'm in the side-nav slot of the d2l-page component!

    + + + +
    Course Overview
    +
    +
    + + +
    Unit 1: Foundations
    +
    3 items
    +
    + + + +
    Reading: Core Concepts
    +
    +
    + + +
    Discussion: Key Takeaways
    +
    +
    + + +
    Quiz: Chapter 1
    +
    +
    +
    +
    + + +
    Unit 2: Applications
    +
    4 items
    +
    + + + +
    Case Study Analysis
    +
    +
    + + +
    Group Project
    +
    +
    + + +
    Lab: Data Collection
    +
    +
    + + +
    Reflection Journal
    +
    +
    +
    +
    + + +
    Unit 3: Research Methods
    +
    5 items
    +
    + + + +
    Introduction to Research Design
    +
    +
    + + +
    Qualitative vs Quantitative
    +
    +
    + + +
    Survey Design Workshop
    +
    +
    + + +
    Ethics in Research
    +
    +
    + + +
    Quiz: Research Methods
    +
    +
    +
    +
    +
    +
    End of Content
    ` : nothing; } #renderSupportingPanel() { return this.hasSupportingPanel ? html` -
    - I'm in the supporting slot of the d2l-page component! +
    + +

    I'm in the supporting slot of the d2l-page component!

    + + + +
    + + + Has start date + Has end date + +
    +
    + + +
    + + + + + Include in final grade calculation + +
    +
    + + +
    + + + Allow learners to retract submissions + Notify instructor on submission + + +
    +
    +
    +
    End of Content
    ` : nothing; } From 2c5444ade09e6c9f619932af081f02342c0bd0bb Mon Sep 17 00:00:00 2001 From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com> Date: Mon, 4 May 2026 18:20:56 -0400 Subject: [PATCH 5/6] Add dialog demonstrating stacking context issue, since dialogs don't use popover --- components/page/demo/page-component.js | 33 ++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/components/page/demo/page-component.js b/components/page/demo/page-component.js index 54c0633c761..468ebe83e2c 100644 --- a/components/page/demo/page-component.js +++ b/components/page/demo/page-component.js @@ -1,8 +1,10 @@ +import '../../button/button.js'; import '../../button/button-subtle.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 '../../inputs/input-checkbox.js'; import '../../inputs/input-date.js'; import '../../inputs/input-number.js'; @@ -75,6 +77,15 @@ class PageDemo extends LitElement { } } + // TO DO: Don't commit this! Fix the dialog and add this in the main header + #handleDialogBrokenClick() { + this.shadowRoot.querySelector('#broken-dialog').opened = true; + } + + #handleDialogNativeClick() { + this.shadowRoot.querySelector('#native-dialog').opened = true; + } + #handleNavTypeChange(e) { this.navType = e.target.on ? 'immersive' : 'full'; } @@ -186,6 +197,8 @@ class PageDemo extends LitElement {

    List with Sticky Controls (extend-separators)

    + + @@ -330,6 +343,26 @@ class PageDemo extends LitElement { + +
    + + + + Allow late submissions +
    + Add + Cancel +
    + +
    + + + + Allow late submissions +
    + Add + Cancel +
    End of Content
    `; From 82e9355bccc0fd045f47f2c30f967f83a324b2e0 Mon Sep 17 00:00:00 2001 From: Stacey Van Herk <13419300+svanherk@users.noreply.github.com> Date: Mon, 4 May 2026 19:09:00 -0400 Subject: [PATCH 6/6] GAUD-9165 - Example of how we could apply fix to our sticky components --- components/collapsible-panel/collapsible-panel.js | 10 ++++++---- components/page/page.js | 3 +++ components/selection/selection-controls.js | 2 +- components/table/table-wrapper.js | 6 +++--- 4 files changed, 13 insertions(+), 8 deletions(-) diff --git a/components/collapsible-panel/collapsible-panel.js b/components/collapsible-panel/collapsible-panel.js index 8ace4071b6d..043c1ba26fc 100644 --- a/components/collapsible-panel/collapsible-panel.js +++ b/components/collapsible-panel/collapsible-panel.js @@ -173,15 +173,17 @@ class CollapsiblePanel extends SkeletonMixin(FocusMixin(LitElement)) { border-radius: 0; outline-offset: -2px; } - .d2l-collapsible-panel.scrolled .d2l-collapsible-panel-header { + .d2l-collapsible-panel .d2l-collapsible-panel-header { background-color: var(--d2l-theme-background-color-base); - box-shadow: 0 8px 12px -9px rgba(0, 0, 0, 0.3); position: sticky; - top: 0; + top: var(--d2l-sticky-offset, 0px); z-index: 11; /* must be greater greater than list-items with open dropdowns or tooltips */ } + .d2l-collapsible-panel.scrolled .d2l-collapsible-panel-header { + box-shadow: 0 8px 12px -9px rgba(0, 0, 0, 0.3); + } .d2l-collapsible-panel.focused.scrolled .d2l-collapsible-panel-header { - top: 2px; + top: calc(var(--d2l-sticky-offset, 0px) + 2px); } .d2l-collapsible-panel-title { flex: 1; diff --git a/components/page/page.js b/components/page/page.js index 85618b4b849..ee9fb5da102 100644 --- a/components/page/page.js +++ b/components/page/page.js @@ -66,6 +66,9 @@ class Page extends LocalizeCoreElement(LitElement) { position: relative; z-index: 0; } + .page.header-sticky main { + --d2l-sticky-offset: var(--d2l-page-header-height, 0px); + } .side-nav-panel, .supporting-panel { diff --git a/components/selection/selection-controls.js b/components/selection/selection-controls.js index d164e68ea46..c10344e3c6e 100644 --- a/components/selection/selection-controls.js +++ b/components/selection/selection-controls.js @@ -50,7 +50,7 @@ export class SelectionControls extends PageableSubscriberMixin(SelectionObserver :host { display: block; position: sticky; - top: 0; + top: var(--d2l-sticky-offset, 0px); } :host([no-sticky]) { position: static; diff --git a/components/table/table-wrapper.js b/components/table/table-wrapper.js index 780249f5348..b59c1ab2c36 100644 --- a/components/table/table-wrapper.js +++ b/components/table/table-wrapper.js @@ -241,7 +241,7 @@ export const tableStyles = css` d2l-table-wrapper[sticky-headers][sticky-headers-scroll-wrapper] .d2l-table > thead { display: block; position: sticky; - top: calc(var(--d2l-table-sticky-top, 0px) + var(--d2l-table-border-radius-sticky-offset, 0px)); + top: calc(var(--d2l-sticky-offset, 0px) + var(--d2l-table-sticky-top, 0px) + var(--d2l-table-border-radius-sticky-offset, 0px)); z-index: 2; } @@ -382,7 +382,7 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM } .d2l-sticky-headers-backdrop { position: sticky; - top: calc(var(--d2l-table-sticky-top, 0px) + var(--d2l-table-border-radius)); + top: calc(var(--d2l-sticky-offset, 0px) + var(--d2l-table-sticky-top, 0px) + var(--d2l-table-border-radius)); width: 100%; z-index: 2; /* Must sit under d2l-table sticky-headers but over sticky columns and regular cells */ } @@ -832,7 +832,7 @@ export class TableWrapper extends PropertyRequiredMixin(PageableMixin(SelectionM const stickyRows = Array.from(this._table.querySelectorAll(SELECTORS.headers)); stickyRows.forEach(r => { - const thTop = hasStickyControls ? `${rowTop}px` : `calc(${rowTop}px + var(--d2l-table-border-radius-sticky-offset, 0px))`; + const thTop = hasStickyControls ? `calc(var(--d2l-sticky-offset, 0px) + ${rowTop}px)` : `calc(var(--d2l-sticky-offset, 0px) + ${rowTop}px + var(--d2l-table-border-radius-sticky-offset, 0px))`; const ths = Array.from(r.querySelectorAll('th,td')); ths.forEach(th => th.style.top = thTop);