Skip to content
Closed
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 27 additions & 17 deletions components/page/page-divider-internal.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import '../colors/colors.js';
import '../icons/icon-custom.js';
import { css, html, LitElement, nothing } from 'lit';
import { classMap } from 'lit/directives/class-map.js';
import { FocusMixin } from '../../mixins/focus/focus-mixin.js';
import { formatPercent } from '@brightspace-ui/intl';
import { ifDefined } from 'lit/directives/if-defined.js';
Expand Down Expand Up @@ -141,7 +142,7 @@ class PageDivider extends FocusMixin(PropertyRequiredMixin(LitElement)) {
.divider-arrow {
align-items: center;
background-color: rgba(255, 255, 255, 0.5);
cursor: pointer;
cursor: default;
display: none;
height: 24px;
inset-block-start: max(50%, 97px); /* Do not hide behind slider on short screens */
Expand All @@ -150,16 +151,19 @@ class PageDivider extends FocusMixin(PropertyRequiredMixin(LitElement)) {
transform: translateY(-50%);
width: 24px;
}
.divider-arrow.active {
cursor: pointer;
}
.divider-arrow d2l-icon-custom {
color: var(--d2l-color-celestine);
}
.divider:focus-within .divider-arrow:not([hidden]) {
display: flex;
}
.divider-arrow:hover {
.divider-arrow.active:hover {
background-color: var(--d2l-color-gypsum);
}
.divider-arrow:hover d2l-icon-custom {
.divider-arrow.active:hover d2l-icon-custom {
color: var(--d2l-color-celestine-minus-1);
}
.divider-arrow.start {
Expand Down Expand Up @@ -205,6 +209,8 @@ class PageDivider extends FocusMixin(PropertyRequiredMixin(LitElement)) {

render() {
const { showStartArrow, showEndArrow } = this.#getArrowVisibility();
const startArrowClasses = { 'divider-arrow': true, 'start': true, 'active': !this.collapsed };
const endArrowClasses = { 'divider-arrow': true, 'end': true, 'active': !this.collapsed };
let ariaValues = {};
if (this.maxSize > 0) {
ariaValues = { max: this.maxSize, min: 0, now: this.currentSize, text: formatPercent(this.currentSize / this.maxSize, { maximumFractionDigits: 0 }) };
Expand All @@ -213,10 +219,10 @@ class PageDivider extends FocusMixin(PropertyRequiredMixin(LitElement)) {
return html`
<div class="divider" @click="${this.#handleClick}" @pointerdown="${this.#handlePointerDown}">
${this.panelType === 'panel' ? html`
<div class="divider-arrow start" data-position="start" ?hidden="${!showStartArrow}">
<div class="${classMap(startArrowClasses)}" data-position="start" ?hidden="${!showStartArrow}">
<d2l-icon-custom size="tier1">${ICON_ARROW_COLLAPSE_LEFT}</d2l-icon-custom>
</div>
<div class="divider-arrow end" data-position="end" ?hidden="${!showEndArrow}">
<div class="${classMap(endArrowClasses)}" data-position="end" ?hidden="${!showEndArrow}">
<d2l-icon-custom size="tier1">${ICON_ARROW_COLLAPSE_RIGHT}</d2l-icon-custom>
</div>
` : nothing}
Expand All @@ -241,6 +247,7 @@ class PageDivider extends FocusMixin(PropertyRequiredMixin(LitElement)) {
`;
}

#clickedArrow;
#clickedHandle = false;

#getArrowVisibility() {
Expand All @@ -265,10 +272,20 @@ class PageDivider extends FocusMixin(PropertyRequiredMixin(LitElement)) {
}

#handleClick(e) {
// Do not toggle until click event is received, to avoid clicking on elements under the handle in drawer mode
// Do not toggle/resize until click event is received,
// to avoid clicking on elements under the arrows in overlay mode or under the handle in drawer mode
e.stopPropagation();
if (this.collapsed || this.#clickedHandle) {
if (this.collapsed && this.#clickedArrow) {
return;
} else if (this.collapsed || this.#clickedHandle) {
this.#sendToggleEvent();
} else if (this.#clickedArrow) {
const endArrowClicked = this.#clickedArrow.dataset.position === 'end';
const shouldGrow = endArrowClicked === (this.panelPosition === 'start');

const step = (shouldGrow ? 1 : -1) * KEYBOARD_STEP;
const requestedSize = this.currentSize + step;
this.#sendResizeEvent(requestedSize);
}
}

Expand Down Expand Up @@ -313,17 +330,10 @@ class PageDivider extends FocusMixin(PropertyRequiredMixin(LitElement)) {

const path = e.composedPath();
this.#clickedHandle = path.some(el => el.classList?.contains('divider-handle'));
this.#clickedArrow = path.find(el => el.classList?.contains('divider-arrow'));
Comment on lines 332 to +333

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing I wanted to call out here - this is a maybe a tad confusing, because one stores a boolean and one stores the actual arrow (since I need it later). I could store the handle to be consistent, but I don't need it. I could use two variables (one to store that the arrow was clicked, and one to store the arrow) but that felt like overkill. Or I could update the naming, but I liked how this flowed 🤷‍♀️

if (this.#clickedArrow) return; // Arrows don't support dragging

const clickedArrow = path.find(el => el.classList?.contains('divider-arrow'));
if (clickedArrow) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This code moving into the click handler above fixes an eventual bug that would have come up with overlay. It's the same issue the drawer would have had - if we move the divider before the click happens, it'll click what's behind it. I thought this was only an issue with the drawer handle, but it also happens to the arrows in overlay mode, where clicking the overlay closes the drawer.

Even if we don't want that to happen (I need to ask design), it's just better and will likely save us from some other bug in the future.

const endArrowClicked = clickedArrow.dataset.position === 'end';
const shouldGrow = endArrowClicked === (this.panelPosition === 'start');

const step = (shouldGrow ? 1 : -1) * KEYBOARD_STEP;
const requestedSize = this.currentSize + step;
this.#sendResizeEvent(requestedSize);
return;
}
// TO DO: Dragging
}

#sendResizeEvent(requestedSize) {
Expand Down
5 changes: 5 additions & 0 deletions components/page/test/page-divider-internal-fixtures.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,12 @@ export const pageDividerFixtures = {
supportingImmersiveFooter: createDividerPage({ layout: 'supporting', header: 'immersive', hasFooter: true }),
supportingImmersiveBothHeaders: createDividerPage({ layout: 'supporting', header: 'immersive', hasMainHeader: true, hasSupportingHeader: true }),
supportingImmersiveLongMainLongBothHeaders: createDividerPage({ layout: 'supporting', header: 'immersive', hasMainHeader: true, hasSupportingHeader: true, mainHeight: '400px', supportingHeight: '400px' }),
// Alternate width-type iterations
sideNavBothHeadersWide: createDividerPage({ widthType: 'wide', layout: 'side-nav', hasMainHeader: true, hasSideNavHeader: true }),
supportingLongFooterWide: createDividerPage({ widthType: 'wide', layout: 'supporting', hasFooter: true, supportingHeight: '400px' }),
// With state-storage-key set
sideNavBothHeadersStorageKey: createDividerPage({ setStateStorageKey: true, layout: 'side-nav', hasMainHeader: true, hasSideNavHeader: true }),
sideNavBothHeadersFooterStorageKey: createDividerPage({ setStateStorageKey: true, layout: 'side-nav', hasMainHeader: true, hasSideNavHeader: true, hasFooter: true }),
supportingLongFooterStorageKey: createDividerPage({ setStateStorageKey: true, layout: 'supporting', hasFooter: true, supportingHeight: '400px' }),
supportingImmersiveBothHeadersStorageKey: createDividerPage({ setStateStorageKey: true, layout: 'supporting', header: 'immersive', hasMainHeader: true, hasSupportingHeader: true }),
};
26 changes: 21 additions & 5 deletions components/page/test/page-divider-internal.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,18 @@ describe('d2l-page-divider-internal', () => {
clickElem(elem);
await oneEvent(elem, 'd2l-page-divider-toggle');
});

it('does not dispatch event when divider arrow is clicked if collapsed', async() => {
const elem = await fixture(createDivider({ collapsed: true }));
const arrow = getDividerArrow(elem, 'end');
expect(arrow.hidden).to.be.false;

await focusElem(elem);
let dispatched = false;
elem.addEventListener('d2l-page-divider-toggle', () => dispatched = true);
await clickElem(arrow);
expect(dispatched).to.be.false;
});
});

});
Expand Down Expand Up @@ -211,15 +223,19 @@ describe('d2l-page-divider-internal', () => {
expect(arrowElem.hidden).to.be.true;
});

it('grow arrow appears and requests a resize to min size when collapsed', async() => {
it('grow arrow appears when collapsed but does not dispatch event on click', async() => {
const elem = await fixture(
createDivider({ collapsed: true, currentSize: 0, panelPosition: test.panelPosition }),
createDivider({ collapsed: true, currentSize, panelPosition: test.panelPosition }),
{ rtl: test.rtl }
);
const arrowElem = getDividerArrow(elem, test.growArrow);
expect(arrowElem.hidden).to.be.false;

await focusElem(elem);
clickElem(getDividerArrow(elem, test.growArrow));
const e = await oneEvent(elem, 'd2l-page-divider-resize');
expect(e.detail.requestedSize).to.equal(minSize);
let dispatched = false;
elem.addEventListener('d2l-page-divider-resize', () => dispatched = true);
await clickElem(elem);
expect(dispatched).to.be.false;
});
});
});
Expand Down
145 changes: 141 additions & 4 deletions components/page/test/page-divider-internal.vdiff.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { addMarkers, clearStoredPanelState, scrollBody, scrollPanel, setStoredPanelState } from './page-fixtures.js';
import { clickElem, expect, fixture, focusElem, hoverElem, nextFrame, sendKeysElem } from '@brightspace-ui/testing';
import { getDivider, getSlider, pageDividerFixtures } from './page-divider-internal-fixtures.js';
import { scrollBody, scrollPanel } from './page-fixtures.js';
import { DIVIDER_WIDTH, KEYBOARD_STEP, KEYBOARD_STEP_LARGE } from '../page-divider-internal.js';
import { getDivider, getDividerArrow, getSlider, pageDividerFixtures } from './page-divider-internal-fixtures.js';
import { MAIN_MIN_WIDTH, PANEL_MIN_WIDTH, SIDE_NAV_DEFAULT_WIDTH, supportingDefaultWidth } from '../page.js';

describe('page-divider-internal', () => {

Expand All @@ -27,10 +29,66 @@ describe('page-divider-internal', () => {
await expect(elem).to.be.golden({ margin: 0 });
});
});

[
{ name: 'arrow-start-side-nav', arrow: 'start', divider: 'side-nav', fixture: pageDividerFixtures.sideNavBothHeaders },
{ name: 'arrow-end-side-nav', arrow: 'end', divider: 'side-nav', fixture: pageDividerFixtures.sideNavBothHeaders },
{ name: 'arrow-start-supporting-immersive', arrow: 'start', divider: 'supporting', fixture: pageDividerFixtures.supportingImmersiveFooter },
{ name: 'arrow-end-supporting-immersive', arrow: 'end', divider: 'supporting', fixture: pageDividerFixtures.supportingImmersiveFooter }
].forEach(test => {
it(test.name, async() => {
const elem = await fixture(test.fixture, { pagePadding: false, viewport: { width: 1000, height: 400 } });
const divider = getDivider(elem, test.divider);
await focusElem(divider);
await hoverElem(getDividerArrow(divider, test.arrow));
await expect(elem).to.be.golden({ margin: 0 });
});
});
});

