feat(overlays): add viewport placement to global overlays

This commit is contained in:
qa46hx 2019-09-23 14:38:17 +02:00 committed by Thijs Louisse
parent 4529efb6a6
commit 1cc92fbd6e
6 changed files with 148 additions and 60 deletions

View file

@ -13,6 +13,9 @@ export class GlobalOverlayController extends BaseOverlayController {
preventsScroll: false,
trapsKeyboardFocus: false,
hidesOnEsc: false,
viewportConfig: {
placement: 'center',
},
...params,
};
@ -26,12 +29,14 @@ export class GlobalOverlayController extends BaseOverlayController {
this.trapsKeyboardFocus = finalParams.trapsKeyboardFocus;
this.hidesOnEsc = finalParams.hidesOnEsc;
this.invokerNode = finalParams.invokerNode;
this.overlayContainerClass = `global-overlays__overlay-container`;
this.overlayContainerPlacementClass = `${this.overlayContainerClass}--${finalParams.viewportConfig.placement}`;
}
/**
* Syncs shown state and data.
*
* @param {object} options optioons to sync
* @param {object} options options to sync
* @param {boolean} [options.isShown] whether the overlay should be shown
* @param {object} [options.data] data to pass to the content template function
* @param {HTMLElement} [options.elementToFocusAfterHide] element to return focus when hiding
@ -69,6 +74,8 @@ export class GlobalOverlayController extends BaseOverlayController {
return;
}
if (!this.content.isConnected) {
this.content.classList.add(this.overlayContainerClass);
this.content.classList.add(this.overlayContainerPlacementClass);
this.manager.globalRootNode.appendChild(this.content);
}
@ -99,6 +106,7 @@ export class GlobalOverlayController extends BaseOverlayController {
this.hideDone();
if (this.contentTemplate) {
this.content.classList.remove(this.overlayContainerPlacementClass);
this.manager.globalRootNode.removeChild(this.content);
}
}
@ -183,11 +191,11 @@ export class GlobalOverlayController extends BaseOverlayController {
return;
}
const blockingContoller = this.manager.shownList.find(
const blockingController = this.manager.shownList.find(
ctrl => ctrl !== this && ctrl.isBlocking === true,
);
// if there are no other blocking overlays remaning, stop hiding regular overlays
if (!blockingContoller) {
// if there are no other blocking overlays remaining, stop hiding regular overlays
if (!blockingController) {
this.manager.globalRootNode.classList.remove('global-overlays--blocking-opened');
}
@ -206,7 +214,7 @@ export class GlobalOverlayController extends BaseOverlayController {
* it is removed. Otherwise this is the first time displaying a backdrop, so a fade-in
* animation is played.
* @param {OverlayController} overlay the overlay
* @param {boolean} noAnimation prevent an animatin from being displayed
* @param {boolean} noAnimation prevent an animation from being displayed
*/
enableBackdrop({ animation = true } = {}) {
if (this.__hasActiveBackdrop === true) {
@ -243,4 +251,12 @@ export class GlobalOverlayController extends BaseOverlayController {
this.__hasActiveBackdrop = false;
}
// TODO: this method has to be removed when EventTarget polyfill is available on IE11
__fakeExtendsEventTarget() {
const delegate = document.createDocumentFragment();
['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(funcName => {
this[funcName] = (...args) => delegate[funcName](...args);
});
}
}

View file

@ -7,6 +7,9 @@ export class ModalDialogController extends GlobalOverlayController {
preventsScroll: true,
trapsKeyboardFocus: true,
hidesOnEsc: true,
viewportConfig: {
placement: 'center',
},
...params,
});
}

View file

