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 + }); + +});