@lucca-front/ng 21.1.2 → 21.1.3

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.
Files changed (42) hide show
  1. package/fesm2022/lucca-front-ng-callout.mjs +2 -2
  2. package/fesm2022/lucca-front-ng-callout.mjs.map +1 -1
  3. package/fesm2022/lucca-front-ng-core-select.mjs +5 -2
  4. package/fesm2022/lucca-front-ng-core-select.mjs.map +1 -1
  5. package/fesm2022/lucca-front-ng-date2.mjs +2 -2
  6. package/fesm2022/lucca-front-ng-date2.mjs.map +1 -1
  7. package/fesm2022/lucca-front-ng-dialog.mjs +1 -1
  8. package/fesm2022/lucca-front-ng-dialog.mjs.map +1 -1
  9. package/fesm2022/lucca-front-ng-form-field.mjs +4 -4
  10. package/fesm2022/lucca-front-ng-form-field.mjs.map +1 -1
  11. package/fesm2022/lucca-front-ng-forms-phone-number-input.mjs +2 -2
  12. package/fesm2022/lucca-front-ng-forms-phone-number-input.mjs.map +1 -1
  13. package/fesm2022/lucca-front-ng-forms-rich-text-input.mjs +163 -64
  14. package/fesm2022/lucca-front-ng-forms-rich-text-input.mjs.map +1 -1
  15. package/fesm2022/lucca-front-ng-forms.mjs +14 -14
  16. package/fesm2022/lucca-front-ng-forms.mjs.map +1 -1
  17. package/fesm2022/lucca-front-ng-highlight-data.mjs.map +1 -1
  18. package/fesm2022/lucca-front-ng-multi-select.mjs +20 -10
  19. package/fesm2022/lucca-front-ng-multi-select.mjs.map +1 -1
  20. package/fesm2022/lucca-front-ng-resource-card.mjs +2 -2
  21. package/fesm2022/lucca-front-ng-resource-card.mjs.map +1 -1
  22. package/fesm2022/lucca-front-ng-simple-select.mjs +5 -4
  23. package/fesm2022/lucca-front-ng-simple-select.mjs.map +1 -1
  24. package/fesm2022/lucca-front-ng-skeleton.mjs +4 -4
  25. package/fesm2022/lucca-front-ng-skeleton.mjs.map +1 -1
  26. package/fesm2022/lucca-front-ng-toast.mjs +2 -2
  27. package/fesm2022/lucca-front-ng-toast.mjs.map +1 -1
  28. package/fesm2022/lucca-front-ng-tooltip.mjs +129 -162
  29. package/fesm2022/lucca-front-ng-tooltip.mjs.map +1 -1
  30. package/package.json +14 -14
  31. package/schematics/lib/component-mapper.js +1 -1
  32. package/schematics/lib/css-mapper.js +1 -1
  33. package/schematics/lu-button/index.js +1 -1
  34. package/schematics/lu-container/index.js +1 -1
  35. package/schematics/lu-icon/index.js +1 -1
  36. package/schematics/lu-loading/index.js +1 -1
  37. package/schematics/lu-select/index.js +1 -1
  38. package/schematics/lu-text-input/index.js +1 -1
  39. package/schematics/new-icons/index.js +1 -1
  40. package/src/components/cdk/_global.scss +8 -8
  41. package/types/lucca-front-ng-forms-rich-text-input.d.ts +69 -14
  42. package/types/lucca-front-ng-tooltip.d.ts +9 -17
@@ -1,12 +1,12 @@
1
1
  import { trigger, state, style, transition, animate } from '@angular/animations';
2
2
  import * as i0 from '@angular/core';
3
- import { inject, DestroyRef, signal, ChangeDetectionStrategy, Component, Injectable, ElementRef, Renderer2, NgZone, Injector, input, linkedSignal, numberAttribute, booleanAttribute, computed, effect, Directive, NgModule } from '@angular/core';
4
- import { Subject, timer, shareReplay, filter, firstValueFrom, Observable, combineLatest, switchMap, startWith } from 'rxjs';
3
+ import { inject, DestroyRef, signal, ChangeDetectionStrategy, Component, Injectable, ElementRef, Renderer2, Injector, input, linkedSignal, numberAttribute, booleanAttribute, computed, effect, Directive, NgModule } from '@angular/core';
4
+ import { Subject, switchMap, of, from, timer, startWith } from 'rxjs';
5
5
  import { Overlay, OverlayModule } from '@angular/cdk/overlay';