describe('hover-collapsed', () => {
// TO DO once previous panel state is saved and restored (so I can set collapsed easily)
afterEach(() => {
clearStoredPanelState();
});

[
{ name: 'side-nav', divider: 'side-nav', fixture: pageDividerFixtures.sideNavBothHeadersFooterStorageKey },
{ name: 'supporting-immersive', divider: 'supporting', fixture: pageDividerFixtures.supportingImmersiveBothHeadersStorageKey }
].forEach(test => {
it(test.name, async() => {
setStoredPanelState({ [test.divider]: { size: 400, collapsed: true } });
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 });
});
});

[
{ name: 'handle-side-nav', divider: 'side-nav', fixture: pageDividerFixtures.sideNavBothHeadersFooterStorageKey },
{ name: 'handle-supporting-immersive', divider: 'supporting', fixture: pageDividerFixtures.supportingImmersiveBothHeadersStorageKey }
].forEach(test => {
it(test.name, async() => {
setStoredPanelState({ [test.divider]: { size: 400, collapsed: true } });
const elem = await fixture(test.fixture, { pagePadding: false, viewport: { width: 1000, height: 400 } });
const divider = getDivider(elem, test.divider);
await hoverElem(getSlider(divider));
await expect(elem).to.be.golden({ margin: 0 });
});
});

