ember-attacher 1.3.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,102 +1,149 @@
1
- import classic from 'ember-classic-decorator';
2
- import { tagName, layout as templateLayout } from '@ember-decorators/component';
3
- import { observes } from '@ember-decorators/object';
4
- import { action, computed } from '@ember/object';
5
- import Component from '@ember/component';
6
- import DEFAULTS from '../defaults';
7
- import layout from '../templates/components/attach-popover';
1
+ import { action } from '@ember/object';
2
+ import Component from '@glimmer/component';
8
3
  import { cancel, debounce, later, next, run } from '@ember/runloop';
9
4
  import { getOwner } from '@ember/application';
10
5
  import { guidFor } from '@ember/object/internals';
11
- import { htmlSafe, isHTMLSafe } from '@ember/string';
6
+ import { htmlSafe, isHTMLSafe } from '@ember/template';
12
7
  import { stripInProduction } from 'ember-attacher/-debug/helpers';
13
- import { warn } from '@ember/debug';
14
- import { isEmpty } from '@ember/utils';
8
+ import { warn, assert } from '@ember/debug';
9
+ import { isEmpty, typeOf } from '@ember/utils';
10
+ import { autoUpdate, computePosition, arrow, flip } from '@floating-ui/dom';
11
+ import { buildWaiter } from '@ember/test-waiters';
12
+ import { tracked } from '@glimmer/tracking';
13
+ import DEFAULTS from '../defaults';
14
+
15
+ const animationTestWaiter = buildWaiter('attach-popover');
15
16
 
