feat(overlays): add viewport placement to global overlays
This commit is contained in:
parent
4529efb6a6
commit
1cc92fbd6e
6 changed files with 148 additions and 60 deletions
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,9 @@ export class ModalDialogController extends GlobalOverlayController {
|
|||
preventsScroll: true,
|
||||
trapsKeyboardFocus: true,
|
||||
hidesOnEsc: true,
|
||||
viewportConfig: {
|
||||
placement: 'center',
|
||||
},
|
||||
...params,
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue