diff --git a/components/page/page-divider-internal.js b/components/page/page-divider-internal.js
index c95b93824f8..d2466c50cb0 100644
--- a/components/page/page-divider-internal.js
+++ b/components/page/page-divider-internal.js
@@ -1,19 +1,20 @@
import '../colors/colors.js';
import { css, html, LitElement } from 'lit';
+import { FocusMixin } from '../../mixins/focus/focus-mixin.js';
import { formatPercent } from '@brightspace-ui/intl';
import { ifDefined } from 'lit/directives/if-defined.js';
import { PropertyRequiredMixin } from '../../mixins/property-required/property-required-mixin.js';
export const DIVIDER_WIDTH = 4;
-const KEYBOARD_STEP = 20; // TO DO: Confirm
-const KEYBOARD_STEP_LARGE = 80; // TO DO: Confirm
+export const KEYBOARD_STEP = 20; // TO DO: Confirm
+export const KEYBOARD_STEP_LARGE = 80; // TO DO: Confirm
const clampedSize = (size, min, max) => Math.max(min, Math.min(size, max));
/**
* Internal divider used by d2l-page to resize its side-nav and supporting panels.
*/
-class PageDivider extends PropertyRequiredMixin(LitElement) {
+class PageDivider extends FocusMixin(PropertyRequiredMixin(LitElement)) {
static properties = {
/**
@@ -77,6 +78,8 @@ class PageDivider extends PropertyRequiredMixin(LitElement) {
`;
+ static focusElementSelector = '.divider';
+
constructor() {
super();
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-immersive-main.png b/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-immersive-main.png
new file mode 100644
index 00000000000..60e70fd295b
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-immersive-main.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-immersive-panel.png b/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-immersive-panel.png
new file mode 100644
index 00000000000..800f728e098
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-immersive-panel.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-main.png b/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-main.png
new file mode 100644
index 00000000000..01a97a44b34
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-main.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-panel.png b/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-panel.png
new file mode 100644
index 00000000000..6510ee34016
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-scrolled-panel.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-side-nav-immersive.png b/components/page/test/golden/page-divider-internal/chromium/focus-side-nav-immersive.png
new file mode 100644
index 00000000000..8bd70f6bf65
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-side-nav-immersive.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-side-nav-long-no-scroll.png b/components/page/test/golden/page-divider-internal/chromium/focus-side-nav-long-no-scroll.png
new file mode 100644
index 00000000000..a4f2df69cc1
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-side-nav-long-no-scroll.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-side-nav.png b/components/page/test/golden/page-divider-internal/chromium/focus-side-nav.png
new file mode 100644
index 00000000000..82fc925b104
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-side-nav.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-supporting-immersive-long-no-scroll.png b/components/page/test/golden/page-divider-internal/chromium/focus-supporting-immersive-long-no-scroll.png
new file mode 100644
index 00000000000..dc71718ef48
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-supporting-immersive-long-no-scroll.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-supporting-immersive.png b/components/page/test/golden/page-divider-internal/chromium/focus-supporting-immersive.png
new file mode 100644
index 00000000000..103666b1f23
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-supporting-immersive.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/focus-supporting.png b/components/page/test/golden/page-divider-internal/chromium/focus-supporting.png
new file mode 100644
index 00000000000..debe0fbfbda
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/focus-supporting.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/hover-side-nav.png b/components/page/test/golden/page-divider-internal/chromium/hover-side-nav.png
new file mode 100644
index 00000000000..270402d3906
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/hover-side-nav.png differ
diff --git a/components/page/test/golden/page-divider-internal/chromium/hover-supporting-immersive.png b/components/page/test/golden/page-divider-internal/chromium/hover-supporting-immersive.png
new file mode 100644
index 00000000000..1f4f61bb884
Binary files /dev/null and b/components/page/test/golden/page-divider-internal/chromium/hover-supporting-immersive.png differ
diff --git a/components/page/test/page-divider-internal-fixtures.js b/components/page/test/page-divider-internal-fixtures.js
new file mode 100644
index 00000000000..94d14da6fb7
--- /dev/null
+++ b/components/page/test/page-divider-internal-fixtures.js
@@ -0,0 +1,50 @@
+import { createPage } from './page-fixtures.js';
+import { html } from 'lit';
+
+export function createDivider({
+ currentSize = 450,
+ maxSize = 600,
+ minSize = 320,
+ panelType = 'panel',
+ panelPosition = 'start'
+} = {}) {
+ return html`
+
+
+ `;
+}
+
+function createDividerPage(options) {
+ return createPage({
+ mainHeight: '100px',
+ sideNavHeight: '100px',
+ supportingHeight: '100px',
+ ...options
+ });
+}
+
+export function getDivider(elem, panelKey) {
+ return elem.shadowRoot.querySelector(`d2l-page-divider-internal[data-panel-key="${panelKey}"]`);
+}
+
+export function getSlider(elem) {
+ return elem.shadowRoot.querySelector('.divider');
+}
+
+export const pageDividerFixtures = {
+ sideNavBothHeaders: createDividerPage({ layout: 'side-nav', hasMainHeader: true, hasSideNavHeader: true }),
+ sideNavBothHeadersFooter: createDividerPage({ layout: 'side-nav', hasMainHeader: true, hasSideNavHeader: true, hasFooter: true }),
+ sideNavLongMainBothHeaders: createDividerPage({ layout: 'side-nav', hasMainHeader: true, hasSideNavHeader: true, mainHeight: '400px' }),
+ sideNavImmersiveFooter: createDividerPage({ layout: 'side-nav', header: 'immersive', hasFooter: true }),
+ sideNavImmersiveLongFooter: createDividerPage({ layout: 'side-nav', header: 'immersive', hasFooter: true, sideNavHeight: '400px' }),
+ supportingLongFooter: createDividerPage({ layout: 'supporting', hasFooter: true, supportingHeight: '400px' }),
+ supportingImmersiveLongMain: createDividerPage({ layout: 'supporting', header: 'immersive', mainHeight: '400px' }),
+ supportingImmersiveFooter: createDividerPage({ layout: 'supporting', header: 'immersive', hasFooter: true }),
+ supportingImmersiveBothHeaders: createDividerPage({ layout: 'supporting', header: 'immersive', hasMainHeader: true, hasSupportingHeader: true }),
+};
diff --git a/components/page/test/page-divider-internal.axe.js b/components/page/test/page-divider-internal.axe.js
new file mode 100644
index 00000000000..f50626d8ce3
--- /dev/null
+++ b/components/page/test/page-divider-internal.axe.js
@@ -0,0 +1,19 @@
+import '../page-divider-internal.js';
+import { clickElem, expect, fixture, hoverElem } from '@brightspace-ui/testing';
+import { getDivider, pageDividerFixtures } from './page-divider-internal-fixtures.js';
+
+describe('page-divider-internal', () => {
+
+ it('hover', async() => {
+ const elem = await fixture(pageDividerFixtures.supportingImmersiveFooter);
+ await hoverElem(getDivider(elem, 'supporting'));
+ await expect(elem).to.be.accessible();
+ });
+
+ it('focus', async() => {
+ const elem = await fixture(pageDividerFixtures.sideNavBothHeadersFooter);
+ await clickElem(getDivider(elem, 'side-nav'));
+ await expect(elem).to.be.accessible();
+ });
+
+});
diff --git a/components/page/test/page-divider-internal.test.js b/components/page/test/page-divider-internal.test.js
new file mode 100644
index 00000000000..11327608695
--- /dev/null
+++ b/components/page/test/page-divider-internal.test.js
@@ -0,0 +1,131 @@
+import { expect, fixture, html, nextFrame, oneEvent, runConstructor, sendKeysElem } from '@brightspace-ui/testing';
+import { KEYBOARD_STEP, KEYBOARD_STEP_LARGE } from '../page-divider-internal.js';
+import { createDivider } from './page-divider-internal-fixtures.js';
+
+describe('d2l-page-divider-internal', () => {
+
+ it('should construct', () => {
+ runConstructor('d2l-page-divider-internal');
+ });
+
+ describe('accessibility', () => {
+ it('calculates aria values correctly', async() => {
+ const elem = await fixture(html``);
+ const slider = elem.shadowRoot.querySelector('.divider');
+ expect(slider.getAttribute('aria-valuemax')).to.equal('200');
+ expect(slider.getAttribute('aria-valuemin')).to.equal('0');
+ expect(slider.getAttribute('aria-valuenow')).to.equal('150');
+ expect(slider.getAttribute('aria-valuetext')).to.equal('75 %');
+ });
+
+ it('does not set aria values when in unknown state', async() => {
+ const elem = await fixture(html``);
+ const slider = elem.shadowRoot.querySelector('.divider');
+ expect(slider.hasAttribute('aria-valuemax')).to.be.false;
+ expect(slider.hasAttribute('aria-valuemin')).to.be.false;
+ expect(slider.hasAttribute('aria-valuenow')).to.be.false;
+ expect(slider.hasAttribute('aria-valuetext')).to.be.false;
+ });
+ });
+
+ describe('events', () => {
+ describe('d2l-page-divider-toggle', () => {
+ describe('keyboard', () => {
+ ['Enter', ' '].forEach(key => {
+ it(`dispatches d2l-page-divider-toggle on "${key === ' ' ? 'Space' : key}"`, async() => {
+ const elem = await fixture(createDivider());
+ sendKeysElem(elem, 'press', key);
+ const e = await oneEvent(elem, 'd2l-page-divider-toggle');
+ expect(e.detail).to.be.null; // TO DO: Will eventually have the collapse state
+ });
+ });
+ });
+
+ describe('mouse', () => {
+ // TO DO
+ });
+
+ });
+
+ describe('d2l-page-divider-resize', () => {
+ describe('keyboard', () => {
+ const currentSize = 450;
+ const minSize = 320;
+ const maxSize = 600;
+ const step = KEYBOARD_STEP;
+ const halfStep = Math.ceil(step / 2);
+ const largeStep = KEYBOARD_STEP_LARGE;
+ const halfLargeStep = Math.ceil(largeStep / 2);
+
+ [
+ { name: 'start panel', panelType: 'panel', panelPosition: 'start', grow: 'ArrowRight', shrink: 'ArrowLeft', inactiveKeys: ['ArrowUp', 'ArrowDown'] },
+ { name: 'end panel', panelType: 'panel', panelPosition: 'end', grow: 'ArrowLeft', shrink: 'ArrowRight', inactiveKeys: ['ArrowUp', 'ArrowDown'] },
+ { name: 'drawer', panelType: 'drawer', grow: 'ArrowUp', shrink: 'ArrowDown', inactiveKeys: ['ArrowLeft', 'ArrowRight'] },
+ { name: 'start panel in rtl', rtl: true, panelType: 'panel', panelPosition: 'start', grow: 'ArrowLeft', shrink: 'ArrowRight', inactiveKeys: ['ArrowUp', 'ArrowDown'] },
+ { name: 'end panel in rtl', rtl: true, panelType: 'panel', panelPosition: 'end', grow: 'ArrowRight', shrink: 'ArrowLeft', inactiveKeys: ['ArrowUp', 'ArrowDown'] },
+ { name: 'drawer rtl', rtl: true, panelType: 'drawer', grow: 'ArrowUp', shrink: 'ArrowDown', inactiveKeys: ['ArrowLeft', 'ArrowRight'] },
+ ].forEach(test => {
+
+ describe(test.name, () => {
+
+ [
+ { action: 'grow', key: test.grow, expectedSize: currentSize + step },
+ { action: 'shrink', key: test.shrink, expectedSize: currentSize - step },
+ { action: 'large grow', key: 'PageUp', expectedSize: currentSize + largeStep },
+ { action: 'large shrink', key: 'PageDown', expectedSize: currentSize - largeStep },
+ { action: 'max', key: 'End', expectedSize: maxSize },
+ { action: 'min', key: 'Home', expectedSize: minSize }
+ ].forEach(({ action, key, expectedSize }) => {
+ it(`dispatches event with requestedSize ${expectedSize} when "${action}" action requested (Key: ${key})`, async() => {
+ const elem = await fixture(
+ createDivider({ panelType: test.panelType, panelPosition: test.panelPosition }),
+ { rtl: test.rtl }
+ );
+ sendKeysElem(elem, 'press', key);
+ const e = await oneEvent(elem, 'd2l-page-divider-resize');
+ expect(e.detail.requestedSize).to.equal(expectedSize);
+ });
+ });
+
+ [
+ { action: 'grow', key: test.grow, currentSize: maxSize - halfStep, expectedSize: maxSize },
+ { action: 'shrink', key: test.shrink, currentSize: minSize + halfStep, expectedSize: minSize },
+ { action: 'large grow', key: 'PageUp', currentSize: maxSize - halfLargeStep, expectedSize: maxSize },
+ { action: 'large shrink', key: 'PageDown', currentSize: minSize + halfLargeStep, expectedSize: minSize },
+ { action: 'max', key: 'End', currentSize: maxSize, expectedSize: maxSize },
+ { action: 'min', key: 'Home', currentSize: minSize, expectedSize: minSize }
+ ].forEach(({ action, key, currentSize, expectedSize }) => {
+ it(`does not ${action} past limits`, async() => {
+ const elem = await fixture(
+ createDivider({ currentSize, panelType: test.panelType, panelPosition: test.panelPosition }),
+ { rtl: test.rtl }
+ );
+ sendKeysElem(elem, 'press', key);
+ const e = await oneEvent(elem, 'd2l-page-divider-resize');
+ expect(e.detail.requestedSize).to.equal(expectedSize);
+ });
+ });
+
+ it('does not dispatch event when inactive keys pressed', async() => {
+ const elem = await fixture(
+ createDivider({ panelType: test.panelType, panelPosition: test.panelPosition }),
+ { rtl: test.rtl }
+ );
+ let dispatched = false;
+ elem.addEventListener('d2l-page-divider-resize', () => dispatched = true);
+ for (const key of test.inactiveKeys) {
+ await sendKeysElem(elem, 'press', key);
+ }
+ await nextFrame();
+ expect(dispatched).to.be.false;
+ });
+ });
+ });
+ });
+
+ describe('mouse', () => {
+ // TO DO
+ });
+ });
+ });
+});
diff --git a/components/page/test/page-divider-internal.vdiff.js b/components/page/test/page-divider-internal.vdiff.js
new file mode 100644
index 00000000000..8268d3f1dcb
--- /dev/null
+++ b/components/page/test/page-divider-internal.vdiff.js
@@ -0,0 +1,62 @@
+import { expect, fixture, focusElem, hoverElem } from '@brightspace-ui/testing';
+import { getDivider, pageDividerFixtures } from './page-divider-internal-fixtures.js';
+
+describe('page-divider-internal', () => {
+
+ describe('hover', () => {
+ [
+ { name: 'side-nav', divider: 'side-nav', fixture: pageDividerFixtures.sideNavBothHeaders },
+ { name: 'supporting-immersive', divider: 'supporting', fixture: pageDividerFixtures.supportingImmersiveFooter }
+ ].forEach(test => {
+ it(test.name, async() => {
+ const elem = await fixture(test.fixture, { pagePadding: false, viewport: { width: 1000, height: 400 } });
+ await hoverElem(getDivider(elem, test.divider));
+ await expect(elem).to.be.golden({ margin: 0 });
+ });
+ });
+ });
+
+ describe('focus', () => {
+ [
+ { name: 'side-nav', divider: 'side-nav', fixture: pageDividerFixtures.sideNavBothHeadersFooter },
+ { name: 'side-nav-immersive', divider: 'side-nav', fixture: pageDividerFixtures.sideNavImmersiveFooter },
+ { name: 'side-nav-long-no-scroll', divider: 'side-nav', fixture: pageDividerFixtures.sideNavLongMainBothHeaders },
+ { name: 'supporting', divider: 'supporting', fixture: pageDividerFixtures.supportingLongFooter },
+ { name: 'supporting-immersive', divider: 'supporting', fixture: pageDividerFixtures.supportingImmersiveBothHeaders },
+ { name: 'supporting-immersive-long-no-scroll', divider: 'supporting', fixture: pageDividerFixtures.supportingImmersiveLongMain }
+ ].forEach(test => {
+ it(test.name, async() => {
+ const elem = await fixture(test.fixture, { pagePadding: false, viewport: { width: 1000, height: 400 } });
+ await focusElem(getDivider(elem, test.divider));
+ await expect(elem).to.be.golden({ margin: 0 });
+ });
+ });
+ });
+
+ describe('focus-scrolled', () => {
+ [
+ { name: 'main', divider: 'side-nav', fixture: pageDividerFixtures.sideNavLongMainBothHeaders },
+ { name: 'panel', divider: 'supporting', fixture: pageDividerFixtures.supportingLongFooter },
+ { name: 'immersive-main', divider: 'supporting', fixture: pageDividerFixtures.supportingImmersiveLongMain },
+ { name: 'immersive-panel', divider: 'side-nav', fixture: pageDividerFixtures.sideNavImmersiveLongFooter }
+ ].forEach(test => {
+ it(test.name, async() => {
+ const elem = await fixture(test.fixture, { pagePadding: false, viewport: { width: 1000, height: 400 } });
+ await focusElem(getDivider(elem, test.divider));
+ window.scrollTo(0, document.body.scrollHeight);
+ const panel = elem.shadowRoot.querySelector(`.${test.divider}-panel`);
+ panel.scrollTop = panel.scrollHeight;
+ await expect(elem).to.be.golden({ margin: 0 });
+ });
+ });
+ });
+
+ describe('keyboard', () => {
+ // TO DO once arrow visuals added
+ });
+
+ describe('mouse', () => {
+ // TO DO
+ });
+
+});