[
{ name: 'arrow-side-nav', arrow: 'end', divider: 'side-nav', fixture: pageDividerFixtures.sideNavBothHeadersFooterStorageKey },
{ name: 'arrow-supporting-immersive', arrow: 'start', divider: 'supporting', fixture: pageDividerFixtures.supportingImmersiveBothHeadersStorageKey }
].forEach(test => {
it(test.name, async() => {
setStoredPanelState({ [test.divider]: { size: 400, collapsed: true } });
const elem = await fixture(test.fixture, { pagePadding: false, viewport: { width: 1000, height: 400 } });
const divider = getDivider(elem, test.divider);
await focusElem(divider);
await hoverElem(getDividerArrow(divider, test.arrow));
await expect(elem).to.be.golden({ margin: 0 });
});
});
});

describe('focus', () => {
Expand Down Expand Up @@ -138,8 +196,46 @@ describe('page-divider-internal', () => {
});
});

// Grey marker is default size
// Blue marker is requested step size
// Green marker is the expected resulting position
describe('keyboard', () => {
// TO DO once arrow visuals added
const width = 1250;
const maxPanelSize = width - MAIN_MIN_WIDTH - DIVIDER_WIDTH;
const minPanelSize = PANEL_MIN_WIDTH;
const sideNavDefault = SIDE_NAV_DEFAULT_WIDTH;
const supportingDefault = supportingDefaultWidth(width);

[
{ name: 'side-nav', position: 'start', fixture: pageDividerFixtures.sideNavBothHeadersWide, divider: 'side-nav', grow: 'ArrowRight', shrink: 'ArrowLeft', default: sideNavDefault },
{ name: 'supporting', position: 'end', fixture: pageDividerFixtures.supportingLongFooterWide, divider: 'supporting', grow: 'ArrowLeft', shrink: 'ArrowRight', default: supportingDefault },
{ name: 'rtl-side-nav', rtl: true, position: 'start', fixture: pageDividerFixtures.sideNavBothHeadersWide, divider: 'side-nav', grow: 'ArrowLeft', shrink: 'ArrowRight', default: sideNavDefault },
{ name: 'rtl-supporting', rtl: true, position: 'end', fixture: pageDividerFixtures.supportingLongFooterWide, divider: 'supporting', grow: 'ArrowRight', shrink: 'ArrowLeft', default: supportingDefault }
].forEach(test => {
describe(test.name, () => {
[
{ action: 'grow-small', key: test.grow, requested: test.default + KEYBOARD_STEP, expected: Math.min(test.default + KEYBOARD_STEP, maxPanelSize) },
{ action: 'shrink-small', key: test.shrink, requested: test.default - KEYBOARD_STEP, expected: Math.max(test.default - KEYBOARD_STEP, minPanelSize) },
{ action: 'grow-large', key: 'PageUp', requested: test.default + KEYBOARD_STEP_LARGE, expected: Math.min(test.default + KEYBOARD_STEP_LARGE, maxPanelSize) },
{ action: 'shrink-large', key: 'PageDown', requested: test.default - KEYBOARD_STEP_LARGE, expected: Math.max(test.default - KEYBOARD_STEP_LARGE, minPanelSize) },
{ action: 'max', key: 'End', requested: maxPanelSize, expected: maxPanelSize },
{ action: 'min', key: 'Home', requested: minPanelSize, expected: minPanelSize },
].forEach(({ action, key, requested, expected }) => {
it(action, async() => {
const elem = await fixture(test.fixture, { pagePadding: false, rtl: test.rtl, viewport: { height: 325, width: width } });
addMarkers(elem, test.position, [
{ color: 'grey', size: test.default },
{ color: 'green', size: expected },
...(requested !== expected ? [{ color: 'blue', size: requested }] : [])
]);

const divider = getDivider(elem, test.divider);
await sendKeysElem(divider, 'press', key);
await expect(elem).to.be.golden({ margin: 0 });
});
});
});
});
});