6
6
  import { ComponentPortal } from '@angular/cdk/portal';
7
- import { toObservable, toSignal, takeUntilDestroyed } from '@angular/core/rxjs-interop';
7
+ import { toSignal, toObservable, takeUntilDestroyed } from '@angular/core/rxjs-interop';
8
8
  import { isNotNil, ɵeffectWithDeps as _effectWithDeps } from '@lucca-front/ng/core';
9
- import { debounceTime, filter as filter$1, debounce, tap, map } from 'rxjs/operators';
9
+ import { debounceTime, filter, debounce, tap, map } from 'rxjs/operators';
10
10
  import { DOCUMENT } from '@angular/common';
11
11
 
12
12
  const luTransformTooltip = trigger('transformTooltip', [
@@ -30,7 +30,6 @@ class LuTooltipPanelComponent {
30
30
  this.mouseEnter$ = new Subject();
31
31
  this.mouseLeave$ = new Subject();
32
32
  this.content = signal(null, ...(ngDevMode ? [{ debugName: "content" }] : []));
33
- this.id = signal(null, ...(ngDevMode ? [{ debugName: "id" }] : []));
34
33
  this.contentPositionClasses = signal({}, ...(ngDevMode ? [{ debugName: "contentPositionClasses" }] : []));
35
34
  }
36
35
  setPanelPosition(posX, posY) {
@@ -42,115 +41,75 @@ class LuTooltipPanelComponent {
42
41
  });
43
42
  }
44
43
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: LuTooltipPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
45
- static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: LuTooltipPanelComponent, isStandalone: true, selector: "lu-tooltip-panel", host: { attributes: { "role": "tooltip" }, listeners: { "mouseenter": "mouseEnter$.next()", "mouseleave": "mouseLeave$.next()" }, properties: { "attr.id": "id" } }, ngImport: i0, template: "@if (content(); as content) {\n\t<div class=\"tooltip\" [class]=\"contentPositionClasses()\" [innerHtml]=\"content\"></div>\n}\n", styles: ["@layer components{.tooltip{--components-tooltip-background-color: var(--palettes-neutral-900);--components-tooltip-color: var(--pr-t-color-text-reverse);--components-tooltip-max-width: 15rem;--components-tooltip-transformOrigin: center;--components-tooltip-margin: 0;background-color:var(--components-tooltip-background-color);color:var(--components-tooltip-color);padding-block:var(--pr-t-spacings-50);padding-inline:var(--pr-t-spacings-100);max-inline-size:var(--components-tooltip-max-width);border-radius:var(--pr-t-border-radius-default);font:var(--pr-t-font-body-XS);transform-origin:var(--components-tooltip-transformOrigin);margin:var(--components-tooltip-margin);text-align:center;inline-size:fit-content;animation-name:scaleIn;animation-duration:var(--commons-animations-durations-fast);animation-iteration-count:1;overflow-wrap:break-word}@keyframes scaleIn{0%{transform:scale(0)}to{transform:scale(1)}}.tooltip:empty{display:none}@supports (background-image: -webkit-named-image(i)){@media(hover:hover){[luTooltipWhenEllipsis]:after{content:\"\";display:block}}}}@layer mods{.tooltip.is-above{--components-tooltip-transformOrigin: bottom center}.tooltip.is-below{--components-tooltip-transformOrigin: top center}.tooltip.is-before{--components-tooltip-transformOrigin: center right}.tooltip.is-before.is-above{--components-tooltip-transformOrigin: bottom right}.tooltip.is-before.is-below{--components-tooltip-transformOrigin: top right}.tooltip.is-after{--components-tooltip-transformOrigin: center left}.tooltip.is-after.is-above{--components-tooltip-transformOrigin: bottom left}.tooltip.is-after.is-below{--components-tooltip-transformOrigin: top left}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
44
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.0.8", type: LuTooltipPanelComponent, isStandalone: true, selector: "lu-tooltip-panel", host: { attributes: { "role": "tooltip" }, listeners: { "mouseenter": "mouseEnter$.next()", "mouseleave": "mouseLeave$.next()" } }, ngImport: i0, template: "@if (content(); as content) {\n\t<div class=\"tooltip\" [class]=\"contentPositionClasses()\" [innerHtml]=\"content\"></div>\n}\n", styles: ["@layer components{.tooltip{--components-tooltip-background-color: var(--palettes-neutral-900);--components-tooltip-color: var(--pr-t-color-text-reverse);--components-tooltip-max-width: 15rem;--components-tooltip-transformOrigin: center;--components-tooltip-margin: 0;background-color:var(--components-tooltip-background-color);color:var(--components-tooltip-color);padding-block:var(--pr-t-spacings-50);padding-inline:var(--pr-t-spacings-100);max-inline-size:var(--components-tooltip-max-width);border-radius:var(--pr-t-border-radius-default);font:var(--pr-t-font-body-XS);transform-origin:var(--components-tooltip-transformOrigin);margin:var(--components-tooltip-margin);text-align:center;inline-size:fit-content;animation-name:scaleIn;animation-duration:var(--commons-animations-durations-fast);animation-iteration-count:1;overflow-wrap:break-word}@keyframes scaleIn{0%{transform:scale(0)}to{transform:scale(1)}}.tooltip:empty{display:none}@supports (background-image: -webkit-named-image(i)){@media(hover:hover){[luTooltipWhenEllipsis]:after{content:\"\";display:block}}}}@layer mods{.tooltip.is-above{--components-tooltip-transformOrigin: bottom center}.tooltip.is-below{--components-tooltip-transformOrigin: top center}.tooltip.is-before{--components-tooltip-transformOrigin: center right}.tooltip.is-before.is-above{--components-tooltip-transformOrigin: bottom right}.tooltip.is-before.is-below{--components-tooltip-transformOrigin: top right}.tooltip.is-after{--components-tooltip-transformOrigin: center left}.tooltip.is-after.is-above{--components-tooltip-transformOrigin: bottom left}.tooltip.is-after.is-below{--components-tooltip-transformOrigin: top left}}\n"], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
46
45
  }
47
46
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: LuTooltipPanelComponent, decorators: [{
48
47
  type: Component,
49
48
  args: [{ selector: 'lu-tooltip-panel', host: {
50
49
  role: 'tooltip',
51
- '[attr.id]': 'id',
52
50
  '(mouseenter)': 'mouseEnter$.next()',
53
51
  '(mouseleave)': 'mouseLeave$.next()',
54
52
  }, changeDetection: ChangeDetectionStrategy.OnPush, template: "@if (content(); as content) {\n\t<div class=\"tooltip\" [class]=\"contentPositionClasses()\" [innerHtml]=\"content\"></div>\n}\n", styles: ["@layer components{.tooltip{--components-tooltip-background-color: var(--palettes-neutral-900);--components-tooltip-color: var(--pr-t-color-text-reverse);--components-tooltip-max-width: 15rem;--components-tooltip-transformOrigin: center;--components-tooltip-margin: 0;background-color:var(--components-tooltip-background-color);color:var(--components-tooltip-color);padding-block:var(--pr-t-spacings-50);padding-inline:var(--pr-t-spacings-100);max-inline-size:var(--components-tooltip-max-width);border-radius:var(--pr-t-border-radius-default);font:var(--pr-t-font-body-XS);transform-origin:var(--components-tooltip-transformOrigin);margin:var(--components-tooltip-margin);text-align:center;inline-size:fit-content;animation-name:scaleIn;animation-duration:var(--commons-animations-durations-fast);animation-iteration-count:1;overflow-wrap:break-word}@keyframes scaleIn{0%{transform:scale(0)}to{transform:scale(1)}}.tooltip:empty{display:none}@supports (background-image: -webkit-named-image(i)){@media(hover:hover){[luTooltipWhenEllipsis]:after{content:\"\";display:block}}}}@layer mods{.tooltip.is-above{--components-tooltip-transformOrigin: bottom center}.tooltip.is-below{--components-tooltip-transformOrigin: top center}.tooltip.is-before{--components-tooltip-transformOrigin: center right}.tooltip.is-before.is-above{--components-tooltip-transformOrigin: bottom right}.tooltip.is-before.is-below{--components-tooltip-transformOrigin: top right}.tooltip.is-after{--components-tooltip-transformOrigin: center left}.tooltip.is-after.is-above{--components-tooltip-transformOrigin: bottom left}.tooltip.is-after.is-below{--components-tooltip-transformOrigin: top left}}\n"] }]
55
53
  }] });
