ember-attacher 1.3.0 → 2.0.2

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,153 @@
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, limitShift, shift } 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.isOffset;
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
+ get overflowPadding() {
133
+ return this.args.overflowPadding ?? this._config.overflowPadding ?? DEFAULTS.overflowPadding;
134
+ }
135
+
136
+ // The circle element needs a special duration that is slightly faster than the floating element's
83
137
  // transition, this prevents text from appearing outside the circle as it fills the background
84
- @computed('_transitionDuration')
85
138
  get _circleTransitionDuration() {
86
139
  return htmlSafe(
87
140
  `transition-duration: ${Math.round(this._transitionDuration / 1.25)}ms`
88
141
  );
89
142
  }
90
143
 
91
- @computed('class', 'arrow', 'animation', '_isStartingAnimation')
92
144
  get _class() {
93
145
  const showOrHideClass = `ember-attacher-${this._isStartingAnimation ? 'show' : 'hide'}`;
94
146
  const arrowClass = `ember-attacher-${this.arrow ? 'with' : 'without'}-arrow`;
95
147
 
96
- return `ember-attacher-${this.animation} ${this.class || ''} ${showOrHideClass} ${arrowClass}`;
148
+ return [`ember-attacher-${this.animation}`, this.class || '', showOrHideClass, arrowClass].filter(Boolean).join(' ');
97
149
  }
98
150
 
99
- @computed('style', '_transitionDuration')
100
151
  get _style() {
101
152
  const style = this.style;
102
153
  const transitionDuration = this._transitionDuration;
@@ -114,7 +165,6 @@ export default class AttachPopover extends Component {
114
165
  return getOwner(this).resolveRegistration('config:environment').emberAttacher || {};
115
166
  }
116
167
 
117
- @computed('_envConfig', 'configKey')
118
168
  get _config() {
119
169
  return {
120
170
  ...this._envConfig,
@@ -122,7 +172,6 @@ export default class AttachPopover extends Component {
122
172
  };
123
173
  }
124
174
 
125
- @computed('hideOn')
126
175
  get _hideOn() {
127
176
  let hideOn = this.hideOn;
128
177
 
@@ -133,63 +182,94 @@ export default class AttachPopover extends Component {
133
182
  return hideOn === null ? [] : hideOn.split(' ');
134
183
  }
135
184
 
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) : {};
141
-
142
- const arrow = this.arrow;
143
- if (typeof(arrow) === 'boolean' && !modifiers.arrow) {
144
- modifiers.arrow = { enabled: arrow };
145
- }
185
+ get _middleware() {
186
+ // Copy the middleware since we might write to the provided array
187
+ const middleware = this.args.middleware ? [...this.args.middleware] : [];
146
188
 
147
189
  const flipString = this.flip;
148
190
  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);
191
+ const flipOptions = { fallbackPlacements: flipString.split(' ') };
192
+ const flipMiddleware = middleware.find(name => name === 'flip');
193
+ if (!flipMiddleware) {
194
+ middleware.push(flip(flipOptions));
195
+ } else if (!flipMiddleware.fallbackPlacements) {
196
+ Object.assign({}, flipMiddleware, flipOptions);
155
197
  }
198
+ } else if (this.overflowPadding !== false) {
199
+ middleware.push(flip());
200
+ }
201
+
202
+ if (this.overflowPadding !== false && !middleware.find(name => name === 'shift')) {
203
+ middleware.push(shift({ limiter: limitShift(), padding: this.overflowPadding }))
204
+ }
205
+
206
+ if (this.arrow && this._arrowElement && !middleware.find(name => name === 'arrow')) {
207
+ middleware.push(arrow({ element: this._arrowElement }));
208
+ }
209
+
210
+ return middleware;
211
+ }
212
+
213
+ get _floatingElementContainer() {
214
+ const maybeContainer = this.floatingElementContainer;
215
+ const renderInPlace = this._renderInPlace;
216
+ let floatingElementContainer;
217
+
218
+ if (renderInPlace) {
219
+ floatingElementContainer = this.parentElement;
220
+ } else if (maybeContainer instanceof Element) {
221
+ floatingElementContainer = maybeContainer;
222
+ } else if (typeof maybeContainer === 'string') {
223
+ const selector = maybeContainer;
224
+ const possibleContainers = self.document.querySelectorAll(selector);
225
+
226
+ assert(`floatingElementContainer selector "${selector}" found `
227
+ + `${possibleContainers.length} possible containers when there should be exactly 1`, possibleContainers.length === 1);
228
+
229
+ floatingElementContainer = possibleContainers[0];
156
230
  }
157
231
 
158
- return modifiers;
232
+ return floatingElementContainer;
233
+ }
234
+
235
+ get _renderInPlace() {
236
+ // self.document is undefined in Fastboot, so we have to render in
237
+ // place for the floating element to show up at all.
238
+ return self.document ? !!this.renderInPlace : true;
159
239
  }
