11import { render , html } from '@lion/core' ;
2- import { managePosition } from './utils/manage-position.js' ;
32import { containFocus } from './utils/contain-focus.js' ;
43import { keyCodes } from './utils/key-codes.js' ;
54
5+ async function __preloadPopper ( ) {
6+ return import ( 'popper.js/dist/popper.min.js' ) ;
7+ }
68export 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}
0 commit comments