Skip to content

Commit 1cc92fb

Browse files
gerjanvangeesttlouisse
authored andcommitted
feat(overlays): add viewport placement to global overlays
1 parent 4529efb commit 1cc92fb

File tree

6 files changed

+148
-60
lines changed

6 files changed

+148
-60
lines changed

packages/overlays/src/GlobalOverlayController.js

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,9 @@ export class GlobalOverlayController extends BaseOverlayController {
1313
preventsScroll: false,
1414
trapsKeyboardFocus: false,
1515
hidesOnEsc: false,
16+
viewportConfig: {
17+
placement: 'center',
18+
},
1619
...params,
1720
};
1821

@@ -26,12 +29,14 @@ export class GlobalOverlayController extends BaseOverlayController {
2629
this.trapsKeyboardFocus = finalParams.trapsKeyboardFocus;
2730
this.hidesOnEsc = finalParams.hidesOnEsc;
2831
this.invokerNode = finalParams.invokerNode;
32+
this.overlayContainerClass = `global-overlays__overlay-container`;
33+
this.overlayContainerPlacementClass = `${this.overlayContainerClass}--${finalParams.viewportConfig.placement}`;
2934
}
3035

3136
/**
3237
* Syncs shown state and data.
3338
*
34-
* @param {object} options optioons to sync
39+
* @param {object} options options to sync
3540
* @param {boolean} [options.isShown] whether the overlay should be shown
3641
* @param {object} [options.data] data to pass to the content template function
3742
* @param {HTMLElement} [options.elementToFocusAfterHide] element to return focus when hiding
@@ -69,6 +74,8 @@ export class GlobalOverlayController extends BaseOverlayController {
6974
return;
7075
}
7176
if (!this.content.isConnected) {
77+
this.content.classList.add(this.overlayContainerClass);
78+
this.content.classList.add(this.overlayContainerPlacementClass);
7279
this.manager.globalRootNode.appendChild(this.content);
7380
}
7481

@@ -99,6 +106,7 @@ export class GlobalOverlayController extends BaseOverlayController {
99106

100107
this.hideDone();
101108
if (this.contentTemplate) {
109+
this.content.classList.remove(this.overlayContainerPlacementClass);
102110
this.manager.globalRootNode.removeChild(this.content);
103111
}
104112
}
@@ -183,11 +191,11 @@ export class GlobalOverlayController extends BaseOverlayController {
183191
return;
184192
}
185193

186-
const blockingContoller = this.manager.shownList.find(
194+
const blockingController = this.manager.shownList.find(
187195
ctrl => ctrl !== this && ctrl.isBlocking === true,
188196
);
189-
// if there are no other blocking overlays remaning, stop hiding regular overlays
190-
if (!blockingContoller) {
197+
// if there are no other blocking overlays remaining, stop hiding regular overlays
198+
if (!blockingController) {
191199
this.manager.globalRootNode.classList.remove('global-overlays--blocking-opened');
192200
}
193201

@@ -206,7 +214,7 @@ export class GlobalOverlayController extends BaseOverlayController {
206214
* it is removed. Otherwise this is the first time displaying a backdrop, so a fade-in
207215
* animation is played.
208216
* @param {OverlayController} overlay the overlay
209-
* @param {boolean} noAnimation prevent an animatin from being displayed
217+
* @param {boolean} noAnimation prevent an animation from being displayed
210218
*/
211219
enableBackdrop({ animation = true } = {}) {
212220
if (this.__hasActiveBackdrop === true) {
@@ -243,4 +251,12 @@ export class GlobalOverlayController extends BaseOverlayController {
243251

244252
this.__hasActiveBackdrop = false;
245253
}
254+
255+
// TODO: this method has to be removed when EventTarget polyfill is available on IE11
256+
__fakeExtendsEventTarget() {
257+
const delegate = document.createDocumentFragment();
258+
['addEventListener', 'dispatchEvent', 'removeEventListener'].forEach(funcName => {
259+
this[funcName] = (...args) => delegate[funcName](...args);
260+
});
261+
}
246262
}

packages/overlays/src/ModalDialogController.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ export class ModalDialogController extends GlobalOverlayController {
77
preventsScroll: true,
88
trapsKeyboardFocus: true,
99
hidesOnEsc: true,
10+
viewportConfig: {
11+
placement: 'center',
12+
},
1013
...params,
1114
});
1215
}

packages/overlays/stories/global-overlay.stories.js

Lines changed: 59 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7,23 +7,29 @@ import { overlays, GlobalOverlayController } from '../index.js';
77
const globalOverlayDemoStyle = css`
88
.demo-overlay {
99
background-color: white;
10-
position: absolute;
11-
top: 20px;
12-
left: 20px;
1310
width: 200px;
14-
border: 1px solid blue;
15-
}
16-
17-
.demo-overlay--2 {
18-
left: 240px;
19-
}
20-
21-
.demo-overlay--toast {
22-
left: initial;
23-
right: 20px;
11+
border: 1px solid lightgrey;
2412
}
2513
`;
2614

