@ngneat/helipopper 9.2.0 → 10.0.0-alpha.0

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,12 +1,21 @@
1
1
  import * as i0 from '@angular/core';
2
- import { ElementRef, InjectionToken, EventEmitter, Injector, PLATFORM_ID, booleanAttribute, Directive, Inject, Input, Output, Injectable, makeEnvironmentProviders, inject } from '@angular/core';
2
+ import { ElementRef, InjectionToken, inject, NgZone, Injectable, booleanAttribute, input, model, EventEmitter, computed, DestroyRef, PLATFORM_ID, Injector, effect, untracked, Directive, Inject, Output, makeEnvironmentProviders } from '@angular/core';
3
3
  import { isPlatformServer } from '@angular/common';
4
- import tippy from 'tippy.js';
5
- import { Observable, Subject, fromEvent, merge } from 'rxjs';
6
- import { auditTime, map, switchMap, takeUntil, filter } from 'rxjs/operators';
4
+ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
5
+ import { Observable, defer, from, map as map$1, of, shareReplay, Subject, fromEvent, merge } from 'rxjs';
6
+ import { auditTime, map, switchMap, filter, takeUntil } from 'rxjs/operators';
7
7
  import * as i1 from '@ngneat/overview';
8
8
  import { isString as isString$1, isComponent, isTemplateRef } from '@ngneat/overview';
9
9
 
10
+ // Let's retrieve the native `IntersectionObserver` implementation hidden by
11
+ // `__zone_symbol__IntersectionObserver`. This would be the unpatched version of
12
+ // the observer present in zone.js environments.
13
+ // Otherwise, if the user is using zoneless change detection (and zone.js is not included),
14
+ // we fall back to the native implementation. Accessing the native implementation
15
+ // allows us to remove `runOutsideAngular` calls and reduce indentation,
16
+ // making the code a bit more readable.
17
+ const IntersectionObserver = globalThis['__zone_symbol__IntersectionObserver'] || globalThis.IntersectionObserver;
18
+
10
19
  let supportsIntersectionObserver = false;
11
20
  let supportsResizeObserver = false;
12
21
  if (typeof window !== 'undefined') {
@@ -39,7 +48,7 @@ function inView(host, options = {
39
48
  function isElementOverflow(host) {
40
49
  // Don't access the `offsetWidth` multiple times since it triggers layout updates.
41
50
  const hostOffsetWidth = host.offsetWidth;
42
- return hostOffsetWidth > host.parentElement.offsetWidth || hostOffsetWidth < host.scrollWidth;
51
+ return (hostOffsetWidth > host.parentElement.offsetWidth || hostOffsetWidth < host.scrollWidth);
43
52
  }
44
53
  function overflowChanges(host) {
45
54
  const element = coerceElement(host);
@@ -130,35 +139,139 @@ function observeVisibility(host, hiddenHandler) {
130
139
  };
131
140
  }
132
141
 
133
- const TIPPY_CONFIG = new InjectionToken('Tippy config', {
134
- providedIn: 'root',
135
- factory() {
136
- return {};
137
- }
138
- });
139
142
  const TIPPY_REF = new InjectionToken('TIPPY_REF');
143
+ const TIPPY_CONFIG = new InjectionToken('Tippy config');
144
+
145
+ // We need to use `isPromise` instead of checking whether
146
+ // `value instanceof Promise`. In zone.js patched environments, `global.Promise`
147
+ // is the `ZoneAwarePromise`.
148
+ // `import(...)` returns a native promise (not a `ZoneAwarePromise`), causing
149
+ // `instanceof` check to be falsy.
150
+ function isPromise(value) {
151
+ return typeof value?.then === 'function';
152
+ }
153
+ class TippyFactory {
154
+ constructor() {
155
+ this._ngZone = inject(NgZone);
156
+ this._config = inject(TIPPY_CONFIG);
157
+ this._tippyImpl$ = null;
158
+ }
159
+ /**
160
+ * This returns an observable because the user should provide a `loader`
161
+ * function, which may return a promise if the tippy.js library is to be
162
+ * loaded asynchronously.
163
+ */
164
+ create(target, props) {
165
+ // We use `shareReplay` to ensure that subsequent emissions are
166
+ // synchronous and to avoid triggering the `defer` callback repeatedly
167
+ // when new subscribers arrive.
168
+ this._tippyImpl$ ||= defer(() => {
169
+ const maybeTippy = this._ngZone.runOutsideAngular(() => this._config.loader());
170
+ return isPromise(maybeTippy) ? from(maybeTippy).pipe(map$1((tippy) => tippy.default)) : of(maybeTippy);
171
+ }).pipe(shareReplay());
172
+ return this._tippyImpl$.pipe(map$1((tippy) => {
173
+ return this._ngZone.runOutsideAngular(() => tippy(target, props));
174
+ }));
175
+ }
176
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: TippyFactory, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
177
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: TippyFactory, providedIn: 'root' }); }
178
+ }
179
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: TippyFactory, decorators: [{
180
+ type: Injectable,
181
+ args: [{ providedIn: 'root' }]
182
+ }] });
183
+
184
+ /**
185
+ * Transforms a value (typically a string) to a boolean.
186
+ * Intended to be used as a transform function of an input.
187
+ *
188
+ * @see https://material.angular.io/cdk/coercion/overview
189
+ */
190
+ const coerceBooleanAttribute = booleanAttribute;
140
191
 