56
54
 
57
55
  class EllipsisRuler {
58
- // As EllipsisRuler is a singleton, we can use shareReplay so the first subscriber starts the timer and the last one stops it
59
- // The timer allows us to alternate between read and write phases
60
- #interval;
61
- #readPhase;
62
- #writePhase;
63
- #document;
56
+ #document = inject(DOCUMENT);
57
+ #parentMasked = this.#document.createElement('div');
64
58
  constructor() {
65
- // As EllipsisRuler is a singleton, we can use shareReplay so the first subscriber starts the timer and the last one stops it
66
- // The timer allows us to alternate between read and write phases
67
- this.#interval = timer(100, 100).pipe(shareReplay({ refCount: true, bufferSize: 1 }));
68
- this.#readPhase = this.#interval.pipe(filter((n) => n % 2 === 0));
69
- this.#writePhase = this.#interval.pipe(filter((n) => n % 2 === 1));
70
- this.#document = inject(DOCUMENT);
71
- this.parentMasked = this.#document.createElement('div');
72
- this.parentMasked.classList.add('pr-u-mask');
73
- this.parentMasked.setAttribute('aria-hidden', 'true');
74
- this.#document.body.appendChild(this.parentMasked);
59
+ this.#parentMasked.classList.add('pr-u-mask');
60
+ this.#parentMasked.setAttribute('aria-hidden', 'true');
61
+ this.#document.body.appendChild(this.#parentMasked);
75
62
  }
76
63
  /**
77
- * Hacky af but let's explain everything
78
- * This method checks for ellipsis by cloning the node and checking its width against original element.
64
+ * Checks for ellipsis by cloning the node and comparing its unconstrained width against the original element.
79
65
  *
80
66
  * We used to do this using scrollWidth but the thing is, it's a rounded value. Sometimes,
81
67
  * you'd get true while it should be false and vice-versa, because of rounding.
82
68
  *
83
69
  * So we duplicate the properties we're interested in on the element to be tested to calculate its ideal size,
84
70
  * which we then compare with its current size.
85
- *
86
- * To avoid doing multiple reflow per check, we wait for the next microtask on each key step of the process:
87
- * - After computing element style
88
- * - After cloning the element and appending it to the DOM
89
- * - After computing the width of the cloned element
90
- * - After removing the cloned element from the DOM
91
- *
92
- * This way, we have 2 reflows per check, no matter how many elements are checked in a row.
93
71
  */
94
72
  async hasEllipsis(element) {
95
- let elementStyle;
96
- let bodyStyle;
97
- await this.#readOperation(() => {
98
- elementStyle = getComputedStyle(element);
99
- bodyStyle = getComputedStyle(document.body);
100
- });
73
+ await this.#nextFrame();
74
+ const elementStyle = getComputedStyle(element);
75
+ const bodyStyle = getComputedStyle(this.#document.body);
101
76
  if (elementStyle.textOverflow !== 'ellipsis') {
102
77
  return false;
103
78
  }
104
79
  const { padding, borderWidth, borderStyle, boxSizing, fontFamily, fontWeight, fontStyle } = elementStyle;
105
80
  const fontSize = (Number(elementStyle.fontSize.replace('px', '')) / Number(bodyStyle.fontSize.replace('px', ''))).toString() + 'rem';
106
- let elementCloned;
107
- await this.#writeOperation(() => {
108
- elementCloned = this.#document.createElement('div');
109
- Object.assign(elementCloned.style, {
110
- inlineSize: 'fit-content',
111
- whiteSpace: 'nowrap',
112
- position: 'absolute',
113
- visibility: 'hidden',
114
- padding,
115
- borderWidth,
116
- borderStyle,
117
- boxSizing,
118
- fontFamily,
119
- fontWeight,
120
- fontStyle,
121
- fontSize,
122
- });
123
- this.parentMasked.appendChild(elementCloned);
124
- elementCloned.innerHTML = element.innerHTML;
81
+ await this.#nextFrame();
82
+ const elementCloned = this.#document.createElement('div');
83
+ Object.assign(elementCloned.style, {
84
+ inlineSize: 'fit-content',
85
+ whiteSpace: 'nowrap',
86
+ position: 'absolute',
87
+ visibility: 'hidden',
88
+ padding,
89
+ borderWidth,
90
+ borderStyle,
91
+ boxSizing,
92
+ fontFamily,
93
+ fontWeight,
94
+ fontStyle,
95
+ fontSize,
125
96
  });
126
- // To avoid multiple reflows, we wait for the next microtask before calculating the width
97
+ this.#parentMasked.appendChild(elementCloned);
98
+ elementCloned.innerHTML = element.innerHTML;
127
99
  try {
128
- let clonedElementWidth;
129
- let elementWidth;
130
- await this.#readOperation(() => {
131
- clonedElementWidth = elementCloned.getBoundingClientRect().width;
132
- elementWidth = element.getBoundingClientRect().width;
133
- });
100
+ const clonedElementWidth = elementCloned.getBoundingClientRect().width;
101
+ const elementWidth = element.getBoundingClientRect().width;
134
102
  return clonedElementWidth > elementWidth;