15+
let placement = 'center';
16+
const togglePlacement = overlayCtrl => {
17+
const placements = [
18+
'top-left',
19+
'top',
20+
'top-right',
21+
'right',
22+
'bottom-left',
23+
'bottom',
24+
'bottom-right',
25+
'left',
26+
'center',
27+
];
28+
placement = placements[(placements.indexOf(placement) + 1) % placements.length];
29+
// eslint-disable-next-line no-param-reassign
30+
overlayCtrl.overlayContainerClass = `global-overlays__overlay-container--${placement}`;
31+
};
32+
2733
storiesOf('Global Overlay System|Global Overlay', module)
2834
.add('Default', () => {
2935
const overlayCtrl = overlays.add(
@@ -126,7 +132,7 @@ storiesOf('Global Overlay System|Global Overlay', module)
126132
<a id="el2" href="#">Anchor</a>
127133
<div id="el3" tabindex="0">Tabindex</div>
128134
<input id="el4" placeholder="Input" />
129-
<div id="el5" contenteditable>Contenteditable</div>
135+
<div id="el5" contenteditable="true">Contenteditable</div>
130136
<textarea id="el6">Textarea</textarea>
131137
<select id="el7">
132138
<option>1</option>
@@ -156,8 +162,11 @@ storiesOf('Global Overlay System|Global Overlay', module)
156162
const overlayCtrl2 = overlays.add(
157163
new GlobalOverlayController({
158164
trapsKeyboardFocus: true,
165+
viewportConfig: {
166+
placement: 'left',
167+
},
159168
contentTemplate: () => html`
160-
<div class="demo-overlay demo-overlay--2">
169+
<div class="demo-overlay">
161170
<p>Overlay 2. Tab key is trapped within the overlay</p>
162171
<button @click="${() => overlayCtrl2.hide()}">Close</button>
163172
</div>
@@ -203,8 +212,11 @@ storiesOf('Global Overlay System|Global Overlay', module)
203212
const blockingOverlayCtrl = overlays.add(
204213
new GlobalOverlayController({
205214
isBlocking: true,
215+
viewportConfig: {
216+
placement: 'left',
217+
},
206218
contentTemplate: () => html`
207-
<div class="demo-overlay demo-overlay--2">
219+
<div class="demo-overlay">
208220
<p>Hides other overlays</p>
209221
<button @click="${() => blockingOverlayCtrl.hide()}">Close</button>
210222
</div>
@@ -243,6 +255,37 @@ storiesOf('Global Overlay System|Global Overlay', module)
243255
</button>
244256
`;
245257
})
258+
.add('Option "viewportConfig:placement"', () => {
259+
const overlayCtrl = overlays.add(
260+
new GlobalOverlayController({
261+
viewportConfig: {
262+
placement: 'center',
263+
},
264+
hasBackdrop: true,
265+
trapsKeyboardFocus: true,
266+
contentTemplate: () => html`
267+
<div class="demo-overlay">
268+
<p>Overlay placement: ${placement}</p>
269+
<button @click="${() => overlayCtrl.hide()}">Close</button>
270+
</div>
271+
`,
272+
}),
273+
);
274+
275+
return html`
276+
<style>
277+
${globalOverlayDemoStyle}
278+
</style>
279+
<button @click=${() => togglePlacement(overlayCtrl)}>Change placement</button>
280+
<button
281+
@click="${event => overlayCtrl.show(event.target)}"
282+
aria-haspopup="dialog"
283+
aria-expanded="false"
284+
>
285+
Open overlay
286+
</button>
287+
`;
288+
})
246289
.add('Sync', () => {
247290
const overlayCtrl = overlays.add(
248291
new GlobalOverlayController({

packages/overlays/stories/modal-dialog.stories.js

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,15 +6,8 @@ import { overlays, ModalDialogController } from '../index.js';
66
const modalDialogDemoStyle = css`
77
.demo-overlay {
88
background-color: white;
9-
position: absolute;
10-
top: 20px;
11-
left: 20px;
129
width: 200px;
13-
border: 1px solid blue;
14-
}
15-
16-
.demo-overlay--2 {
17-
left: 240px;
10+
border: 1px solid lightgrey;
1811
}
1912
`;
2013

@@ -23,7 +16,7 @@ storiesOf('Global Overlay System|Modal Dialog', module)
2316
const nestedDialogCtrl = overlays.add(
2417
new ModalDialogController({
2518
contentTemplate: () => html`
26-
<div class="demo-overlay demo-overlay--2">
19+
<div class="demo-overlay">
2720
<p>Nested modal dialog</p>
2821
<button @click="${() => nestedDialogCtrl.hide()}">Close</button>
2922
</div>

packages/overlays/test/GlobalOverlayController.test.js

Lines changed: 62 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,18 @@ describe('GlobalOverlayController', () => {
7676
it('removes the overlay from DOM when hiding', async () => {
7777
const ctrl = overlays.add(
7878
new GlobalOverlayController({
79+
viewportConfig: {
80+
placement: 'top-left',
81+
},
7982
contentTemplate: () => html`
80-
<p>Content</p>
83+
<div>Content</div>
8184
`,
8285
}),
8386
);
8487

8588
await ctrl.show();
8689
expect(getRenderedContainers().length).to.equal(1);
87-
expect(getRenderedOverlay(0).tagName).to.equal('P');
90+
expect(getRenderedOverlay(0).tagName).to.equal('DIV');
8891
expect(getRenderedOverlay(0).textContent).to.equal('Content');
8992
expect(getTopContainer()).to.equal(getRenderedContainer(0));
9093

@@ -111,34 +114,6 @@ describe('GlobalOverlayController', () => {
111114
expect(ctrl.isShown).to.equal(false);
112115
});
113116

114-
it('puts the latest shown overlay always on top', async () => {
115-
const controller0 = overlays.add(
116-
new GlobalOverlayController({
117-
contentTemplate: () => html`
118-
<p>Content0</p>
119-
`,
120-
}),
121-
);
122-
const controller1 = overlays.add(
123-
new GlobalOverlayController({
124-
contentTemplate: () => html`
125-
<p>Content1</p>
126-
`,
127-
}),
128-
);
129-
130-
await controller0.show();
131-
await controller1.show();
132-
await controller0.show();
133-
134-
expect(getRenderedContainers().length).to.equal(2);
135-
expect(getRenderedOverlay(0).tagName).to.equal('P');
136-
expect(getRenderedOverlay(0).textContent).to.equal('Content0');
137-
expect(getRenderedOverlay(1).tagName).to.equal('P');
138-
expect(getRenderedOverlay(1).textContent).to.equal('Content1');
139-
expect(getTopOverlay().textContent).to.equal('Content0');
140-
});
141-
142117
it('does not recreate the overlay elements when calling show multiple times', async () => {
143118
const ctrl = overlays.add(
144119
new GlobalOverlayController({
@@ -185,6 +160,9 @@ describe('GlobalOverlayController', () => {
185160
it('focuses body when hiding by default', async () => {
186161
const ctrl = overlays.add(
187162
new GlobalOverlayController({
163+
viewportConfig: {
164+
placement: 'top-left',
165+
},
188166
contentTemplate: () => html`
189167
<div><input />=</div>
190168
`,
@@ -208,6 +186,9 @@ describe('GlobalOverlayController', () => {
208186
const ctrl = overlays.add(
209187
new GlobalOverlayController({
210188
elementToFocusAfterHide: input,
189+
viewportConfig: {
190+
placement: 'top-left',
191+
},
211192
contentTemplate: () => html`
212193
<div><textarea></textarea></div>
213194
`,
@@ -230,6 +211,9 @@ describe('GlobalOverlayController', () => {
230211

231212
const ctrl = overlays.add(
232213
new GlobalOverlayController({
214+
viewportConfig: {
215+
placement: 'top-left',
216+
},
233217
contentTemplate: () => html`
234218
<div><textarea></textarea></div>
235219
`,
@@ -252,6 +236,9 @@ describe('GlobalOverlayController', () => {
252236

253237
const ctrl = overlays.add(
254238
new GlobalOverlayController({
239+
viewportConfig: {
240+
placement: 'top-left',
241+
},
255242
contentTemplate: () => html`
256243
<div><textarea></textarea></div>
257244
`,
@@ -351,4 +338,49 @@ describe('GlobalOverlayController', () => {
351338
expect(ctrl.backdropNode).to.have.class('global-overlays__backdrop');
352339
});
353340
});
341+
342+
describe('viewportConfig', () => {
343+
it('places the overlay in center by default', async () => {
344+
const controller = new GlobalOverlayController({
345+
contentTemplate: () =>
346+
html`
347+
<p>Content</p>
348+
`,
349+
});
350+
351+
controller.show();
352+
expect(controller.overlayContainerClass).to.equal(
353+
'global-overlays__overlay-container--center',
354+
);
355+
});
356+
357+
it('can set the placement relative to the viewport ', async () => {
358+
const placementMap = [
359+
'top-left',
360+
'top',
361+
'top-right',
362+
'right',
363+
'bottom-right',
364+
'bottom',
365+
'bottom-left',
366+
'left',
367+
'center',
368+
];
369+
placementMap.forEach(viewportPlacement => {
370+
const controller = new GlobalOverlayController({
371+
viewportConfig: {
372+
placement: viewportPlacement,
373+
},
374+
contentTemplate: () =>
375+
html`
376+
<p>Content</p>
377+
`,
378+
});
379+
controller.show();
380+
expect(controller.overlayContainerClass).to.equal(
381+
`global-overlays__overlay-container--${viewportPlacement}`,
382+
);
383+
});
384+
});
385+
});
354386
});

packages/overlays/test/ModalDialogController.test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,6 @@ describe('ModalDialogController', () => {
2525
expect(ctrl.preventsScroll).to.be.true;
2626
expect(ctrl.trapsKeyboardFocus).to.be.true;
2727
expect(ctrl.hidesOnEsc).to.be.true;
28+
expect(ctrl.overlayContainerClass).to.equal('global-overlays__overlay-container--center');
2829
});
2930
});

0 commit comments

Comments
 (0)