160
240
 
161
241
  _setIsVisibleAfterDelay(isVisible, delay) {
162
- if (!this._popperElement) {
242
+ if (!this._floatingElement) {
163
243
  this._animationTimeout = requestAnimationFrame(() => {
164
- this._animationTimeout = this._setIsVisibleAfterDelay(isVisible, delay);
244
+ this._setIsVisibleAfterDelay(isVisible, delay);
165
245
  });
166
-
167
246
  return;
168
247
  }
169
- const onChange = this.onChange;
248
+ const onChange = this.args.onChange;
170
249
 
171
250
  if (delay) {
172
251
  this._delayedVisibilityToggle = later(this, () => {
173
252
  this._animationTimeout = requestAnimationFrame(() => {
253
+ animationTestWaiter.endAsync(this._animationTimeout);
174
254
  if (!this.isDestroyed && !this.isDestroying) {
175
- this._popperElement.style.display = isVisible ? '' : 'none';
255
+ this._floatingElement.style.display = isVisible ? 'block' : 'none';
176
256
 
177
257
  // Prevent jank by making the attachment invisible until positioned.
178
258
  // The visibility style will be toggled by this._startShowAnimation()
179
- this._popperElement.style.visibility = isVisible ? 'hidden' : '';
259
+ this._floatingElement.style.visibility = isVisible ? 'hidden' : '';
180
260
 
181
261
  if (onChange) {
182
262
  onChange(isVisible);
183
263
  }
184
264
  }
185
265
  });
266
+ animationTestWaiter.beginAsync(this._animationTimeout);
186
267
  }, delay);
187
268
  } else {
188
- this._popperElement.style.display = isVisible ? '' : 'none';
189
-
269
+ this._floatingElement.style.display = isVisible ? 'block' : 'none';
190
270
  // Prevent jank by making the attachment invisible until positioned.
191
271
  // The visibility style will be toggled by this._startShowAnimation()
192
- this._popperElement.style.visibility = isVisible ? 'hidden' : '';
272
+ this._floatingElement.style.visibility = isVisible ? 'hidden' : '';
193
273
 
194
274
  if (onChange) {
195
275
  onChange(isVisible);
@@ -197,10 +277,6 @@ export default class AttachPopover extends Component {
197
277
  }
198
278
  }
199
279
 
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
280
  get _showOn() {
205
281
  let showOn = this.showOn;
206
282
 
@@ -211,26 +287,41 @@ export default class AttachPopover extends Component {
211
287
  return showOn === null ? [] : showOn.split(' ');
212
288
  }
213
289
 
214
- _transitionDuration = 0;
290
+ // Exposed via the named yield to enable custom hide events
291
+ @action
292
+ hide() {
293
+ this._hide();
294
+ }
215
295
 
216
- /**
217
- * ================== LIFECYCLE HOOKS ==================
218
- */
296
+ @action
297
+ onParentFinderInsert(element) {
298
+ this.parentElement = element.parentElement;
299
+ this._initializeAttacher();
300
+ }
219
301
 
220
- init() {
221
- super.init(...arguments);
302
+ @action
303
+ _ensureArgumentsAreValid() {
304
+ stripInProduction(() => {
305
+ if (this.arrow && this.isFillAnimation) {
306
+ warn('Animation: \'fill\' is not compatible with arrow: true', { id: 70015 });
307
+ }
222
308
 
223
- // Used to determine the attachments initial parent element
224
- this._parentFinder = self.document ? self.document.createTextNode('') : '';
309
+ if (this.useCapture !== this._lastUseCaptureArgumentValue) {
310
+ warn(
311
+ 'The value of the useCapture argument was mutated',
312
+ { id: 'ember-attacher.use-capture-mutated' }
313
+ );
314
+ }
315
+ });
316
+ }
225
317
 
226
- // Holds the current popper target so event listeners can be removed if the target changes
227
- this._currentTarget = null;
318
+
319
+ constructor() {
320
+ super(...arguments);
228
321
 
229
322
  // The debounced _hide() and _show() are stored here so they can be cancelled when necessary
230
323
  this._delayedVisibilityToggle = null;
231
324
 
232
- this.id = this.id || `${guidFor(this)}-popper`;
233
-
234
325
  // The final source of truth on whether or not all _hide() or _show() actions have completed
235
326
  this._isHidden = true;
236
327
 
@@ -257,97 +348,12 @@ export default class AttachPopover extends Component {
257
348
  this._show = this._show.bind(this);
258
349
  this._showAfterDelay = this._showAfterDelay.bind(this);
259
350
 
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();
351
+ this._lastUseCaptureArgumentValue = this.useCapture;
344
352
  }
345
353
 
346
354
  _initializeAttacher() {
347
355
  this._removeEventListeners();
348
-
349
- this.set('_currentTarget', this.popperTarget || this._parentFinder.parentNode);
350
-
356
+ this._currentTarget = this.args.explicitTarget || this.parentElement;
351
357
  this._addListenersForShowEvents();
352
358
 
353
359
  if (!this._isHidden || this.isShown) {
@@ -360,7 +366,6 @@ export default class AttachPopover extends Component {
360
366
  }
361
367
 
362
368
  _addListenersForShowEvents() {
363
-
364
369
  if (!this._currentTarget) {
365
370
  return;
366
371
  }
@@ -372,10 +377,10 @@ export default class AttachPopover extends Component {
372
377
  });
373
378
  }
374
379
 
375
- willDestroyElement() {
376
- super.willDestroyElement(...arguments);
380
+ willDestroy() {
381
+ super.willDestroy(...arguments);
377
382
 
378
- cancelAnimationFrame(this._animationTimeout);
383
+ this._cancelAnimation();
379
384
  cancel(this._delayedVisibilityToggle);
380
385
 
381
386
  this._removeEventListeners();
@@ -386,7 +391,6 @@ export default class AttachPopover extends Component {
386
391
  document.removeEventListener(eventType, this._hideListenersOnDocumentByEvent[eventType], this.useCapture);
387
392
  delete this._hideListenersOnDocumentByEvent[eventType];
388
393
  });
389
-
390
394
  if (!this._currentTarget) {
391
395
  return;
392
396
  }
@@ -399,13 +403,13 @@ export default class AttachPopover extends Component {
399
403
  });
400
404
  }
401
405
 
402
- @observes('hideOn', 'showOn', 'popperTarget')
403
- _targetOrTriggersChanged() {
406
+ @action
407
+ onTargetOrTriggerChange() {
404
408
  this._initializeAttacher();
405
409
  }
406
410
 
407
- @observes('isShown')
408
- _isShownChanged() {
411
+ @action
412
+ onIsShownChange() {
409
413
  const isShown = this.isShown;
410
414
 
411
415
  if (isShown === true && this._isHidden) {
@@ -426,7 +430,7 @@ export default class AttachPopover extends Component {
426
430
  _showAfterDelay() {
427
431
  cancel(this._delayedVisibilityToggle);
428
432
 
429
- this.set('_mustRender', true);
433
+ this._mustRender = true;
430
434
 
431
435
  this._addListenersForHideEvents();
432
436
 
@@ -436,13 +440,13 @@ export default class AttachPopover extends Component {
436
440
  }
437
441
 
438
442
  _show() {
439
- cancelAnimationFrame(this._animationTimeout);
443
+ this._cancelAnimation();
440
444
 
441
445
  if (!this._currentTarget) {
442
446
  return;
443
447
  }
444
448
 
445
- this.set('_mustRender', true);
449
+ this._mustRender = true;
446
450
 
447
451
  // Make the attachment visible immediately so transition animations can take place
448
452
  this._setIsVisibleAfterDelay(true, 0);
@@ -457,43 +461,44 @@ export default class AttachPopover extends Component {
457
461
  // All included animations set opaque: 0, so the attachment is still effectively hidden until
458
462
  // the final RAF occurs.
459
463
  this._animationTimeout = requestAnimationFrame(() => {
464
+ animationTestWaiter.endAsync(this._animationTimeout);
460
465
  if (this.isDestroyed || this.isDestroying || !this._currentTarget) {
461
466
  return;
462
467
  }
463
468
 
464
- const popperElement = this._popperElement;
469
+ const floatingElement = this._floatingElement;
465
470
 
466
471
  // Wait until the element is visible before continuing
467
- if (!popperElement || popperElement.style.display === 'none') {
468
- this._animationTimeout = this._startShowAnimation();
469
-
472
+ if (!floatingElement || floatingElement.style.display === 'none') {
473
+ this._startShowAnimation();
470
474
  return;
471
475
  }
472
476
 
473
- this._enableEventListeners();
474
477
  this._update();
475
478
 
476
479
  // Wait for the above positioning to take effect before starting the show animation,
477
480
  // else the positioning itself will be animated, causing animation glitches.
478
481
  this._animationTimeout = requestAnimationFrame(() => {
482
+ animationTestWaiter.endAsync(this._animationTimeout);
479
483
  if (this.isDestroyed || this.isDestroying || !this._currentTarget) {
480
484
  return;
481
485
  }
482
-
483
486
  run(() => {
484
487
  if (this.isDestroyed || this.isDestroying || !this._currentTarget) {
485
488
  return;
486
489
  }
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');
490
+ // Make the floating element visible now that it has been positioned
491
+ floatingElement.style.visibility = '';
492
+ this._transitionDuration = parseInt(this.showDuration);
493
+ this._isStartingAnimation = true;
494
+ floatingElement.setAttribute('aria-hidden', 'false');
492
495
  });
493
496
 
494
497
  this._isHidden = false;
495
498
  });
499
+ animationTestWaiter.beginAsync(this._animationTimeout);
496
500
  });
501
+ animationTestWaiter.beginAsync(this._animationTimeout);
497
502
  }
498
503
 
499
504
  /**
@@ -509,18 +514,20 @@ export default class AttachPopover extends Component {
509
514
  }
510
515
 
511
516
  _hide() {
512
- if (!this._popperElement) {
517
+ if (!this._floatingElement) {
513
518
  this._animationTimeout = requestAnimationFrame(() => {
519
+ animationTestWaiter.endAsync(this._animationTimeout);
514
520
  this._animationTimeout = this._hide();
515
521
  });
522
+ animationTestWaiter.beginAsync(this._animationTimeout);
516
523
  return;
517
524
  }
518
525
 
519
- cancelAnimationFrame(this._animationTimeout);
526
+ this._cancelAnimation();
520
527
 
521
528
  this._removeListenersForHideEvents();
522
-
523
529
  this._animationTimeout = requestAnimationFrame(() => {
530
+ animationTestWaiter.endAsync(this._animationTimeout);
524
531
  // Avoid a race condition where we attempt to hide after the component is being destroyed.
525
532
  if (this.isDestroyed || this.isDestroying) {
526
533
  return;
@@ -533,18 +540,16 @@ export default class AttachPopover extends Component {
533
540
  return;
534
541
  }
535
542
 
536
- this.set('_transitionDuration', hideDuration);
537
- this.set('_isStartingAnimation', false);
538
- this._popperElement.setAttribute('aria-hidden', 'true');
539
-
543
+ this._transitionDuration = hideDuration;
544
+ this._isStartingAnimation = false;
545
+ this._floatingElement.setAttribute('aria-hidden', 'true');
540
546
  // Wait for any animations to complete before hiding the attachment
541
547
  this._setIsVisibleAfterDelay(false, hideDuration);
542
548
  });
543
549
 
544
- this._disableEventListeners();
545
-
546
550
  this._isHidden = true;
547
551
  });
552
+ animationTestWaiter.beginAsync(this._animationTimeout);
548
553
  }
549
554
 
550
555
  /**
@@ -628,10 +633,10 @@ export default class AttachPopover extends Component {
628
633
  return;
629
634
  }
630
635
 
631
- // If cursor is not on the attachment or target, hide the popover
636
+ // If cursor is not on the attachment or target, hide the floating element
632
637
  if (!target.contains(event.target)
633
638
  && !(this.isOffset && this._isCursorBetweenTargetAndAttachment(event))
634
- && (this._popperElement && !this._popperElement.contains(event.target))) {
639
+ && (this._floatingElement && !this._floatingElement.contains(event.target))) {
635
640
  // Remove this listener before hiding the attachment
636
641
  delete this._hideListenersOnDocumentByEvent.mousemove;
637
642
  document.removeEventListener('mousemove', this._hideIfMouseOutsideTargetOrAttachment, this.useCapture);
@@ -648,7 +653,7 @@ export default class AttachPopover extends Component {
648
653
 
649
654
  const { clientX, clientY } = event;
650
655
 
651
- const attachmentPosition = this._popperElement.getBoundingClientRect();
656
+ const attachmentPosition = this._floatingElement.getBoundingClientRect();
652
657
  const targetPosition = this._currentTarget.getBoundingClientRect();
653
658
 
654
659
  const isBetweenLeftAndRight = clientX > Math.min(attachmentPosition.left, targetPosition.left)
@@ -692,7 +697,7 @@ export default class AttachPopover extends Component {
692
697
  const targetReceivedClick = this._currentTarget.contains(event.target);
693
698
 
694
699
  if (this.interactive) {
695
- if (!targetReceivedClick && !this._popperElement.contains(event.target)) {
700
+ if (!targetReceivedClick && !this._floatingElement.contains(event.target)) {
696
701
  this._hideAfterDelay();
697
702
  }
698
703
  } else if (!targetReceivedClick) {
@@ -718,7 +723,7 @@ export default class AttachPopover extends Component {
718
723
  const targetContainsFocus = this._currentTarget.contains(event.relatedTarget);
719
724
 
720
725
  if (this.interactive) {
721
- if (!targetContainsFocus && !this._popperElement.contains(event.relatedTarget)) {
726
+ if (!targetContainsFocus && !this._floatingElement.contains(event.relatedTarget)) {
722
727
  this._hideAfterDelay();
723
728
  }
724
729
  } else if (!targetContainsFocus) {
@@ -762,4 +767,75 @@ export default class AttachPopover extends Component {
762
767
  }
763
768
  });
764
769
  }
770
+
771
+ @action
772
+ didInsertFloatingElement(floatingElement) {
773
+ this._floatingElement = floatingElement;
774
+
775
+ if (this.renderInPlace) {
776
+ this.parentElement = floatingElement.parentElement;
777
+ this._initializeAttacher();
778
+ }
779
+ }
780
+
781
+ @action
782
+ didInsertArrow(element) {
783
+ this._arrowElement = element;
784
+ }
785
+
786
+ @action
787
+ onOptionsChange() {
788
+ this._ensureArgumentsAreValid();
789
+ this._update();
790
+ }
791
+
792
+ @action
793
+ willDestroyFloatingElement() {
794
+ this._cleanup?.();
795
+ }
796
+
797
+ _update() {
798
+ this._cleanup?.();
799
+ if (this.autoUpdate) {
800
+ this._cleanup = autoUpdate(
801
+ this._currentTarget,
802
+ this._floatingElement,
803
+ this._updatePosition,
804
+ typeOf(this.autoUpdate) === 'object' ? this.autoUpdate : undefined
805
+ );
806
+ } else {
807
+ this._updatePosition();
808
+ }
809
+ }
810
+
811
+ @action
812
+ _updatePosition() {
813
+ const computePositionToken = animationTestWaiter.beginAsync();
814
+ computePosition(this._currentTarget, this._floatingElement, {
815
+ ...this.floatingUiOptions,
816
+ middleware: this._middleware,
817
+ placement: this.placement
818
+ }).then(({ x, y, placement, middlewareData }) => {
819
+ animationTestWaiter.endAsync(computePositionToken);
820
+ Object.assign(this._floatingElement.style, { left: `${x}px`, top: `${y}px`, });
821
+
822
+ if (middlewareData.arrow) {
823
+ const { x, y } = middlewareData.arrow;
824
+
825
+ Object.assign(this._arrowElement.style, {
826
+ left: x != null ? `${x}px` : '',
827
+ top: y != null ? `${y}px` : '',
828
+ });
829
+ }
830
+ this._floatingElement.setAttribute('x-placement', placement);
831
+ });
832
+ }
833
+
834
+ _cancelAnimation() {
835
+ cancelAnimationFrame(this._animationTimeout);
836
+
837
+ if (animationTestWaiter.items.get(this._animationTimeout)) {
838
+ animationTestWaiter.endAsync(this._animationTimeout);
839
+ }
840
+ }
765
841
  }