135
103
  }
136
104
  catch {
137
105
  return false;
138
106
  }
139
107
  finally {
140
- await this.#writeOperation(() => {
141
- this.parentMasked.removeChild(elementCloned);
142
- });
108
+ this.#parentMasked.removeChild(elementCloned);
143
109
  }
144
110
  }
145
- // To avoid multiple reflows, we wait for the next read phase before computing/reading element style
146
- async #readOperation(operation) {
147
- await firstValueFrom(this.#readPhase);
148
- operation();
149
- }
150
- // To avoid multiple reflows, we wait for the next write before inserting/removing the cloned element
151
- async #writeOperation(operation) {
152
- await firstValueFrom(this.#writePhase);
153
- operation();
111
+ #nextFrame() {
112
+ return new Promise((resolve) => requestAnimationFrame(() => resolve()));
154
113
  }
155
114
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: EllipsisRuler, deps: [], target: i0.ɵɵFactoryTarget.Injectable }); }
156
115
  static { this.ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: EllipsisRuler, providedIn: 'root' }); }
@@ -166,40 +125,21 @@ class LuTooltipTriggerDirective {
166
125
  #host;
167
126
  #renderer;
168
127
  #ruler;
169
- #zone;
170
128
  #injector;
171
129
  #destroyRef;
172
- #previousTickContent;
173
- #innerTextChange$;
174
- #hasEllipsis$;
130
+ #isVisible;
131
+ #ellipsisTrigger;
175
132
  #hasEllipsis;
176
133
  #action;
177
134
  #realAction;
178
- onMouseEnter() {
179
- this.#action.set('open');
180
- }
181
- onMouseLeave() {
182
- this.#action.set('close');
183
- }
184
- onFocus() {
185
- if (this.#host.nativeElement.getAttribute('aria-expanded') !== 'true') {
186
- this.#action.set('open');
187
- }
188
- }
189
- onBlur() {
190
- this.#action.set('close');
191
- }
192
135
  #effectRef;
193
- #idEffectRef;
194
136
  constructor() {
195
137
  this.#overlay = inject(Overlay);
196
138
  this.#host = inject(ElementRef);
197
139
  this.#renderer = inject(Renderer2);
198
140
  this.#ruler = inject(EllipsisRuler);
199
- this.#zone = inject(NgZone, { optional: true });
200
141
  this.#injector = inject(Injector);
201
142
  this.#destroyRef = inject(DestroyRef);
202
- this.#previousTickContent = this.#host.nativeElement.innerText;
203
143
  this.luTooltipInput = input('', { ...(ngDevMode ? { debugName: "luTooltipInput" } : {}), alias: 'luTooltip' });
204
144
  this.luTooltip = linkedSignal(() => this.luTooltipInput(), ...(ngDevMode ? [{ debugName: "luTooltip" }] : []));
205
145
  this.luTooltipEnterDelay = input(300, { ...(ngDevMode ? { debugName: "luTooltipEnterDelay" } : {}), transform: numberAttribute });
@@ -211,30 +151,20 @@ class LuTooltipTriggerDirective {
211
151
  this.luTooltipWhenEllipsis = linkedSignal(() => this.luTooltipWhenEllipsisInput(), ...(ngDevMode ? [{ debugName: "luTooltipWhenEllipsis" }] : []));
212
152
  this.luTooltipAnchor = input(this.#host, ...(ngDevMode ? [{ debugName: "luTooltipAnchor" }] : []));
213
153
  this.id = input(`${this.#host.nativeElement.tagName.toLowerCase()}-tooltip-${nextId++}`, ...(ngDevMode ? [{ debugName: "id" }] : []));
214
- this.resize$ = new Observable((observer) => {
215
- const resizeObserver = new ResizeObserver(() => {
216
- observer.next();
217
- });
218
- resizeObserver.observe(this.#host.nativeElement);
219
- return () => {
220
- resizeObserver.disconnect();
221
- };
222
- });
223
- this.#innerTextChange$ = new Subject();
224
- this.#hasEllipsis$ = combineLatest([
225
- toObservable(
226
- // 1. Group necessary inputs
227
- computed(() => ({ whenEllipsis: this.luTooltipWhenEllipsis(), disabled: this.luTooltipDisabled() }))),
228
- // Resend resize events to trigger the check
229
- this.resize$.pipe(debounceTime(150)),
230
- // Include content changes
231
- this.#innerTextChange$,
232
- ]).pipe(
233
- // 2. Keep only necessary inputs
234
- filter$1(([{ whenEllipsis, disabled }]) => !disabled && whenEllipsis),
235
- // 3. Check for ellipsis
236
- switchMap(() => this.runOutsideZoneJS(() => this.#ruler.hasEllipsis(this.#host.nativeElement))));
237
- this.#hasEllipsis = toSignal(this.#hasEllipsis$, { initialValue: false });
154
+ this.ariaDescribedBy = computed(() => {
155
+ if (this.luTooltipDisabled() || this.luTooltipWhenEllipsis() || this.luTooltipOnlyForDisplay()) {
156
+ return null;
157
+ }
158
+ return `${this.id()}-panel`;
159
+ }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
160
+ this.#isVisible = signal(false, ...(ngDevMode ? [{ debugName: "#isVisible" }] : []));
161
+ this.#ellipsisTrigger = signal(0, ...(ngDevMode ? [{ debugName: "#ellipsisTrigger" }] : []));
162
+ this.#hasEllipsis = toSignal(toObservable(this.#ellipsisTrigger).pipe(debounceTime(150), switchMap(() => {
163
+ if (!this.luTooltipWhenEllipsis() || this.luTooltipDisabled()) {
164
+ return of(false);
165
+ }
166
+ return from(this.#ruler.hasEllipsis(this.#host.nativeElement));
167
+ })), { initialValue: false });
238
168
  this.#action = signal(null, ...(ngDevMode ? [{ debugName: "#action" }] : []));
239
169
  this.#realAction = linkedSignal({ ...(ngDevMode ? { debugName: "#realAction" } : {}), source: this.#action,
240
170
  computation: (action, previous) => {
@@ -244,23 +174,16 @@ class LuTooltipTriggerDirective {
244
174
  // We only filter open events because even if it's disabled while opened,
245
175
  // we want the tooltip to be able to close itself no matter what
246
176
  if (this.luTooltipDisabled()) {
247
- return previous?.value;
177
+ return previous?.value ?? null;
248
178
  }
249
- // If not disabled, let's check for ellipsis if needed
250
179
  if (this.luTooltipWhenEllipsis()) {
251
- return this.#hasEllipsis() ? 'open' : previous.value;
180
+ return this.#hasEllipsis() ? 'open' : (previous?.value ?? null);
252
181
  }
253
- // If it's not disabled and is not triggered based on ellipsis, just return true
254
182
  return 'open';
255
183
  } });
256
- this.ariaDescribedBy = computed(() => {
257
- if (this.luTooltipDisabled() || this.luTooltipWhenEllipsis() || this.luTooltipOnlyForDisplay()) {
258
- return null;
259
- }
260
- return `${this.id()}-panel`;
261
- }, ...(ngDevMode ? [{ debugName: "ariaDescribedBy" }] : []));
184
+ // Action debounce pipeline — kept as Observable since signals can't debounce
262
185
  toObservable(this.#realAction)
263
- .pipe(filter$1(isNotNil), debounce((action) => timer(action === 'open' ? this.luTooltipEnterDelay() : this.luTooltipLeaveDelay())), tap((event) => {
186
+ .pipe(filter(isNotNil), debounce((action) => timer(action === 'open' ? this.luTooltipEnterDelay() : this.luTooltipLeaveDelay())), tap((event) => {
264
187
  if (event === 'open') {
265
188
  this.openTooltip();
266
189
  }
@@ -277,27 +200,88 @@ class LuTooltipTriggerDirective {
277
200
  this.setAccessibilityProperties(null);
278
201
  }
279
202
  });
203
+ effect((onCleanup) => {
204
+ if (!this.luTooltipWhenEllipsis() || this.luTooltipDisabled()) {
205
+ this.#isVisible.set(false);
206
+ return;
207
+ }
208
+ const observer = new IntersectionObserver((entries) => this.#isVisible.set(entries.some((e) => e.isIntersecting)), { rootMargin: '100px' });
209
+ observer.observe(this.#host.nativeElement);
210
+ onCleanup(() => observer.disconnect());
211
+ });
212
+ effect((onCleanup) => {
213
+ if (!this.#isVisible() || !this.luTooltipWhenEllipsis() || this.luTooltipDisabled()) {
214
+ return;
215
+ }
216
+ const el = this.#host.nativeElement;
217
+ const bump = () => this.#ellipsisTrigger.update((v) => v + 1);
218
+ const resizeObserver = new ResizeObserver(() => bump());
219
+ resizeObserver.observe(el);
220
+ const mutationObserver = new MutationObserver(() => bump());
221
+ mutationObserver.observe(el, { characterData: true, subtree: true, childList: true });
222
+ // Initial check when element becomes visible — prevents regression where tooltips never appear
223
+ bump();
224
+ onCleanup(() => {
225
+ resizeObserver.disconnect();
226
+ mutationObserver.disconnect();
227
+ });
228
+ });
280
229
  }
281
- ngAfterContentChecked() {
282
- if (this.#previousTickContent != this.#host.nativeElement.innerText) {
283
- this.#innerTextChange$.next();
284
- this.#previousTickContent = this.#host.nativeElement.innerText;
230
+ onMouseEnter() {
231
+ this.#action.set('open');
232
+ }
233
+ onMouseLeave() {
234
+ this.#action.set('close');
235
+ }
236
+ onFocus() {
237
+ if (this.#host.nativeElement.getAttribute('aria-expanded') !== 'true') {
238
+ this.#action.set('open');
285
239
  }
286
240
  }
241
+ onBlur() {
242
+ this.#action.set('close');
243
+ }
244
+ requestOpen() {
245
+ this.#action.set('open');
246
+ }
247
+ requestClose() {
248
+ this.#action.set('close');
249
+ }
287
250
  ngOnDestroy() {
288
251
  this.closeTooltip();
252
+ if (this.overlayRef) {
253
+ this.overlayRef.dispose();
254
+ delete this.overlayRef;
255
+ }
289
256
  }
290
- openTooltip() {
291
- // If tooltip is already opened, don't do anything
257
+ prepareOverlay() {
292
258
  if (this.overlayRef) {
293
259
  return;
294
260
  }
295
- const position = this.legacyPositionBuilder();
296
261
  this.overlayRef = this.#overlay.create({
297
- positionStrategy: position,
298
262
  scrollStrategy: this.#overlay.scrollStrategies.close(),
299
263
  disposeOnNavigation: true,
300
264
  });
265
+ const describedBy = this.ariaDescribedBy();
266
+ if (describedBy !== null) {
267
+ this.overlayRef.overlayElement.id = describedBy;
268
+ }
269
+ }
270
+ openTooltip() {
271
+ if (this.overlayRef?.hasAttached()) {
272
+ return;
273
+ }
274
+ const position = this.legacyPositionBuilder();
275
+ if (!this.overlayRef) {
276
+ this.overlayRef = this.#overlay.create({
277
+ positionStrategy: position,
278
+ scrollStrategy: this.#overlay.scrollStrategies.close(),
279
+ disposeOnNavigation: true,
280
+ });
281
+ }
282
+ else {
283
+ this.overlayRef.updatePositionStrategy(position);
284
+ }
301
285
  const portal = new ComponentPortal(LuTooltipPanelComponent);
302
286
  const ref = this.overlayRef.attach(portal);
303
287
  position.positionChanges
@@ -316,27 +300,23 @@ class LuTooltipTriggerDirective {
316
300
  else {
317
301
  ref.instance.content.set('');
318
302
  }
319
- this.#idEffectRef = _effectWithDeps([this.ariaDescribedBy], (ariaDescribedBy) => {
320
- ref.instance.id.set(ariaDescribedBy);
321
- }, { injector: this.#injector });
322
- // On tooltip leave => trigger close
323
303
  ref.instance.mouseLeave$.pipe(takeUntilDestroyed(ref.instance.destroyRef)).subscribe(() => this.#action.set('close'));
324
- // On tooltip enter => trigger open to keep it opened
325
304
  ref.instance.mouseEnter$.pipe(takeUntilDestroyed(ref.instance.destroyRef)).subscribe(() => this.#action.set('open'));
326
305
  }
327
306
  closeTooltip() {
328
307
  if (this.overlayRef) {
329
308
  this.overlayRef.detach();
330
- delete this.overlayRef;
331
309
  }
332
310
  this.#effectRef?.destroy();
333
- this.#idEffectRef?.destroy();
334
311
  }
335
312
  setAccessibilityProperties(tabindex) {
336
313
  if (tabindex === null) {
337
314
  this.#renderer.removeAttribute(this.#host.nativeElement, 'tabindex');
338
315
  return;
339
316
  }
317
+ if (!this.luTooltipWhenEllipsis() && !this.luTooltipOnlyForDisplay()) {
318
+ this.prepareOverlay();
319
+ }
340
320
  const tag = this.#host.nativeElement.tagName.toLowerCase();
341
321
  const nativelyFocusableTags = ['a', 'button', 'input', 'select', 'textarea'];
342
322
  const isNativelyFocusableTag = nativelyFocusableTags.includes(tag);
@@ -348,14 +328,7 @@ class LuTooltipTriggerDirective {
348
328
  this.#renderer.setAttribute(this.#host.nativeElement, 'role', 'button');
349
329
  }
350
330
  }
351
- runOutsideZoneJS(callback) {
352
- return this.#zone ? this.#zone.runOutsideAngular(callback) : callback();
353
- }
354
- /**********************
355
- *
356
- * LEGACY STUFF TO HANDLE EXISTING POSITIONS
357
- *
358
- ***********************/
331
+ // Legacy position builder to handle existing position API
359
332
  legacyPositionBuilder() {
360
333
  const connectionPosition = {
361
334
  originX: 'start',
@@ -442,12 +415,6 @@ class LuTooltipTriggerDirective {
442
415
  }
443
416
  return x;
444
417
  }
445
- requestClose() {
446
- this.#action.set('close');
447
- }
448
- requestOpen() {
449
- this.#action.set('open');
450
- }
451
418
  static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.8", ngImport: i0, type: LuTooltipTriggerDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive }); }
452
419
  static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "17.1.0", version: "21.0.8", type: LuTooltipTriggerDirective, isStandalone: true, selector: "[luTooltip]", inputs: { luTooltipInput: { classPropertyName: "luTooltipInput", publicName: "luTooltip", isSignal: true, isRequired: false, transformFunction: null }, luTooltipEnterDelay: { classPropertyName: "luTooltipEnterDelay", publicName: "luTooltipEnterDelay", isSignal: true, isRequired: false, transformFunction: null }, luTooltipLeaveDelay: { classPropertyName: "luTooltipLeaveDelay", publicName: "luTooltipLeaveDelay", isSignal: true, isRequired: false, transformFunction: null }, luTooltipDisabled: { classPropertyName: "luTooltipDisabled", publicName: "luTooltipDisabled", isSignal: true, isRequired: false, transformFunction: null }, luTooltipOnlyForDisplay: { classPropertyName: "luTooltipOnlyForDisplay", publicName: "luTooltipOnlyForDisplay", isSignal: true, isRequired: false, transformFunction: null }, luTooltipPosition: { classPropertyName: "luTooltipPosition", publicName: "luTooltipPosition", isSignal: true, isRequired: false, transformFunction: null }, luTooltipWhenEllipsisInput: { classPropertyName: "luTooltipWhenEllipsisInput", publicName: "luTooltipWhenEllipsis", isSignal: true, isRequired: false, transformFunction: null }, luTooltipAnchor: { classPropertyName: "luTooltipAnchor", publicName: "luTooltipAnchor", isSignal: true, isRequired: false, transformFunction: null }, id: { classPropertyName: "id", publicName: "id", isSignal: true, isRequired: false, transformFunction: null } }, host: { listeners: { "mouseenter": "onMouseEnter()", "mouseleave": "onMouseLeave()", "focus": "onFocus()", "blur": "onBlur()" }, properties: { "attr.aria-describedby": "ariaDescribedBy()", "attr.id": "id()" } }, exportAs: ["luTooltip"], ngImport: i0 }); }
453
420
  }