@ -7,23 +7,29 @@ import { overlays, GlobalOverlayController } from '../index.js';
const globalOverlayDemoStyle = css`
.demo-overlay {
background-color: white;
position: absolute;
top: 20px;
left: 20px;
width: 200px;
border: 1px solid blue;
}
.demo-overlay--2 {
left: 240px;
}
.demo-overlay--toast {
left: initial;
right: 20px;
border: 1px solid lightgrey;
}
`;
let placement = 'center';
const togglePlacement = overlayCtrl => {
const placements = [
'top-left',
'top',
'top-right',
'right',
'bottom-left',
'bottom',
'bottom-right',
'left',
'center',
];
placement = placements[(placements.indexOf(placement) + 1) % placements.length];
// eslint-disable-next-line no-param-reassign
overlayCtrl.overlayContainerClass = `global-overlays__overlay-container--${placement}`;
};
storiesOf('Global Overlay System|Global Overlay', module)
.add('Default', () => {
const overlayCtrl = overlays.add(
@ -126,7 +132,7 @@ storiesOf('Global Overlay System|Global Overlay', module)
<a id="el2" href="#">Anchor</a>
<div id="el3" tabindex="0">Tabindex</div>
<input id="el4" placeholder="Input" />
<div id="el5" contenteditable>Contenteditable</div>
<div id="el5" contenteditable="true">Contenteditable</div>
<textarea id="el6">Textarea</textarea>
<select id="el7">
<option>1</option>
@ -156,8 +162,11 @@ storiesOf('Global Overlay System|Global Overlay', module)
const overlayCtrl2 = overlays.add(
new GlobalOverlayController({
trapsKeyboardFocus: true,
viewportConfig: {
placement: 'left',
},
contentTemplate: () => html`
<div class="demo-overlay demo-overlay--2">
<div class="demo-overlay">
<p>Overlay 2. Tab key is trapped within the overlay</p>
<button @click="${() => overlayCtrl2.hide()}">Close</button>
</div>
@ -203,8 +212,11 @@ storiesOf('Global Overlay System|Global Overlay', module)
const blockingOverlayCtrl = overlays.add(
new GlobalOverlayController({
isBlocking: true,
viewportConfig: {
placement: 'left',
},
contentTemplate: () => html`
<div class="demo-overlay demo-overlay--2">
<div class="demo-overlay">
<p>Hides other overlays</p>
<button @click="${() => blockingOverlayCtrl.hide()}">Close</button>
</div>
@ -243,6 +255,37 @@ storiesOf('Global Overlay System|Global Overlay', module)
</button>
`;
})
.add('Option "viewportConfig:placement"', () => {
const overlayCtrl = overlays.add(
new GlobalOverlayController({
viewportConfig: {
placement: 'center',
},
hasBackdrop: true,
trapsKeyboardFocus: true,
contentTemplate: () => html`
<div class="demo-overlay">
<p>Overlay placement: ${placement}</p>
<button @click="${() => overlayCtrl.hide()}">Close</button>
</div>
`,
}),
);
return html`
<style>
${globalOverlayDemoStyle}
</style>
<button @click=${() => togglePlacement(overlayCtrl)}>Change placement</button>
<button
@click="${event => overlayCtrl.show(event.target)}"
aria-haspopup="dialog"
aria-expanded="false"
>
Open overlay
</button>
`;
})
.add('Sync', () => {
const overlayCtrl = overlays.add(
new GlobalOverlayController({

View file

@ -6,15 +6,8 @@ import { overlays, ModalDialogController } from '../index.js';
const modalDialogDemoStyle = css`
.demo-overlay {
background-color: white;
position: absolute;
top: 20px;
left: 20px;
width: 200px;
border: 1px solid blue;
}
.demo-overlay--2 {
left: 240px;
border: 1px solid lightgrey;
}
`;
@ -23,7 +16,7 @@ storiesOf('Global Overlay System|Modal Dialog', module)
const nestedDialogCtrl = overlays.add(
new ModalDialogController({
contentTemplate: () => html`
<div class="demo-overlay demo-overlay--2">
<div class="demo-overlay">
<p>Nested modal dialog</p>
<button @click="${() => nestedDialogCtrl.hide()}">Close</button>
</div>

View file

@ -76,15 +76,18 @@ describe('GlobalOverlayController', () => {
it('removes the overlay from DOM when hiding', async () => {
const ctrl = overlays.add(
new GlobalOverlayController({
viewportConfig: {
placement: 'top-left',
},
contentTemplate: () => html`
<p>Content</p>
<div>Content</div>
`,
}),
);
await ctrl.show();
expect(getRenderedContainers().length).to.equal(1);
expect(getRenderedOverlay(0).tagName).to.equal('P');
expect(getRenderedOverlay(0).tagName).to.equal('DIV');
expect(getRenderedOverlay(0).textContent).to.equal('Content');
expect(getTopContainer()).to.equal(getRenderedContainer(0));
@ -111,34 +114,6 @@ describe('GlobalOverlayController', () => {
expect(ctrl.isShown).to.equal(false);
});
it('puts the latest shown overlay always on top', async () => {
const controller0 = overlays.add(
new GlobalOverlayController({
contentTemplate: () => html`
<p>Content0</p>
`,
}),
);
const controller1 = overlays.add(
new GlobalOverlayController({
contentTemplate: () => html`
<p>Content1</p>
`,
}),
);
await controller0.show();
await controller1.show();
await controller0.show();
expect(getRenderedContainers().length).to.equal(2);
expect(getRenderedOverlay(0).tagName).to.equal('P');
expect(getRenderedOverlay(0).textContent).to.equal('Content0');
expect(getRenderedOverlay(1).tagName).to.equal('P');
expect(getRenderedOverlay(1).textContent).to.equal('Content1');
expect(getTopOverlay().textContent).to.equal('Content0');
});
it('does not recreate the overlay elements when calling show multiple times', async () => {
const ctrl = overlays.add(
new GlobalOverlayController({
@ -185,6 +160,9 @@ describe('GlobalOverlayController', () => {
it('focuses body when hiding by default', async () => {
const ctrl = overlays.add(
new GlobalOverlayController({
viewportConfig: {
placement: 'top-left',
},
contentTemplate: () => html`
<div><input />=</div>
`,
@ -208,6 +186,9 @@ describe('GlobalOverlayController', () => {
const ctrl = overlays.add(
new GlobalOverlayController({
elementToFocusAfterHide: input,
viewportConfig: {
placement: 'top-left',
},
contentTemplate: () => html`
<div><textarea></textarea></div>
`,
@ -230,6 +211,9 @@ describe('GlobalOverlayController', () => {
const ctrl = overlays.add(
new GlobalOverlayController({
viewportConfig: {
placement: 'top-left',
},
contentTemplate: () => html`
<div><textarea></textarea></div>
`,
@ -252,6 +236,9 @@ describe('GlobalOverlayController', () => {
const ctrl = overlays.add(
new GlobalOverlayController({
viewportConfig: {
placement: 'top-left',
},
contentTemplate: () => html`
<div><textarea></textarea></div>
`,
@ -351,4 +338,49 @@ describe('GlobalOverlayController', () => {
expect(ctrl.backdropNode).to.have.class('global-overlays__backdrop');
});
});
describe('viewportConfig', () => {
it('places the overlay in center by default', async () => {
const controller = new GlobalOverlayController({
contentTemplate: () =>
html`
<p>Content</p>
`,
});
controller.show();
expect(controller.overlayContainerClass).to.equal(
'global-overlays__overlay-container--center',
);
});
it('can set the placement relative to the viewport ', async () => {
const placementMap = [
'top-left',
'top',
'top-right',
'right',
'bottom-right',
'bottom',
'bottom-left',
'left',
'center',
];
placementMap.forEach(viewportPlacement => {
const controller = new GlobalOverlayController({
viewportConfig: {
placement: viewportPlacement,
},
contentTemplate: () =>
html`
<p>Content</p>
`,
});
controller.show();
expect(controller.overlayContainerClass).to.equal(
`global-overlays__overlay-container--${viewportPlacement}`,
);
});
});
});
});

View file

@ -25,5 +25,6 @@ describe('ModalDialogController', () => {
expect(ctrl.preventsScroll).to.be.true;
expect(ctrl.trapsKeyboardFocus).to.be.true;
expect(ctrl.hidesOnEsc).to.be.true;
expect(ctrl.overlayContainerClass).to.equal('global-overlays__overlay-container--center');
});
});