Skip to content

Commit 22357ea

Browse files
Joren BroekemaJoren Broekema
authored andcommitted
feat(overlays): base LocalOverlay positioning system on Popper.js
1 parent 187d50b commit 22357ea

File tree

14 files changed

+395
-1512
lines changed

14 files changed

+395
-1512
lines changed

packages/overlays/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@
3232
"*.js"
3333
],
3434
"dependencies": {
35-
"@lion/core": "^0.1.9"
35+
"@lion/core": "^0.1.5",
36+
"popper.js": "^1.15.0"
3637
},
3738
"devDependencies": {
3839
"@open-wc/demoing-storybook": "^0.2.0",

packages/overlays/src/LocalOverlayController.js

Lines changed: 84 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,37 @@
11
import { render, html } from '@lion/core';
2-
import { managePosition } from './utils/manage-position.js';
32
import { containFocus } from './utils/contain-focus.js';
43
import { keyCodes } from './utils/key-codes.js';
54

5+
async function __preloadPopper() {
6+
return import('popper.js/dist/popper.min.js');
7+
}
68
export class LocalOverlayController {
79
constructor(params = {}) {
8-
const finalParams = {
9-
placement: 'top',
10-
position: 'absolute',
11-
...params,
12-
};
13-
this.hidesOnEsc = finalParams.hidesOnEsc;
14-
this.hidesOnOutsideClick = finalParams.hidesOnOutsideClick;
15-
this.trapsKeyboardFocus = finalParams.trapsKeyboardFocus;
16-
this.placement = finalParams.placement;
17-
this.position = finalParams.position;
10+
// TODO: Instead of in constructor, prefetch it or use a preloader-manager to load it during idle time
11+
this.constructor.popperModule = __preloadPopper();
12+
this.__mergePlacementConfigs(params.placementConfig || {});
13+
14+
this.hidesOnEsc = params.hidesOnEsc;
15+
this.hidesOnOutsideClick = params.hidesOnOutsideClick;
16+
this.trapsKeyboardFocus = params.trapsKeyboardFocus;
17+
1818
/**
1919
* A wrapper to render into the invokerTemplate
2020
*
2121
* @property {HTMLElement}
2222
*/
2323
this.invoker = document.createElement('div');
2424
this.invoker.style.display = 'inline-block';
25-
this.invokerTemplate = finalParams.invokerTemplate;
25+
this.invokerTemplate = params.invokerTemplate;
26+
2627
/**
2728
* The actual invoker element we work with - it get's all the events and a11y
2829
*
2930
* @property {HTMLElement}
3031
*/
3132
this.invokerNode = this.invoker;
32-
if (finalParams.invokerNode) {
33-
this.invokerNode = finalParams.invokerNode;
33+
if (params.invokerNode) {
34+
this.invokerNode = params.invokerNode;
3435
this.invoker = this.invokerNode;
3536
}
3637

@@ -41,10 +42,10 @@ export class LocalOverlayController {
4142
*/
4243
this.content = document.createElement('div');
4344
this.content.style.display = 'inline-block';
44-
this.contentTemplate = finalParams.contentTemplate;
45+
this.contentTemplate = params.contentTemplate;
4546
this.contentNode = this.content;
46-
if (finalParams.contentNode) {
47-
this.contentNode = finalParams.contentNode;
47+
if (params.contentNode) {
48+
this.contentNode = params.contentNode;
4849
this.content = this.contentNode;
4950
}
5051

@@ -94,8 +95,17 @@ export class LocalOverlayController {
9495
/**
9596
* Shows the overlay.
9697
*/
97-
show() {
98+
async show() {
9899
this._createOrUpdateOverlay(true, this._prevData);
100+
/**
101+
* Popper is weird about properly positioning the popper element when its is recreated so
102+
* we just recreate the popper instance to make it behave like it should.
103+
* Probably related to this issue: https://github.com/FezVrasta/popper.js/issues/796
104+
* calling just the .update() function on the popper instance sadly does not resolve this.
105+
* This is however necessary for initial placement.
106+
*/
107+
await this.__createPopperInstance();
108+
this._popper.update();
99109
}
100110

101111
/**
@@ -113,6 +123,13 @@ export class LocalOverlayController {
113123
this.isShown ? this.hide() : this.show();
114124
}
115125

126+
// Popper does not export a nice method to update an existing instance with a new config. Therefore we recreate the instance.
127+
// TODO: Send a merge request to Popper to abstract their logic in the constructor to an exposed method which takes in the user config.
128+
async updatePlacementConfig(config = {}) {
129+
this.__mergePlacementConfigs(config);
130+
await this.__createPopperInstance();
131+
}
132+
116133
_createOrUpdateOverlay(shown = this._prevShown, data = this._prevData) {
117134
if (shown) {
118135
this._contentData = { ...this._contentData, ...data };
@@ -122,15 +139,10 @@ export class LocalOverlayController {
122139
render(this.contentTemplate(this._contentData), this.content);
123140
this.contentNode = this.content.firstElementChild;
124141
}
125-
this.contentNode.style.display = 'inline-block';
126142
this.contentNode.id = this.contentId;
143+
this.contentNode.style.display = 'inline-block';
127144
this.invokerNode.setAttribute('aria-expanded', true);
128145

129-
managePosition(this.contentNode, this.invokerNode, {
130-
placement: this.placement,
131-
position: this.position,
132-
});
133-
134146
if (this.trapsKeyboardFocus) this._setupTrapsKeyboardFocus();
135147
if (this.hidesOnOutsideClick) this._setupHidesOnOutsideClick();
136148
if (this.hidesOnEsc) this._setupHidesOnEsc();
@@ -214,4 +226,52 @@ export class LocalOverlayController {
214226
this.hide();
215227
}
216228
}
229+
230+
/**
231+
* Merges the default config with the current config, and finally with the user supplied config
232+
* @param {Object} config user supplied configuration
233+
*/
234+
__mergePlacementConfigs(config = {}) {
235+
this.placementConfig = {
236+
placement: 'top',
237+
positionFixed: false,
238+
...(this.placementConfig || {}),
239+
...(config || {}),
240+
modifiers: {
241+
keepTogether: {
242+
enabled: false,
243+
},
244+
preventOverflow: {
245+
enabled: true,
246+
boundariesElement: 'viewport',
247+
padding: 16, // viewport-margin for shifting/sliding
248+
},
249+
flip: {
250+
boundariesElement: 'viewport',
251+
padding: 16, // viewport-margin for flipping
252+
},
253+
offset: {
254+
enabled: true,
255+
offset: `0, 8px`, // horizontal and vertical margin (distance between popper and referenceElement)
256+
},
257+
arrow: {
258+
enabled: false,
259+
},
260+
...((this.placementConfig && this.placementConfig.modifiers) || {}),
261+
...((config && config.modifiers) || {}),
262+
},
263+
};
264+
}
265+
266+
async __createPopperInstance() {
267+
if (this._popper) {
268+
this._popper.destroy();
269+
this._popper = null;
270+
}
271+
const mod = await this.constructor.popperModule;
272+
const Popper = mod.default;
273+
this._popper = new Popper(this.invokerNode, this.contentNode, {
274+
...this.placementConfig,
275+
});
276+
}
217277
}

packages/overlays/src/utils/async.js

Lines changed: 0 additions & 207 deletions
This file was deleted.

0 commit comments

Comments
 (0)