describe('click', () => {
Expand Down Expand Up @@ -186,6 +282,47 @@ describe('page-divider-internal', () => {
await expect(elem).to.be.golden({ margin: 0 });
});
});

// Grey marker is starting position
// Green marker is the expected resulting position
describe('arrow', () => {
const stored = 400;

afterEach(() => {
clearStoredPanelState();
});

[
{ name: 'side-nav', position: 'start', fixture: pageDividerFixtures.sideNavBothHeadersStorageKey, divider: 'side-nav', grow: 'end', shrink: 'start' },
{ name: 'supporting', position: 'end', fixture: pageDividerFixtures.supportingLongFooterStorageKey, divider: 'supporting', grow: 'start', shrink: 'end' },
{ name: 'rtl-side-nav', rtl: true, position: 'start', fixture: pageDividerFixtures.sideNavBothHeadersStorageKey, divider: 'side-nav', grow: 'end', shrink: 'start' },
{ name: 'rtl-supporting', rtl: true, position: 'end', fixture: pageDividerFixtures.supportingLongFooterStorageKey, divider: 'supporting', grow: 'start', shrink: 'end' }
].forEach(test => {
describe(test.name, () => {
[
{ action: 'grow', arrow: test.grow, collapsed: false, startingSize: stored, expected: stored + KEYBOARD_STEP },
{ action: 'shrink', arrow: test.shrink, collapsed: false, startingSize: stored, expected: stored - KEYBOARD_STEP }
].forEach(({ action, arrow, collapsed, startingSize, expected }) => {
it(action, async() => {
setStoredPanelState({
'side-nav': { collapsed: collapsed, size: stored },
'supporting': { collapsed: collapsed, size: stored }
});
const elem = await fixture(test.fixture, { pagePadding: false, rtl: test.rtl, viewport: { height: 325, width: 1100 } });
addMarkers(elem, test.position, [
{ color: 'grey', size: startingSize },
{ color: 'green', size: expected }
]);

const divider = getDivider(elem, test.divider);
await focusElem(divider);
await clickElem(getDividerArrow(divider, arrow));
await expect(elem).to.be.golden({ margin: 0 });
});
});
});
});
});
});

});
Loading