16
- @classic
17
- @templateLayout(layout)
18
- @tagName('')
19
17
  export default class AttachPopover extends Component {
18
+ @tracked parentNotFound = true;
19
+ @tracked parentElement = null;
20
+ @tracked _isStartingAnimation = false;
21
+ @tracked _arrowElement = null;
22
+ @tracked _currentTarget = null;
23
+ // This is set to true when the popover is shown in order to override lazyRender=false
24
+ @tracked _mustRender = false;
25
+ @tracked _transitionDuration = 0;
26
+ _floatingElement = null;
20
27
  configKey = 'popover';
28
+
21
29
  /**
22
30
  * ================== PUBLIC CONFIG OPTIONS ==================
23
31
  */
32
+ get arrow() {
33
+ return this.args.arrow ?? this._config.arrow ?? DEFAULTS.arrow;
34
+ }
24
35
 
25
- animation = DEFAULTS.animation;
26
-
27
- arrow = DEFAULTS.arrow;
28
- flip = DEFAULTS.flip;
29
- hideDelay = DEFAULTS.hideDelay;
30
- hideDuration = DEFAULTS.hideDuration;
31
- hideOn = DEFAULTS.hideOn;
32
- interactive = DEFAULTS.interactive;
33
- isOffset = DEFAULTS.isOffset;
34
- isShown = DEFAULTS.isShown;
35
- lazyRender = DEFAULTS.lazyRender;
36
- onChange = null;
37
- placement = DEFAULTS.placement;
38
- popperContainer = DEFAULTS.popperContainer;
39
- popperOptions = DEFAULTS.popperOptions;
40
- popperTarget = null;
41
- renderInPlace = DEFAULTS.renderInPlace;
42
- showDelay = DEFAULTS.showDelay;
43
- showDuration = DEFAULTS.showDuration;
44
- showOn = DEFAULTS.showOn;
45
- style = DEFAULTS.style;
46
- useCapture = DEFAULTS.useCapture;
36
+ get autoUpdate() {
37
+ return this.args.autoUpdate ?? this._config.autoUpdate ?? DEFAULTS.autoUpdate;
38
+ }
47
39
 
48
- // Exposed via the named yield to enable custom hide events
49
- @action
50
- hide() {
51
- this._hide();
40
+ get animation() {
41
+ return this.args.animation || this._config.animation || DEFAULTS.animation;
52
42
  }
53
43
 
54
- @action
55
- _registerAPI(api) {
56
- this._disableEventListeners = api.disableEventListeners;
57
- this._enableEventListeners = api.enableEventListeners;
58
- this._popperElement = api.popperElement;
59
- this._update = api.update;
60
-
61
- if (!this.isDestroying && !this.isDestroyed) {
62
- if (this.registerAPI !== undefined) {
63
- this.registerAPI(api);
64
- }
44
+ get flip() {
45
+ return this.args.flip ?? this._config.flip ?? DEFAULTS.flip;
46
+ }
65
47
 
66
- if (this._isHidden) {
67
- // Hide the attachment until it has been positioned,
68
- // preventing jank during initial positioning
69
- this._popperElement.style.visibility = 'hidden';
48
+ get hideDelay() {
49
+ return this.args.hideDelay ?? this._config.hideDelay ?? DEFAULTS.hideDelay;
50
+ }
70
51
 
71
- // The attachment has no width if initially hidden. This can cause it to be positioned so
72
- // far to the right that it overflows the screen until enough updates fix its position.
73
- // We avoid this by positioning initially hidden elements in the top left of the screen.
74
- // The attachment will then correctly update its position from the first this._show()
75
- this._popperElement.style.transform = null;
52
+ get hideDuration() {
53
+ return this.args.hideDuration ?? this._config.hideDuration ?? DEFAULTS.hideDuration;
54
+ }
76
55
 
77
- this._popperElement.style.display = this.isShown ? '' : 'none';
78
- }
56
+ get hideOn() {
57
+ return this.args.hideOn || this._config.hideOn || DEFAULTS.hideOn;
58
+ }
59
+
60
+ get interactive() {
61
+ return this.args.interactive ?? this._config.interactive ?? DEFAULTS.interactive;
62
+ }
63
+
64
+ get isOffset() {
65
+ return this.args.isOffset ?? this._config.isOffset ?? DEFAULTS.interactive;
66
+ }
67
+
68
+ get isShown() {
69
+ return this.args.isShown ?? this._config.isShown ?? DEFAULTS.isShown;
70
+ }
71
+
72
+ get lazyRender() {
73
+ return this.args.lazyRender ?? this._config.lazyRender ?? DEFAULTS.lazyRender;
74
+ }
75
+
76
+ get placement() {
77
+ return this.args.placement ?? this._config.placement ?? DEFAULTS.placement;
78
+ }
79
+
80
+ get floatingElementContainer() {
81
+ return this.args.floatingElementContainer || this._config.floatingElementContainer || DEFAULTS.floatingElementContainer;
82
+ }
83
+
84
+ get class() {
85
+ return this.args.class || this._config.class;
86
+ }
87
+
88
+ get floatingUiOptions() {
89
+ return this.args.floatingUiOptions || this._config.floatingUiOptions || DEFAULTS.floatingUiOptions;
90
+ }
91
+
92
+ get renderInPlace() {
93
+ return this.args.renderInPlace ?? this._config.renderInPlace ?? DEFAULTS.renderInPlace;
94
+ }
95
+
96
+ get showDelay() {
97
+ return this.args.showDelay ?? this._config.showDelay ?? DEFAULTS.showDelay;
98
+ }
99
+
100
+ get showDuration() {
101
+ return this.args.showDuration ?? this._config.showDuration ?? DEFAULTS.showDuration;
102
+ }
103
+
104
+ get showOn() {
105
+ if (this.args.showOn === null) {
106
+ return null;
79
107
  }
108
+
109
+ return this.args.showOn ?? this._config.showOn ?? DEFAULTS.showOn;
80
110
  }
81
111
 
82
- // The circle element needs a special duration that is slightly faster than the popper's
112
+ get style() {
113
+ return this.args.style ?? this._config.style ?? DEFAULTS.style;
114
+ }
115
+
116
+ get useCapture() {
117
+ return this.args.useCapture ?? this._config.useCapture ?? DEFAULTS.useCapture;
118
+ }
119
+
120
+ get isFillAnimation() {
121
+ return this.animation === 'fill';
122
+ }
123
+
124
+ get renderFloatingElement() {
125
+ return (this.renderInPlace || this._currentTarget) && (!this.lazyRender || this._mustRender);
126
+ }
127
+
128
+ get id() {
129
+ return this.args.id || `${guidFor(this)}-floating`;
130
+ }
131
+
132
+ // The circle element needs a special duration that is slightly faster than the floating element's
83
133
  // transition, this prevents text from appearing outside the circle as it fills the background
84
- @computed('_transitionDuration')
85
134
  get _circleTransitionDuration() {
86
135
  return htmlSafe(
87
136
  `transition-duration: ${Math.round(this._transitionDuration / 1.25)}ms`
88
137
  );
89
138
  }
90
139
 
91
- @computed('class', 'arrow', 'animation', '_isStartingAnimation')
92
140
  get _class() {
93
141
  const showOrHideClass = `ember-attacher-${this._isStartingAnimation ? 'show' : 'hide'}`;
94
142
  const arrowClass = `ember-attacher-${this.arrow ? 'with' : 'without'}-arrow`;
95
143
 
96
- return `ember-attacher-${this.animation} ${this.class || ''} ${showOrHideClass} ${arrowClass}`;
144
+ return [`ember-attacher-${this.animation}`, this.class || '', showOrHideClass, arrowClass].filter(Boolean).join(' ');
97
145
  }
98
146
 
99
- @computed('style', '_transitionDuration')
100
147
  get _style() {
101
148
  const style = this.style;
102
149
  const transitionDuration = this._transitionDuration;
@@ -114,7 +161,6 @@ export default class AttachPopover extends Component {
114
161
  return getOwner(this).resolveRegistration('config:environment').emberAttacher || {};
115
162
  }
116
163
 
117
- @computed('_envConfig', 'configKey')
118
164
  get _config() {
119
165
  return {
120
166
  ...this._envConfig,
@@ -122,7 +168,6 @@ export default class AttachPopover extends Component {
122
168
  };
123
169
  }
124
170
 
125
- @computed('hideOn')
126
171
  get _hideOn() {
127
172
  let hideOn = this.hideOn;
128
173
 
@@ -133,63 +178,89 @@ export default class AttachPopover extends Component {
133
178
  return hideOn === null ? [] : hideOn.split(' ');
134
179
  }
135
180
 
136
- @computed('arrow', 'flip', 'modifiers')
137
- get _modifiers() {
138
- // Copy the modifiers since we might write to the provided hash
139
- const modifiers
140
- = this.modifiers ? Object.assign({}, this.modifiers) : {};
181
+ get _middleware() {
182
+ // Copy the middleware since we might write to the provided array
183
+ const middleware
184
+ = this.args.middleware ? [...this.args.middleware] : [];
141
185
 
142
- const arrow = this.arrow;
143
- if (typeof(arrow) === 'boolean' && !modifiers.arrow) {
144
- modifiers.arrow = { enabled: arrow };
186
+ if (this.arrow && this._arrowElement && !middleware.find(name => name === 'arrow')) {
187
+ middleware.push(arrow({ element: this._arrowElement }));
145
188
  }
146
189
 
147
190
  const flipString = this.flip;
148
191
  if (flipString) {
149
- const flipModifier = { behavior: flipString.split(' ') };
150
- if (!modifiers.flip) {
151
- modifiers.flip = flipModifier;
152
- } else if (!modifiers.flip.behavior) {
153
- // Copy the flip modifier since we are altering the provided hash
154
- modifiers.flip = Object.assign({}, modifiers.flip, flipModifier);
192
+ const flipOptions = { fallbackPlacements: flipString.split(' ') };
193
+ const flipMiddleware = middleware.find(name => name === 'flip');
194
+ if (!flipMiddleware) {
195
+ middleware.push(flip(flipOptions));
196
+ } else if (!flipMiddleware.fallbackPlacements) {
197
+ Object.assign({}, flipMiddleware, flipOptions);
155
198
  }
156
199
  }
157
200
 
158
- return modifiers;
201
+ return middleware;
202
+ }
203
+
204
+ get _floatingElementContainer() {
205
+ const maybeContainer = this.floatingElementContainer;
206
+ const renderInPlace = this._renderInPlace;
207
+ let floatingElementContainer;
208
+
209
+ if (renderInPlace) {
210
+ floatingElementContainer = this.parentElement;
211
+ } else if (maybeContainer instanceof Element) {
212
+ floatingElementContainer = maybeContainer;
213
+ } else if (typeof maybeContainer === 'string') {
214
+ const selector = maybeContainer;
215
+ const possibleContainers = self.document.querySelectorAll(selector);
216
+
217
+ assert(`floatingElementContainer selector "${selector}" found `
218
+ + `${possibleContainers.length} possible containers when there should be exactly 1`, possibleContainers.length === 1);
219
+
220
+ floatingElementContainer = possibleContainers[0];
221
+ }
222
+
223
+ return floatingElementContainer;
224
+ }
225
+
226
+ get _renderInPlace() {
227
+ // self.document is undefined in Fastboot, so we have to render in
228
+ // place for the floating element to show up at all.
229
+ return self.document ? !!this.renderInPlace : true;
159
230
  }
160
231
 
161
232
  _setIsVisibleAfterDelay(isVisible, delay) {
162
- if (!this._popperElement) {
233
+ if (!this._floatingElement) {
163
234
  this._animationTimeout = requestAnimationFrame(() => {
164
- this._animationTimeout = this._setIsVisibleAfterDelay(isVisible, delay);
235
+ this._setIsVisibleAfterDelay(isVisible, delay);
165
236
  });
166
-
167
237
  return;
168
238
  }
169
- const onChange = this.onChange;
239
+ const onChange = this.args.onChange;
170
240
 
171
241
  if (delay) {
172
242
  this._delayedVisibilityToggle = later(this, () => {
173
243
  this._animationTimeout = requestAnimationFrame(() => {
244
+ animationTestWaiter.endAsync(this._animationTimeout);
174
245
  if (!this.isDestroyed && !this.isDestroying) {
175
- this._popperElement.style.display = isVisible ? '' : 'none';
246
+ this._floatingElement.style.display = isVisible ? 'block' : 'none';
176
247
 
177
248
  // Prevent jank by making the attachment invisible until positioned.
178
249
  // The visibility style will be toggled by this._startShowAnimation()
179
- this._popperElement.style.visibility = isVisible ? 'hidden' : '';
250
+ this._floatingElement.style.visibility = isVisible ? 'hidden' : '';
180
251
 
181
252
  if (onChange) {
182
253
  onChange(isVisible);
183
254
  }
184
255
  }
185
256
  });
257
+ animationTestWaiter.beginAsync(this._animationTimeout);
186
258
  }, delay);
187
259
  } else {
188
- this._popperElement.style.display = isVisible ? '' : 'none';
189
-
260
+ this._floatingElement.style.display = isVisible ? 'block' : 'none';
190
261
  // Prevent jank by making the attachment invisible until positioned.
191
262
  // The visibility style will be toggled by this._startShowAnimation()
192
- this._popperElement.style.visibility = isVisible ? 'hidden' : '';
263
+ this._floatingElement.style.visibility = isVisible ? 'hidden' : '';
193
264
 
194
265
  if (onChange) {
195
266
  onChange(isVisible);
@@ -197,10 +268,6 @@ export default class AttachPopover extends Component {
197
268
  }
198
269
  }
199
270
 
200
- // This is set to true when the popover is shown in order to override lazyRender=false
201
- _mustRender = false;
202
-
203
- @computed('showOn')
204
271
  get _showOn() {
205
272
  let showOn = this.showOn;
206
273
 
@@ -211,26 +278,41 @@ export default class AttachPopover extends Component {
211
278
  return showOn === null ? [] : showOn.split(' ');
212
279
  }
213
280
 
214
- _transitionDuration = 0;
281
+ // Exposed via the named yield to enable custom hide events
282
+ @action
283
+ hide() {
284
+ this._hide();
285
+ }
215
286
 
216
- /**
217
- * ================== LIFECYCLE HOOKS ==================
218
- */
287
+ @action
288
+ onParentFinderInsert(element) {
289
+ this.parentElement = element.parentElement;
290
+ this._initializeAttacher();
291
+ }
219
292
 
220
- init() {
221
- super.init(...arguments);
293
+ @action
294
+ _ensureArgumentsAreValid() {
295
+ stripInProduction(() => {
296
+ if (this.arrow && this.isFillAnimation) {
297
+ warn('Animation: \'fill\' is not compatible with arrow: true', { id: 70015 });
298
+ }
299
+
300
+ if (this.useCapture !== this._lastUseCaptureArgumentValue) {
301
+ warn(
302
+ 'The value of the useCapture argument was mutated',
303
+ { id: 'ember-attacher.use-capture-mutated' }
304
+ );
305
+ }
306
+ });
307
+ }
222
308
 
223
- // Used to determine the attachments initial parent element
224
- this._parentFinder = self.document ? self.document.createTextNode('') : '';
225
309
 
226
- // Holds the current popper target so event listeners can be removed if the target changes
227
- this._currentTarget = null;
310
+ constructor() {
311
+ super(...arguments);
228
312
 
229
313
  // The debounced _hide() and _show() are stored here so they can be cancelled when necessary
230
314
  this._delayedVisibilityToggle = null;
231
315
 
232
- this.id = this.id || `${guidFor(this)}-popper`;
233
-
234
316
  // The final source of truth on whether or not all _hide() or _show() actions have completed
235
317
  this._isHidden = true;
236
318
 
@@ -257,97 +339,12 @@ export default class AttachPopover extends Component {
257
339
  this._show = this._show.bind(this);
258
340
  this._showAfterDelay = this._showAfterDelay.bind(this);
259
341
 
260
- this._setUserSuppliedDefaults();
261
- }
262
-
263
- didReceiveAttrs() {
264
- super.didReceiveAttrs(...arguments);
265
-
266
- stripInProduction(() => {
267
- // eslint-disable-next-line ember/no-attrs-in-components
268
- const attrs = this.attrs || {};
269
- const userDefaults = this._config;
270
-
271
- let arrow;
272
- if (attrs.arrow !== undefined) {
273
- arrow = attrs.arrow.value;
274
- } else if (userDefaults.arrow !== undefined) {
275
- arrow = userDefaults.arrow;
276
- } else {
277
- arrow = DEFAULTS.arrow;
278
- }
279
-
280
- let animation;
281
- if (attrs.animation !== undefined) {
282
- animation = attrs.animation.value;
283
- } else if (userDefaults.animation !== undefined) {
284
- animation = userDefaults.animation;
285
- } else {
286
- animation = DEFAULTS.animation;
287
- }
288
-
289
- if (arrow && animation === 'fill') {
290
- warn('Animation: \'fill\' is not compatible with arrow: true', { id: 70015 });
291
- }
292
-
293
- this._lastUseCaptureArgumentValue = this.useCapture;
294
- });
295
- }
296
-
297
- didUpdateAttrs() {
298
- super.didUpdateAttrs(...arguments);
299
-
300
- stripInProduction(() => {
301
- if (this.useCapture !== this._lastUseCaptureArgumentValue) {
302
- warn(
303
- 'The value of the useCapture argument was mutated',
304
- { id: 'ember-attacher.use-capture-mutated' }
305
- );
306
- }
307
- })
308
- }
309
-
310
- _setUserSuppliedDefaults() {
311
- const userDefaults = this._config;
312
-
313
- // Exit early if no custom defaults are found
314
- if (!Object.keys(userDefaults).length) {
315
- return;
316
- }
317
-
318
- // eslint-disable-next-line ember/no-attrs-in-components
319
- const attrs = this.attrs || {};
320
-
321
- for (const key in userDefaults) {
322
- stripInProduction(() => {
323
- // eslint-disable-next-line no-prototype-builtins
324
- if (!['popover','tooltip', 'class'].includes(key) && !DEFAULTS.hasOwnProperty(key)) {
325
- warn(`Unknown property given as an ember-attacher default: ${key}`, { id: 700152 });
326
- }
327
- });
328
-
329
- // Don't override attrs manually passed into the component
330
- if (attrs[key] === undefined) {
331
- if (key === 'arrow') {
332
- this.set('arrow', userDefaults[key]);
333
- } else {
334
- this[key] = userDefaults[key];
335
- }
336
- }
337
- }
338
- }
339
-
340
- didInsertElement() {
341
- super.didInsertElement(...arguments);
342
-
343
- this._initializeAttacher();
342
+ this._lastUseCaptureArgumentValue = this.useCapture;
344
343
  }
345
344
 
346
345
  _initializeAttacher() {
347
346
  this._removeEventListeners();
348
-
349
- this.set('_currentTarget', this.popperTarget || this._parentFinder.parentNode);
350
-
347
+ this._currentTarget = this.args.explicitTarget || this.parentElement;
351
348
  this._addListenersForShowEvents();
352
349
 
353
350
  if (!this._isHidden || this.isShown) {
@@ -360,7 +357,6 @@ export default class AttachPopover extends Component {
360
357
  }
361
358
 
362
359
  _addListenersForShowEvents() {
363
-
364
360
  if (!this._currentTarget) {
365
361
  return;
366
362
  }
@@ -372,10 +368,10 @@ export default class AttachPopover extends Component {
372
368
  });
373
369
  }
374
370
 
375
- willDestroyElement() {
376
- super.willDestroyElement(...arguments);
371
+ willDestroy() {
372
+ super.willDestroy(...arguments);
377
373
 
378
- cancelAnimationFrame(this._animationTimeout);
374
+ this._cancelAnimation();
379
375
  cancel(this._delayedVisibilityToggle);
380
376
 
381
377
  this._removeEventListeners();
@@ -386,7 +382,6 @@ export default class AttachPopover extends Component {
386
382
  document.removeEventListener(eventType, this._hideListenersOnDocumentByEvent[eventType], this.useCapture);
387
383
  delete this._hideListenersOnDocumentByEvent[eventType];
388
384
  });
389
-
390
385
  if (!this._currentTarget) {
391
386
  return;
392
387
  }
@@ -399,13 +394,13 @@ export default class AttachPopover extends Component {
399
394
  });
400
395
  }
401
396
 
402
- @observes('hideOn', 'showOn', 'popperTarget')
403
- _targetOrTriggersChanged() {
397
+ @action
398
+ onTargetOrTriggerChange() {
404
399
  this._initializeAttacher();
405
400
  }
406
401
 
407
- @observes('isShown')
408
- _isShownChanged() {
402
+ @action
403
+ onIsShownChange() {
409
404
  const isShown = this.isShown;
410
405
 
411
406
  if (isShown === true && this._isHidden) {
@@ -426,7 +421,7 @@ export default class AttachPopover extends Component {
426
421
  _showAfterDelay() {
427
422
  cancel(this._delayedVisibilityToggle);
428
423
 
429
- this.set('_mustRender', true);
424
+ this._mustRender = true;
430
425
 
431
426
  this._addListenersForHideEvents();
432
427
 
@@ -436,13 +431,13 @@ export default class AttachPopover extends Component {
436
431
  }
437
432
 
438
433
  _show() {
439
- cancelAnimationFrame(this._animationTimeout);
434
+ this._cancelAnimation();
440
435
 
441
436
  if (!this._currentTarget) {
442
437
  return;
443
438
  }
444
439
 
445
- this.set('_mustRender', true);
440
+ this._mustRender = true;
446
441
 
447
442
  // Make the attachment visible immediately so transition animations can take place
448
443
  this._setIsVisibleAfterDelay(true, 0);
@@ -457,43 +452,44 @@ export default class AttachPopover extends Component {
457
452
  // All included animations set opaque: 0, so the attachment is still effectively hidden until
458
453
  // the final RAF occurs.
459
454
  this._animationTimeout = requestAnimationFrame(() => {
455
+ animationTestWaiter.endAsync(this._animationTimeout);
460
456
  if (this.isDestroyed || this.isDestroying || !this._currentTarget) {
461
457
  return;
462
458
  }
463
459
 
464
- const popperElement = this._popperElement;
460
+ const floatingElement = this._floatingElement;
465
461
 
466
462
  // Wait until the element is visible before continuing
467
- if (!popperElement || popperElement.style.display === 'none') {
468
- this._animationTimeout = this._startShowAnimation();
469
-
463
+ if (!floatingElement || floatingElement.style.display === 'none') {
464
+ this._startShowAnimation();
470
465
  return;
471
466
  }
472
467
 
473
- this._enableEventListeners();
474
468
  this._update();
475
469
 
476
470
  // Wait for the above positioning to take effect before starting the show animation,
477
471
  // else the positioning itself will be animated, causing animation glitches.
478
472
  this._animationTimeout = requestAnimationFrame(() => {
473
+ animationTestWaiter.endAsync(this._animationTimeout);
479
474
  if (this.isDestroyed || this.isDestroying || !this._currentTarget) {
480
475
  return;
481
476
  }
482
-
483
477
  run(() => {
484
478
  if (this.isDestroyed || this.isDestroying || !this._currentTarget) {
485
479
  return;
486
480
  }
487
- // Make the popper element visible now that it has been positioned
488
- popperElement.style.visibility = '';
489
- this.set('_transitionDuration', parseInt(this.showDuration));
490
- this.set('_isStartingAnimation', true);
491
- popperElement.setAttribute('aria-hidden', 'false');
481
+ // Make the floating element visible now that it has been positioned
482
+ floatingElement.style.visibility = '';
483
+ this._transitionDuration = parseInt(this.showDuration);
484
+ this._isStartingAnimation = true;
485
+ floatingElement.setAttribute('aria-hidden', 'false');
492
486
  });
493
487
 
494
488
  this._isHidden = false;
495
489
  });
490
+ animationTestWaiter.beginAsync(this._animationTimeout);
496
491
  });
492
+ animationTestWaiter.beginAsync(this._animationTimeout);
497
493
  }
498
494
 
499
495
  /**
@@ -509,18 +505,20 @@ export default class AttachPopover extends Component {
509
505
  }
510
506
 
511
507
  _hide() {
512
- if (!this._popperElement) {
508
+ if (!this._floatingElement) {
513
509
  this._animationTimeout = requestAnimationFrame(() => {
510
+ animationTestWaiter.endAsync(this._animationTimeout);
514
511
  this._animationTimeout = this._hide();
515
512
  });
513
+ animationTestWaiter.beginAsync(this._animationTimeout);
516
514
  return;
517
515
  }
518
516
 
519
- cancelAnimationFrame(this._animationTimeout);
517
+ this._cancelAnimation();
520
518
 
521
519
  this._removeListenersForHideEvents();
522
-
523
520
  this._animationTimeout = requestAnimationFrame(() => {
521
+ animationTestWaiter.endAsync(this._animationTimeout);
524
522
  // Avoid a race condition where we attempt to hide after the component is being destroyed.
525
523
  if (this.isDestroyed || this.isDestroying) {
526
524
  return;
@@ -533,18 +531,16 @@ export default class AttachPopover extends Component {
533
531
  return;
534
532
  }
535
533
 
536
- this.set('_transitionDuration', hideDuration);
537
- this.set('_isStartingAnimation', false);
538
- this._popperElement.setAttribute('aria-hidden', 'true');
539
-
534
+ this._transitionDuration = hideDuration;
535
+ this._isStartingAnimation = false;
536
+ this._floatingElement.setAttribute('aria-hidden', 'true');
540
537
  // Wait for any animations to complete before hiding the attachment
541
538
  this._setIsVisibleAfterDelay(false, hideDuration);
542
539
  });
543
540
 
544
- this._disableEventListeners();
545
-
546
541
  this._isHidden = true;
547
542
  });
543
+ animationTestWaiter.beginAsync(this._animationTimeout);
548
544
  }
549
545
 
550
546
  /**
@@ -628,10 +624,10 @@ export default class AttachPopover extends Component {
628
624
  return;
629
625
  }
630
626
 
631
- // If cursor is not on the attachment or target, hide the popover
627
+ // If cursor is not on the attachment or target, hide the floating element
632
628
  if (!target.contains(event.target)
633
629
  && !(this.isOffset && this._isCursorBetweenTargetAndAttachment(event))
634
- && (this._popperElement && !this._popperElement.contains(event.target))) {
630
+ && (this._floatingElement && !this._floatingElement.contains(event.target))) {
635
631
  // Remove this listener before hiding the attachment
636
632
  delete this._hideListenersOnDocumentByEvent.mousemove;
637
633
  document.removeEventListener('mousemove', this._hideIfMouseOutsideTargetOrAttachment, this.useCapture);
@@ -648,7 +644,7 @@ export default class AttachPopover extends Component {
648
644
 
649
645
  const { clientX, clientY } = event;
650
646
 
651
- const attachmentPosition = this._popperElement.getBoundingClientRect();
647
+ const attachmentPosition = this._floatingElement.getBoundingClientRect();
652
648
  const targetPosition = this._currentTarget.getBoundingClientRect();
653
649
 
654
650
  const isBetweenLeftAndRight = clientX > Math.min(attachmentPosition.left, targetPosition.left)
@@ -692,7 +688,7 @@ export default class AttachPopover extends Component {
692
688
  const targetReceivedClick = this._currentTarget.contains(event.target);
693
689
 
694
690
  if (this.interactive) {
695
- if (!targetReceivedClick && !this._popperElement.contains(event.target)) {
691
+ if (!targetReceivedClick && !this._floatingElement.contains(event.target)) {
696
692
  this._hideAfterDelay();
697
693
  }
698
694
  } else if (!targetReceivedClick) {
@@ -718,7 +714,7 @@ export default class AttachPopover extends Component {
718
714
  const targetContainsFocus = this._currentTarget.contains(event.relatedTarget);
719
715
 
720
716
  if (this.interactive) {
721
- if (!targetContainsFocus && !this._popperElement.contains(event.relatedTarget)) {
717
+ if (!targetContainsFocus && !this._floatingElement.contains(event.relatedTarget)) {
722
718
  this._hideAfterDelay();
723
719
  }
724
720
  } else if (!targetContainsFocus) {
@@ -762,4 +758,75 @@ export default class AttachPopover extends Component {
762
758
  }
763
759
  });
764
760
  }
761
+
762
+ @action
763
+ didInsertFloatingElement(floatingElement) {
764
+ this._floatingElement = floatingElement;
765
+
766
+ if (this.renderInPlace) {
767
+ this.parentElement = floatingElement.parentElement;
768
+ this._initializeAttacher();
769
+ }
770
+ }
771
+
772
+ @action
773
+ didInsertArrow(element) {
774
+ this._arrowElement = element;
775
+ }
776
+
777
+ @action
778
+ onOptionsChange() {
779
+ this._ensureArgumentsAreValid();
780
+ this._update();
781
+ }
782
+
783
+ @action
784
+ willDestroyFloatingElement() {
785
+ this._cleanup?.();
786
+ }
787
+
788
+ _update() {
789
+ this._cleanup?.();
790
+ if (this.autoUpdate) {
791
+ this._cleanup = autoUpdate(
792
+ this._currentTarget,
793
+ this._floatingElement,
794
+ this._updatePosition,
795
+ typeOf(this.autoUpdate) === 'object' ? this.autoUpdate : undefined
796
+ );
797
+ } else {
798
+ this._updatePosition();
799
+ }
800
+ }
801
+
802
+ @action
803
+ _updatePosition() {
804
+ const computePositionToken = animationTestWaiter.beginAsync();
805
+ computePosition(this._currentTarget, this._floatingElement, {
806
+ ...this.floatingUiOptions,
807
+ middleware: this._middleware,
808
+ placement: this.placement
809
+ }).then(({ x, y, placement, middlewareData }) => {
810
+ animationTestWaiter.endAsync(computePositionToken);
811
+ Object.assign(this._floatingElement.style, { left: `${x}px`, top: `${y}px`, });
812
+
813
+ if (middlewareData.arrow) {
814
+ const { x, y } = middlewareData.arrow;
815
+
816
+ Object.assign(this._arrowElement.style, {
817
+ left: x != null ? `${x}px` : '',
818
+ top: y != null ? `${y}px` : '',
819
+ });
820
+ }
821
+ this._floatingElement.setAttribute('x-placement', placement);
822
+ });
823
+ }
824
+
825
+ _cancelAnimation() {
826
+ cancelAnimationFrame(this._animationTimeout);
827
+
828
+ if (animationTestWaiter.items.get(this._animationTimeout)) {
829
+ animationTestWaiter.endAsync(this._animationTimeout);
830
+ }
831
+ }
765
832
  }