141
192
  class TippyDirective {
142
- set appendTo(appendTo) {
143
- this.updateProps({ appendTo });
193
+ // It should be a getter because computations are cached until
194
+ // any of the producers change.
195
+ get hostWidth() {
196
+ return this.host().getBoundingClientRect().width;
144
197
  }
145
- constructor(platformId, globalConfig, injector, viewService, vcr, zone, hostRef) {
146
- this.platformId = platformId;
198
+ constructor(globalConfig, injector, viewService, vcr, ngZone, hostRef) {
147
199
  this.globalConfig = globalConfig;
148
200
  this.injector = injector;
149
201
  this.viewService = viewService;
150
202
  this.vcr = vcr;
151
- this.zone = zone;
203
+ this.ngZone = ngZone;
152
204
  this.hostRef = hostRef;
153
- this.onlyTextOverflow = false;
154
- this.staticWidthHost = false;
155
- this.useHostWidth = false;
156
- this.hideOnEscape = false;
157
- this.detectChangesComponent = true;
158
- this.isVisible = false;
205
+ // Note that default values are not provided for these bindings because `tippy.js`
206
+ // has its own default values and checks whether the provided props are `undefined`.
207
+ // We should keep `undefined` as the default value.
208
+ this.appendTo = input(undefined, { alias: 'tpAppendTo' });
209
+ this.content = input(undefined, { alias: 'tp' });
210
+ this.delay = input(undefined, { alias: 'tpDelay' });
211
+ this.duration = input(undefined, { alias: 'tpDuration' });
212
+ this.hideOnClick = input(undefined, {
213
+ alias: 'tpHideOnClick',
214
+ });
215
+ this.interactive = input(undefined, {
216
+ alias: 'tpInteractive',
217
+ });
218
+ this.interactiveBorder = input(undefined, {
219
+ alias: 'tpInteractiveBorder',
220
+ });
221
+ this.maxWidth = input(undefined, { alias: 'tpMaxWidth' });
222
+ // Note that some of the input signal types are declared explicitly because the compiler
223
+ // also uses types from `@popperjs/core` and requires a type annotation.
224
+ this.offset = input(undefined, { alias: 'tpOffset' });
225
+ this.placement = input(undefined, {
226
+ alias: 'tpPlacement',
227
+ });
228
+ this.popperOptions = input(undefined, {
229
+ alias: 'tpPopperOptions',
230
+ });
231
+ this.showOnCreate = input(undefined, {
232
+ alias: 'tpShowOnCreate',
233
+ });
234
+ this.trigger = input(undefined, { alias: 'tpTrigger' });
235
+ this.triggerTarget = input(undefined, {
236
+ alias: 'tpTriggerTarget',
237
+ });
238
+ this.zIndex = input(undefined, { alias: 'tpZIndex' });
239
+ this.animation = input(undefined, {
240
+ alias: 'tpAnimation',
241
+ });
242
+ this.useTextContent = input(false, {
243
+ transform: coerceBooleanAttribute,
244
+ alias: 'tpUseTextContent',
245
+ });
246
+ this.isLazy = input(false, {
247
+ transform: coerceBooleanAttribute,
248
+ alias: 'tpIsLazy',
249
+ });
250
+ this.variation = input(undefined, { alias: 'tpVariation' });
251
+ this.isEnabled = input(true, { alias: 'tpIsEnabled' });
252
+ this.className = input('', { alias: 'tpClassName' });
253
+ this.onlyTextOverflow = input(false, {
254
+ transform: coerceBooleanAttribute,
255
+ alias: 'tpOnlyTextOverflow',
256
+ });
257
+ this.staticWidthHost = input(false, {
258
+ transform: coerceBooleanAttribute,
259
+ alias: 'tpStaticWidthHost',
260
+ });
261
+ this.data = input(undefined, { alias: 'tpData' });
262
+ this.useHostWidth = input(false, {
263
+ transform: coerceBooleanAttribute,
264
+ alias: 'tpUseHostWidth',
265
+ });
266
+ this.hideOnEscape = input(false, {
267
+ transform: coerceBooleanAttribute,
268
+ alias: 'tpHideOnEscape',
269
+ });
270
+ this.detectChangesComponent = input(true, { alias: 'tpDetectChangesComponent' });
271
+ this.popperWidth = input(undefined, { alias: 'tpPopperWidth' });
272
+ this.customHost = input(undefined, { alias: 'tpHost' });
273
+ this.isVisible = model(false, { alias: 'tpIsVisible' });
159
274
  this.visible = new EventEmitter();
160
- this.destroyed = new Subject();
161
- this.enabled = true;
162
275
  this.variationDefined = false;
163
276
  /**
164
277
  * We had use `visible` event emitter previously as a `takeUntil` subscriber in multiple places
@@ -169,10 +282,26 @@ class TippyDirective {
169
282
  */
170
283
  this.visibleInternal = new Subject();
171
284
  this.contentChanged = new Subject();
285
+ this.host = computed(() => this.customHost() || this.hostRef.nativeElement);
286
+ this.destroyRef = inject(DestroyRef);
287
+ this.isServer = isPlatformServer(inject(PLATFORM_ID));
288
+ this.tippyFactory = inject(TippyFactory);
289
+ if (this.isServer)
290
+ return;
291
+ this.setupListeners();
292
+ this.destroyRef.onDestroy(() => {
293
+ this.instance?.destroy();
294
+ this.destroyView();
295
+ this.visibilityObserverCleanup?.();
296
+ });
172
297
  }
173
298
  ngOnChanges(changes) {
174
- if (this.isServerSide)
299
+ if (this.isServer)
175
300
  return;
301
+ // TODO: we can update it later.
302
+ // We need to go over class properties, check whether it's a signal (`isSignal(prop)`)
303
+ // and construct a record where `Record<string, Signal>`.
304
+ // Follow-up changes would be to have a computation and an effect.
176
305
  let props = Object.keys(changes).reduce((acc, change) => {
177
306
  if (change === 'isVisible')
178
307
  return acc;
@@ -180,9 +309,6 @@ class TippyDirective {
180
309
  return acc;
181
310
  }, {});
182
311
  let variation;
183
- if (isChanged('content', changes)) {
184
- this.contentChanged.next();
185
- }
186
312
  if (isChanged('variation', changes)) {
187
313
  variation = changes.variation.currentValue;
188
314
  this.variationDefined = true;
@@ -197,57 +323,41 @@ class TippyDirective {
197
323
  ...props,
198
324
  };
199
325
  }
200
- if (isChanged('isEnabled', changes)) {
201
- this.enabled = changes.isEnabled.currentValue;
202
- this.setStatus();
203
- }
204
- if (isChanged('isVisible', changes)) {
205
- this.isVisible ? this.show() : this.hide();
206
- }
207
326
  this.updateProps(props);
208
327
  }
209
328
  ngOnInit() {
210
- if (this.useHostWidth) {
329
+ if (this.useHostWidth()) {
211
330
  this.props.maxWidth = this.hostWidth;
212
331
  }
213
332
  }
214
333
  ngAfterViewInit() {
215
- if (this.isServerSide)
334
+ if (this.isServer)
216
335
  return;
217
- this.zone.runOutsideAngular(() => {
218
- if (this.isLazy) {
219
- if (this.onlyTextOverflow) {
220
- inView(this.host)
221
- .pipe(switchMap(() => this.isOverflowing$()), takeUntil(this.destroyed))
222
- .subscribe((isElementOverflow) => {
223
- this.checkOverflow(isElementOverflow);
224
- });
225
- }
226
- else {
227
- inView(this.host)
228
- .pipe(takeUntil(this.destroyed))
229
- .subscribe(() => {
230
- this.createInstance();
231
- });
232
- }
233
- }
234
- else if (this.onlyTextOverflow) {
235
- this.isOverflowing$()
236
- .pipe(takeUntil(this.destroyed))
336
+ if (this.isLazy()) {
337
+ const hostInView$ = inView(this.host());
338
+ if (this.onlyTextOverflow()) {
339
+ hostInView$
340
+ .pipe(switchMap(() => this.isOverflowing$()), takeUntilDestroyed(this.destroyRef))
237
341
  .subscribe((isElementOverflow) => {
238
342
  this.checkOverflow(isElementOverflow);
239
343
  });
240
344
  }
241
345
  else {
242
- this.createInstance();
346
+ hostInView$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe(() => {
347
+ this.createInstance();
348
+ });
243
349
  }
244
- });
245
- }
246
- ngOnDestroy() {
247
- this.destroyed.next();
248
- this.instance?.destroy();
249
- this.destroyView();
250
- this.visibilityObserverCleanup?.();
350
+ }
351
+ else if (this.onlyTextOverflow()) {
352
+ this.isOverflowing$()
353
+ .pipe(takeUntilDestroyed(this.destroyRef))
354
+ .subscribe((isElementOverflow) => {
355
+ this.checkOverflow(isElementOverflow);
356
+ });
357
+ }
358
+ else {
359
+ this.createInstance();
360
+ }
251
361
  }
252
362
  destroyView() {
253
363
  this.viewOptions$ = null;
@@ -260,27 +370,24 @@ class TippyDirective {
260
370
  * For example, if you have a grid row with an element that you toggle using the display CSS property on hover.
261
371
  */
262
372
  observeHostVisibility() {
263
- if (this.isServerSide)
373
+ if (this.isServer)
264
374
  return;
265
375
  // We don't want to observe the host visibility if we are appending to the body.
266
376
  if (this.props.appendTo && this.props.appendTo !== document.body) {
267
377
  this.visibilityObserverCleanup?.();
268
378
  return this.visibleInternal
269
- .asObservable()
270
- .pipe(takeUntil(this.destroyed))
379
+ .pipe(takeUntilDestroyed(this.destroyRef))
271
380
  .subscribe((isVisible) => {
272
381
  if (isVisible) {
273
- this.zone.runOutsideAngular(() => {
274
- this.visibilityObserverCleanup = observeVisibility(this.instance.reference, () => {
275
- this.hide();
276
- // Because we have animation on the popper it doesn't close immediately doesn't trigger the `tpVisible` event.
277
- // Tippy is relying on the transitionend event to trigger the `onHidden` callback.
278
- // https://github.com/atomiks/tippyjs/blob/master/src/dom-utils.ts#L117
279
- // This event never fires because the popper is removed from the DOM before the transition ends.
280
- if (this.props.animation) {
281
- this.onHidden();
282
- }
283
- });
382
+ this.visibilityObserverCleanup = observeVisibility(this.instance.reference, () => {
383
+ this.hide();
384
+ // Because we have animation on the popper it doesn't close immediately doesn't trigger the `tpVisible` event.
385
+ // Tippy is relying on the transitionend event to trigger the `onHidden` callback.
386
+ // https://github.com/atomiks/tippyjs/blob/master/src/dom-utils.ts#L117
387
+ // This event never fires because the popper is removed from the DOM before the transition ends.
388
+ if (this.props.animation) {
389
+ this.onHidden();
390
+ }
284
391
  });
285
392
  }
286
393
  else {
@@ -308,85 +415,86 @@ class TippyDirective {
308
415
  this.props = props;
309
416
  this.instance?.setProps(onlyTippyProps(props));
310
417
  }
311
- setStatus() {
312
- this.enabled ? this.instance?.enable() : this.instance?.disable();
313
- }
314
- get host() {
315
- return this.customHost || this.hostRef.nativeElement;
316
- }
317
- get hostWidth() {
318
- return this.host.getBoundingClientRect().width;
418
+ setStatus(isEnabled) {
419
+ isEnabled ? this.instance?.enable() : this.instance?.disable();
319
420
  }
320
421
  createInstance() {
321
- if (!this.content && !this.useTextContent) {
422
+ if (!this.content() && !this.useTextContent()) {
322
423
  return;
323
424
  }
324
- this.zone.runOutsideAngular(() => {
325
- this.instance = tippy(this.host, {
326
- allowHTML: true,
327
- appendTo: document.body,
328
- ...(this.globalConfig.zIndexGetter ? { zIndex: this.globalConfig.zIndexGetter() } : {}),
329
- ...onlyTippyProps(this.globalConfig),
330
- ...onlyTippyProps(this.props),
331
- onMount: (instance) => {
332
- this.isVisible = true;
333
- this.visibleInternal.next(this.isVisible);
334
- if (this.visible.observed) {
335
- this.zone.run(() => this.visible.next(this.isVisible));
336
- }
337
- this.useHostWidth && this.listenToHostResize();
338
- this.globalConfig.onMount?.(instance);
339
- },
340
- onCreate: (instance) => {
341
- instance.popper.classList.add(`tippy-variation-${this.variation || this.globalConfig.defaultVariation}`);
342
- if (this.className) {
343
- for (const klass of normalizeClassName(this.className)) {
344
- instance.popper.classList.add(klass);
345
- }
346
- }
347
- this.globalConfig.onCreate?.(instance);
348
- if (this.isVisible === true) {
349
- instance.show();
425
+ this.tippyFactory
426
+ .create(this.host(), {
427
+ allowHTML: true,
428
+ appendTo: document.body,
429
+ ...(this.globalConfig.zIndexGetter
430
+ ? { zIndex: this.globalConfig.zIndexGetter() }
431
+ : {}),
432
+ ...onlyTippyProps(this.globalConfig),
433
+ ...onlyTippyProps(this.props),
434
+ onMount: (instance) => {
435
+ const isVisible = true;
436
+ this.isVisible.set(isVisible);
437
+ this.visibleInternal.next(isVisible);
438
+ if (this.visible.observed) {
439
+ this.ngZone.run(() => this.visible.next(isVisible));
440
+ }
441
+ this.useHostWidth() && this.listenToHostResize();
442
+ this.globalConfig.onMount?.(instance);
443
+ },
444
+ onCreate: (instance) => {
445
+ instance.popper.classList.add(`tippy-variation-${this.variation() || this.globalConfig.defaultVariation}`);
446
+ if (this.className()) {
447
+ for (const klass of normalizeClassName(this.className())) {
448
+ instance.popper.classList.add(klass);
350
449
  }
351
- },
352
- onShow: (instance) => {
353
- instance.reference.setAttribute('data-tippy-open', '');
354
- this.zone.run(() => {
355
- const content = this.resolveContent(instance);
356
- if (isString$1(content)) {
357
- instance.setProps({ allowHTML: false });
358
- if (!content?.trim()) {
359
- this.disable();
360
- }
361
- else {
362
- this.enable();
363
- }
364
- }
365
- instance.setContent(content);
366
- this.hideOnEscape && this.handleEscapeButton();
367
- });
368
- if (this.useHostWidth) {
369
- this.setInstanceWidth(instance, this.hostWidth);
450
+ }
451
+ this.globalConfig.onCreate?.(instance);
452
+ if (this.isVisible() === true) {
453
+ instance.show();
454
+ }
455
+ },
456
+ onShow: (instance) => {
457
+ instance.reference.setAttribute('data-tippy-open', '');
458
+ // We're re-entering because we might create an Angular component,
459
+ // which should be done within the zone.
460
+ const content = this.ngZone.run(() => this.resolveContent(instance));
461
+ if (isString$1(content)) {
462
+ instance.setProps({ allowHTML: false });
463
+ if (!content?.trim()) {
464
+ this.disable();
370
465
  }
371
- else if (this.popperWidth) {
372
- this.setInstanceWidth(instance, this.popperWidth);
466
+ else {
467
+ this.enable();
373
468
  }
374
- this.globalConfig.onShow?.(instance);
375
- },
376
- onHide(instance) {
377
- instance.reference.removeAttribute('data-tippy-open');
378
- },
379
- onHidden: (instance) => {
380
- this.onHidden(instance);
381
- },
382
- });
383
- this.setStatus();
469
+ }
470
+ instance.setContent(content);
471
+ this.hideOnEscape() && this.handleEscapeButton();
472
+ if (this.useHostWidth()) {
473
+ this.setInstanceWidth(instance, this.hostWidth);
474
+ }
475
+ else if (this.popperWidth()) {
476
+ this.setInstanceWidth(instance, this.popperWidth());
477
+ }
478
+ this.globalConfig.onShow?.(instance);
479
+ },
480
+ onHide(instance) {
481
+ instance.reference.removeAttribute('data-tippy-open');
482
+ },
483
+ onHidden: (instance) => {
484
+ this.onHidden(instance);
485
+ },
486
+ })
487
+ .pipe(takeUntilDestroyed(this.destroyRef))
488
+ .subscribe((instance) => {
489
+ this.instance = instance;
490
+ this.setStatus(this.isEnabled());
384
491
  this.setProps(this.props);
385
- this.variation === 'contextMenu' && this.handleContextMenu();
492
+ this.variation() === 'contextMenu' && this.handleContextMenu();
386
493
  });
387
494
  }
388
495
  resolveContent(instance) {
389
- if (!this.viewOptions$ && !isString$1(this.content)) {
496
+ const content = this.content();
497
+ if (!this.viewOptions$ && !isString$1(content)) {
390
498
  const injector = Injector.create({
391
499
  providers: [
392
500
  {
@@ -396,42 +504,43 @@ class TippyDirective {
396
504
  ],
397
505
  parent: this.injector,
398
506
  });
399
- if (isComponent(this.content)) {
400
- this.instance.data = this.data;
507
+ const data = this.data();
508
+ if (isComponent(content)) {
509
+ this.instance.data = data;
401
510
  this.viewOptions$ = {
402
511
  injector,
403
512
  };
404
513
  }
405
- else if (isTemplateRef(this.content)) {
514
+ else if (isTemplateRef(content)) {
406
515
  this.viewOptions$ = {
407
516
  injector,
408
517
  context: {
518
+ data,
409
519
  $implicit: this.hide.bind(this),
410
- data: this.data,
411
520
  },
412
521
  };
413
522
  }
414
523
  }
415
- this.viewRef = this.viewService.createView(this.content, {
524
+ this.viewRef = this.viewService.createView(content, {
416
525
  vcr: this.vcr,
417
526
  ...this.viewOptions$,
418
527
  });
419
528
  // We need to call detectChanges for onPush components to update the content
420
- if (this.detectChangesComponent && isComponent(this.content)) {
529
+ if (this.detectChangesComponent() && isComponent(content)) {
421
530
  this.viewRef.detectChanges();
422
531
  }
423
- let content = this.viewRef.getElement();
424
- if (this.useTextContent) {
425
- content = instance.reference.textContent;
532
+ let newContent = this.viewRef.getElement();
533
+ if (this.useTextContent()) {
534
+ newContent = instance.reference.textContent;
426
535
  }
427
- if (isString$1(content) && this.globalConfig.beforeRender) {
428
- content = this.globalConfig.beforeRender(content);
536
+ if (isString$1(newContent) && this.globalConfig.beforeRender) {
537
+ newContent = this.globalConfig.beforeRender(newContent);
429
538
  }
430
- return content;
539
+ return newContent;
431
540
  }
432
541
  handleContextMenu() {
433
- fromEvent(this.host, 'contextmenu')
434
- .pipe(takeUntil(this.destroyed))
542
+ fromEvent(this.host(), 'contextmenu')
543
+ .pipe(takeUntilDestroyed(this.destroyRef))
435
544
  .subscribe((event) => {
436
545
  event.preventDefault();
437
546
  this.instance.setProps({
@@ -448,11 +557,9 @@ class TippyDirective {
448
557
  });
449
558
  }
450
559
  handleEscapeButton() {
451
- this.zone.runOutsideAngular(() => {
452
- fromEvent(document.body, 'keydown')
453
- .pipe(filter(({ code }) => code === 'Escape'), takeUntil(merge(this.destroyed, this.visibleInternal.pipe(filter((v) => !v)))))
454
- .subscribe(() => this.hide());
455
- });
560
+ fromEvent(document.body, 'keydown')
561
+ .pipe(filter(({ code }) => code === 'Escape'), takeUntil(this.visibleInternal.pipe(filter((v) => !v))), takeUntilDestroyed(this.destroyRef))
562
+ .subscribe(() => this.hide());
456
563
  }
457
564
  checkOverflow(isElementOverflow) {
458
565
  if (isElementOverflow) {
@@ -468,8 +575,8 @@ class TippyDirective {
468
575
  }
469
576
  }
470
577
  listenToHostResize() {
471
- dimensionsChanges(this.host)
472
- .pipe(takeUntil(merge(this.destroyed, this.visibleInternal)))
578
+ dimensionsChanges(this.host())
579
+ .pipe(takeUntil(this.visibleInternal), takeUntilDestroyed(this.destroyRef))
473
580
  .subscribe(() => {
474
581
  this.setInstanceWidth(this.instance, this.hostWidth);
475
582
  });
@@ -480,23 +587,22 @@ class TippyDirective {
480
587
  instance.popper.style.maxWidth = inPixels;
481
588
  instance.popper.firstElementChild.style.maxWidth = inPixels;
482
589
  }
483
- get isServerSide() {
484
- return isPlatformServer(this.platformId);
485
- }
486
590
  onHidden(instance = this.instance) {
487
591
  this.destroyView();
488
- this.isVisible = false;
489
- this.visibleInternal.next(this.isVisible);
592
+ const isVisible = false;
593
+ this.isVisible.set(isVisible);
594
+ this.visibleInternal.next(isVisible);
490
595
  if (this.visible.observed) {
491
- this.zone.run(() => this.visible.next(this.isVisible));
596
+ this.ngZone.run(() => this.visible.next(isVisible));
492
597
  }
493
598
  this.globalConfig.onHidden?.(instance);
494
599
  }
495
600
  isOverflowing$() {
496
- const notifiers$ = [overflowChanges(this.host)];
601
+ const host = this.host();
602
+ const notifiers$ = [overflowChanges(host)];
497
603
  // We need to handle cases where the host has a static width but the content might change
498
- if (this.staticWidthHost) {
499
- notifiers$.push(this.contentChanged.asObservable().pipe(
604
+ if (this.staticWidthHost()) {
605
+ notifiers$.push(this.contentChanged.pipe(
500
606
  // We need to wait for the content to be rendered before we can check if it's overflowing.
501
607
  switchMap(() => {
502
608
  return new Observable((subscriber) => {
@@ -506,14 +612,30 @@ class TippyDirective {
506
612
  });
507
613
  return () => cancelAnimationFrame(id);
508
614
  });
509
- }), map(() => isElementOverflow(this.host))));
615
+ }), map(() => isElementOverflow(host))));
510
616
  }
511
617
  return merge(...notifiers$);
512
618
  }
513
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: TippyDirective, deps: [{ token: PLATFORM_ID }, { token: TIPPY_CONFIG }, { token: i0.Injector }, { token: i1.ViewService }, { token: i0.ViewContainerRef }, { token: i0.NgZone }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
514
- static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "17.0.4", type: TippyDirective, isStandalone: true, selector: "[tp]", inputs: { appendTo: ["tpAppendTo", "appendTo"], content: ["tp", "content"], delay: ["tpDelay", "delay"], duration: ["tpDuration", "duration"], hideOnClick: ["tpHideOnClick", "hideOnClick"], interactive: ["tpInteractive", "interactive"], interactiveBorder: ["tpInteractiveBorder", "interactiveBorder"], maxWidth: ["tpMaxWidth", "maxWidth"], offset: ["tpOffset", "offset"], placement: ["tpPlacement", "placement"], popperOptions: ["tpPopperOptions", "popperOptions"], showOnCreate: ["tpShowOnCreate", "showOnCreate"], trigger: ["tpTrigger", "trigger"], triggerTarget: ["tpTriggerTarget", "triggerTarget"], zIndex: ["tpZIndex", "zIndex"], animation: ["tpAnimation", "animation"], useTextContent: ["tpUseTextContent", "useTextContent", booleanAttribute], isLazy: ["tpIsLazy", "isLazy", booleanAttribute], variation: ["tpVariation", "variation"], isEnabled: ["tpIsEnabled", "isEnabled"], className: ["tpClassName", "className"], onlyTextOverflow: ["tpOnlyTextOverflow", "onlyTextOverflow", booleanAttribute], staticWidthHost: ["tpStaticWidthHost", "staticWidthHost", booleanAttribute], data: ["tpData", "data"], useHostWidth: ["tpUseHostWidth", "useHostWidth", booleanAttribute], hideOnEscape: ["tpHideOnEscape", "hideOnEscape", booleanAttribute], detectChangesComponent: ["tpDetectChangesComponent", "detectChangesComponent"], popperWidth: ["tpPopperWidth", "popperWidth"], customHost: ["tpHost", "customHost"], isVisible: ["tpIsVisible", "isVisible", booleanAttribute] }, outputs: { visible: "tpVisible" }, exportAs: ["tippy"], usesOnChanges: true, ngImport: i0 }); }
619
+ setupListeners() {
620
+ effect(() => {
621
+ const appendTo = this.appendTo();
622
+ this.updateProps({ appendTo });
623
+ });
624
+ effect(() => {
625
+ // Capture signal read to track its changes.
626
+ this.content();
627
+ untracked(() => this.contentChanged.next());
628
+ });
629
+ effect(() => this.setStatus(this.isEnabled()));
630
+ effect(() => {
631
+ const isVisible = this.isVisible();
632
+ isVisible ? this.show() : this.hide();
633
+ });
634
+ }
635
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: TippyDirective, deps: [{ token: TIPPY_CONFIG }, { token: i0.Injector }, { token: i1.ViewService }, { token: i0.ViewContainerRef }, { token: i0.NgZone }, { token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); }
636
+ static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "18.0.6", type: TippyDirective, isStandalone: true, selector: "[tp]", inputs: { appendTo: { classPropertyName: "appendTo", publicName: "tpAppendTo", isSignal: true, isRequired: false, transformFunction: null }, content: { classPropertyName: "content", publicName: "tp", isSignal: true, isRequired: false, transformFunction: null }, delay: { classPropertyName: "delay", publicName: "tpDelay", isSignal: true, isRequired: false, transformFunction: null }, duration: { classPropertyName: "duration", publicName: "tpDuration", isSignal: true, isRequired: false, transformFunction: null }, hideOnClick: { classPropertyName: "hideOnClick", publicName: "tpHideOnClick", isSignal: true, isRequired: false, transformFunction: null }, interactive: { classPropertyName: "interactive", publicName: "tpInteractive", isSignal: true, isRequired: false, transformFunction: null }, interactiveBorder: { classPropertyName: "interactiveBorder", publicName: "tpInteractiveBorder", isSignal: true, isRequired: false, transformFunction: null }, maxWidth: { classPropertyName: "maxWidth", publicName: "tpMaxWidth", isSignal: true, isRequired: false, transformFunction: null }, offset: { classPropertyName: "offset", publicName: "tpOffset", isSignal: true, isRequired: false, transformFunction: null }, placement: { classPropertyName: "placement", publicName: "tpPlacement", isSignal: true, isRequired: false, transformFunction: null }, popperOptions: { classPropertyName: "popperOptions", publicName: "tpPopperOptions", isSignal: true, isRequired: false, transformFunction: null }, showOnCreate: { classPropertyName: "showOnCreate", publicName: "tpShowOnCreate", isSignal: true, isRequired: false, transformFunction: null }, trigger: { classPropertyName: "trigger", publicName: "tpTrigger", isSignal: true, isRequired: false, transformFunction: null }, triggerTarget: { classPropertyName: "triggerTarget", publicName: "tpTriggerTarget", isSignal: true, isRequired: false, transformFunction: null }, zIndex: { classPropertyName: "zIndex", publicName: "tpZIndex", isSignal: true, isRequired: false, transformFunction: null }, animation: { classPropertyName: "animation", publicName: "tpAnimation", isSignal: true, isRequired: false, transformFunction: null }, useTextContent: { classPropertyName: "useTextContent", publicName: "tpUseTextContent", isSignal: true, isRequired: false, transformFunction: null }, isLazy: { classPropertyName: "isLazy", publicName: "tpIsLazy", isSignal: true, isRequired: false, transformFunction: null }, variation: { classPropertyName: "variation", publicName: "tpVariation", isSignal: true, isRequired: false, transformFunction: null }, isEnabled: { classPropertyName: "isEnabled", publicName: "tpIsEnabled", isSignal: true, isRequired: false, transformFunction: null }, className: { classPropertyName: "className", publicName: "tpClassName", isSignal: true, isRequired: false, transformFunction: null }, onlyTextOverflow: { classPropertyName: "onlyTextOverflow", publicName: "tpOnlyTextOverflow", isSignal: true, isRequired: false, transformFunction: null }, staticWidthHost: { classPropertyName: "staticWidthHost", publicName: "tpStaticWidthHost", isSignal: true, isRequired: false, transformFunction: null }, data: { classPropertyName: "data", publicName: "tpData", isSignal: true, isRequired: false, transformFunction: null }, useHostWidth: { classPropertyName: "useHostWidth", publicName: "tpUseHostWidth", isSignal: true, isRequired: false, transformFunction: null }, hideOnEscape: { classPropertyName: "hideOnEscape", publicName: "tpHideOnEscape", isSignal: true, isRequired: false, transformFunction: null }, detectChangesComponent: { classPropertyName: "detectChangesComponent", publicName: "tpDetectChangesComponent", isSignal: true, isRequired: false, transformFunction: null }, popperWidth: { classPropertyName: "popperWidth", publicName: "tpPopperWidth", isSignal: true, isRequired: false, transformFunction: null }, customHost: { classPropertyName: "customHost", publicName: "tpHost", isSignal: true, isRequired: false, transformFunction: null }, isVisible: { classPropertyName: "isVisible", publicName: "tpIsVisible", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { isVisible: "tpIsVisibleChange", visible: "tpVisible" }, exportAs: ["tippy"], usesOnChanges: true, ngImport: i0 }); }
515
637
  }
516
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: TippyDirective, decorators: [{
638
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: TippyDirective, decorators: [{
517
639
  type: Directive,
518
640
  args: [{
519
641
  // eslint-disable-next-line @angular-eslint/directive-selector
@@ -522,102 +644,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImpor
522
644
  standalone: true,
523
645
  }]
524
646
  }], ctorParameters: () => [{ type: undefined, decorators: [{
525
- type: Inject,
526
- args: [PLATFORM_ID]
527
- }] }, { type: undefined, decorators: [{
528
647
  type: Inject,
529
648
  args: [TIPPY_CONFIG]
530
- }] }, { type: i0.Injector }, { type: i1.ViewService }, { type: i0.ViewContainerRef }, { type: i0.NgZone }, { type: i0.ElementRef }], propDecorators: { appendTo: [{
531
- type: Input,
532
- args: ['tpAppendTo']
533
- }], content: [{
534
- type: Input,
535
- args: ['tp']
536
- }], delay: [{
537
- type: Input,
538
- args: ['tpDelay']
539
- }], duration: [{
540
- type: Input,
541
- args: ['tpDuration']
542
- }], hideOnClick: [{
543
- type: Input,
544
- args: ['tpHideOnClick']
545
- }], interactive: [{
546
- type: Input,
547
- args: ['tpInteractive']
548
- }], interactiveBorder: [{
549
- type: Input,
550
- args: ['tpInteractiveBorder']
551
- }], maxWidth: [{
552
- type: Input,
553
- args: ['tpMaxWidth']
554
- }], offset: [{
555
- type: Input,
556
- args: ['tpOffset']
557
- }], placement: [{
558
- type: Input,
559
- args: ['tpPlacement']
560
- }], popperOptions: [{
561
- type: Input,
562
- args: ['tpPopperOptions']
563
- }], showOnCreate: [{
564
- type: Input,
565
- args: ['tpShowOnCreate']
566
- }], trigger: [{
567
- type: Input,
568
- args: ['tpTrigger']
569
- }], triggerTarget: [{
570
- type: Input,
571
- args: ['tpTriggerTarget']
572
- }], zIndex: [{
573
- type: Input,
574
- args: ['tpZIndex']
575
- }], animation: [{
576
- type: Input,
577
- args: ['tpAnimation']
578
- }], useTextContent: [{
579
- type: Input,
580
- args: [{ transform: booleanAttribute, alias: 'tpUseTextContent' }]
581
- }], isLazy: [{
582
- type: Input,
583
- args: [{ transform: booleanAttribute, alias: 'tpIsLazy' }]
584
- }], variation: [{
585
- type: Input,
586
- args: ['tpVariation']
587
- }], isEnabled: [{
588
- type: Input,
589
- args: ['tpIsEnabled']
590
- }], className: [{
591
- type: Input,
592
- args: ['tpClassName']
593
- }], onlyTextOverflow: [{
594
- type: Input,
595
- args: [{ transform: booleanAttribute, alias: 'tpOnlyTextOverflow' }]
596
- }], staticWidthHost: [{
597
- type: Input,
598
- args: [{ transform: booleanAttribute, alias: 'tpStaticWidthHost' }]
599
- }], data: [{
600
- type: Input,
601
- args: ['tpData']
602
- }], useHostWidth: [{
603
- type: Input,
604
- args: [{ transform: booleanAttribute, alias: 'tpUseHostWidth' }]
605
- }], hideOnEscape: [{
606
- type: Input,
607
- args: [{ transform: booleanAttribute, alias: 'tpHideOnEscape' }]
608
- }], detectChangesComponent: [{
609
- type: Input,
610
- args: ['tpDetectChangesComponent']
611
- }], popperWidth: [{
612
- type: Input,
613
- args: ['tpPopperWidth']
614
- }], customHost: [{
615
- type: Input,
616
- args: ['tpHost']
617
- }], isVisible: [{
618
- type: Input,
619
- args: [{ transform: booleanAttribute, alias: 'tpIsVisible' }]
620
- }], visible: [{
649
+ }] }, { type: i0.Injector }, { type: i1.ViewService }, { type: i0.ViewContainerRef }, { type: i0.NgZone }, { type: i0.ElementRef }], propDecorators: { visible: [{
621
650
  type: Output,
622
651
  args: ['tpVisible']
623
652
  }] } });
@@ -655,32 +684,34 @@ class TippyService {
655
684
  this.globalConfig = globalConfig;
656
685
  this.view = view;
657
686
  this.injector = injector;
687
+ this._tippyFactory = inject(TippyFactory);
658
688
  }
659
689
  create(host, content, options = {}) {
660
690
  const variation = options.variation || this.globalConfig.defaultVariation;
661
691
  const config = {
662
- onShow: instance => {
692
+ onShow: (instance) => {
663
693
  host.setAttribute('data-tippy-open', '');
664
694
  if (!instance.$viewOptions) {
665
695
  instance.$viewOptions = {};
696
+ const injector = Injector.create({
697
+ providers: [
698
+ {
699
+ provide: TIPPY_REF,
700
+ useValue: instance,
701
+ },
702
+ ],
703
+ parent: options.injector || this.injector,
704
+ });
705
+ instance.$viewOptions.injector = injector;
666
706
  if (isTemplateRef(content)) {
667
707
  instance.$viewOptions.context = {
668
708
  $implicit: instance.hide.bind(instance),
669
- ...options.context
709
+ ...options.context,
670
710
  };
671
711
  }
672
712
  else if (isComponent(content)) {
673
713
  instance.context = options.context;
674
714
  instance.data = options.data;
675
- instance.$viewOptions.injector = Injector.create({
676
- providers: [
677
- {
678
- provide: TIPPY_REF,
679
- useValue: instance
680
- }
681
- ],
682
- parent: options.injector || this.injector
683
- });
684
715
  }
685
716
  }
686
717
  if (!instance.view) {
@@ -689,7 +720,7 @@ class TippyService {
689
720
  instance.setContent(instance.view.getElement());
690
721
  options?.onShow?.(instance);
691
722
  },
692
- onHidden: instance => {
723
+ onHidden: (instance) => {
693
724
  host.removeAttribute('data-tippy-open');
694
725
  if (!options.preserveView) {
695
726
  instance.view.destroy();
@@ -700,7 +731,7 @@ class TippyService {
700
731
  ...onlyTippyProps(this.globalConfig),
701
732
  ...this.globalConfig.variations[variation],
702
733
  ...onlyTippyProps(options),
703
- onCreate: instance => {
734
+ onCreate: (instance) => {
704
735
  instance.popper.classList.add(`tippy-variation-${variation}`);
705
736
  if (options.className) {
706
737
  for (const klass of normalizeClassName(options.className)) {
@@ -709,14 +740,14 @@ class TippyService {
709
740
  }
710
741
  this.globalConfig.onCreate?.(instance);
711
742
  options.onCreate?.(instance);
712
- }
743
+ },
713
744
  };
714
- return tippy(host, config);
745
+ return this._tippyFactory.create(host, config);
715
746
  }
716
- static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: TippyService, deps: [{ token: TIPPY_CONFIG }, { token: i1.ViewService }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable }); }
717
- static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: TippyService, providedIn: 'root' }); }
747
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: TippyService, deps: [{ token: TIPPY_CONFIG }, { token: i1.ViewService }, { token: i0.Injector }], target: i0.ɵɵFactoryTarget.Injectable }); }
748
+ static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: TippyService, providedIn: 'root' }); }
718
749
  }
719
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImport: i0, type: TippyService, decorators: [{
750
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "18.0.6", ngImport: i0, type: TippyService, decorators: [{
720
751
  type: Injectable,
721
752
  args: [{ providedIn: 'root' }]
722
753
  }], ctorParameters: () => [{ type: undefined, decorators: [{
@@ -724,7 +755,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "17.0.4", ngImpor
724
755
  args: [TIPPY_CONFIG]
725
756
  }] }, { type: i1.ViewService }, { type: i0.Injector }] });
726
757
 
727
- function provideTippyConfig(config = {}) {
758
+ function provideTippyConfig(config) {
728
759
  return makeEnvironmentProviders([{ provide: TIPPY_CONFIG, useValue: config }]);
729
760
  }
730
761
  function injectTippyRef() {