@ng-cn/core 1.0.17 → 1.0.20
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.
- package/package.json +6 -5
- package/src/app/lib/components/ui/alert-dialog/alert-dialog-content.component.ts +22 -21
- package/src/app/lib/components/ui/avatar/ui-avatar.component.ts +4 -0
- package/src/app/lib/components/ui/calendar/calendar.component.ts +70 -13
- package/src/app/lib/components/ui/carousel/carousel-content.component.ts +1 -0
- package/src/app/lib/components/ui/carousel/carousel-item.component.ts +1 -0
- package/src/app/lib/components/ui/carousel/carousel-next.component.ts +1 -0
- package/src/app/lib/components/ui/carousel/carousel-previous.component.ts +1 -0
- package/src/app/lib/components/ui/carousel/carousel.component.ts +1 -0
- package/src/app/lib/components/ui/chart/chart-container.component.ts +1 -0
- package/src/app/lib/components/ui/chart/chart-legend-content.component.ts +1 -0
- package/src/app/lib/components/ui/chart/chart-legend.component.ts +5 -5
- package/src/app/lib/components/ui/chart/chart-tooltip-content.component.ts +5 -5
- package/src/app/lib/components/ui/chart/chart-tooltip.component.ts +5 -5
- package/src/app/lib/components/ui/chart/chart.component.ts +1 -0
- package/src/app/lib/components/ui/checkbox/checkbox.component.ts +1 -1
- package/src/app/lib/components/ui/collapsible/collapsible-content.component.ts +2 -1
- package/src/app/lib/components/ui/collapsible/collapsible-context.ts +1 -0
- package/src/app/lib/components/ui/collapsible/collapsible-trigger.component.ts +1 -0
- package/src/app/lib/components/ui/collapsible/collapsible.component.ts +3 -0
- package/src/app/lib/components/ui/context-menu/context-menu-content.component.ts +49 -17
- package/src/app/lib/components/ui/context-menu/context-menu-sub-content.component.ts +2 -0
- package/src/app/lib/components/ui/context-menu/context-menu-sub-trigger.component.ts +30 -1
- package/src/app/lib/components/ui/context-menu/context-menu-sub.component.ts +3 -0
- package/src/app/lib/components/ui/country-selector/country-data.ts +63 -0
- package/src/app/lib/components/ui/country-selector/country-selector.component.ts +199 -0
- package/src/app/lib/components/ui/country-selector/index.ts +2 -0
- package/src/app/lib/components/ui/date-picker/date-picker.component.ts +48 -5
- package/src/app/lib/components/ui/dialog/dialog-content.component.ts +27 -20
- package/src/app/lib/components/ui/direction/direction-context.ts +9 -0
- package/src/app/lib/components/ui/direction/direction.component.ts +48 -0
- package/src/app/lib/components/ui/direction/index.ts +2 -0
- package/src/app/lib/components/ui/drawer/drawer-content.component.ts +44 -0
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-content.component.ts +25 -23
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-radio-item.component.ts +1 -0
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-content.component.ts +2 -0
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub-trigger.component.ts +28 -2
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-sub.component.ts +3 -0
- package/src/app/lib/components/ui/dropdown-menu/dropdown-menu-trigger.component.ts +25 -0
- package/src/app/lib/components/ui/empty/empty-action.component.ts +1 -0
- package/src/app/lib/components/ui/empty/empty-description.component.ts +1 -0
- package/src/app/lib/components/ui/empty/empty-icon.component.ts +2 -1
- package/src/app/lib/components/ui/empty/empty-title.component.ts +1 -0
- package/src/app/lib/components/ui/empty/empty.component.ts +1 -0
- package/src/app/lib/components/ui/field/field-content.component.ts +34 -0
- package/src/app/lib/components/ui/field/field-description.component.ts +35 -0
- package/src/app/lib/components/ui/field/field-error.component.ts +48 -0
- package/src/app/lib/components/ui/field/field-group.component.ts +34 -0
- package/src/app/lib/components/ui/field/field-label.component.ts +46 -0
- package/src/app/lib/components/ui/field/field-legend.component.ts +41 -0
- package/src/app/lib/components/ui/field/field-separator.component.ts +49 -0
- package/src/app/lib/components/ui/field/field-set.component.ts +37 -0
- package/src/app/lib/components/ui/field/field-title.component.ts +30 -0
- package/src/app/lib/components/ui/field/field.component.ts +66 -0
- package/src/app/lib/components/ui/field/index.ts +15 -0
- package/src/app/lib/components/ui/form/form-description.component.ts +2 -2
- package/src/app/lib/components/ui/hover-card/hover-card-content.component.ts +108 -60
- package/src/app/lib/components/ui/hover-card/hover-card-context.ts +4 -2
- package/src/app/lib/components/ui/hover-card/hover-card-trigger.component.ts +5 -3
- package/src/app/lib/components/ui/hover-card/hover-card.component.ts +8 -3
- package/src/app/lib/components/ui/input-group/input-group-addon.component.ts +1 -0
- package/src/app/lib/components/ui/input-group/input-group-input.component.ts +1 -0
- package/src/app/lib/components/ui/input-group/input-group.component.ts +1 -0
- package/src/app/lib/components/ui/item/index.ts +21 -0
- package/src/app/lib/components/ui/item/item-actions.component.ts +29 -0
- package/src/app/lib/components/ui/item/item-content.component.ts +31 -0
- package/src/app/lib/components/ui/item/item-description.component.ts +30 -0
- package/src/app/lib/components/ui/item/item-footer.component.ts +30 -0
- package/src/app/lib/components/ui/item/item-group.component.ts +32 -0
- package/src/app/lib/components/ui/item/item-header.component.ts +30 -0
- package/src/app/lib/components/ui/item/item-media.component.ts +63 -0
- package/src/app/lib/components/ui/item/item-separator.component.ts +33 -0
- package/src/app/lib/components/ui/item/item-title.component.ts +27 -0
- package/src/app/lib/components/ui/item/item.component.ts +77 -0
- package/src/app/lib/components/ui/menubar/menubar-content.component.ts +1 -1
- package/src/app/lib/components/ui/navigation-menu/navigation-menu-content.component.ts +7 -1
- package/src/app/lib/components/ui/navigation-menu/navigation-menu-context.ts +14 -0
- package/src/app/lib/components/ui/navigation-menu/navigation-menu-item.component.ts +9 -4
- package/src/app/lib/components/ui/navigation-menu/navigation-menu-trigger.component.ts +69 -2
- package/src/app/lib/components/ui/navigation-menu/navigation-menu.component.ts +32 -4
- package/src/app/lib/components/ui/pagination/pagination.component.ts +3 -1
- package/src/app/lib/components/ui/phone-input/index.ts +1 -0
- package/src/app/lib/components/ui/phone-input/phone-input.component.ts +169 -0
- package/src/app/lib/components/ui/popover/popover-content.component.ts +11 -0
- package/src/app/lib/components/ui/popover/popover-context.ts +2 -0
- package/src/app/lib/components/ui/popover/popover.component.ts +4 -0
- package/src/app/lib/components/ui/progress/progress.component.ts +1 -2
- package/src/app/lib/components/ui/resizable/resizable-handle.component.ts +2 -2
- package/src/app/lib/components/ui/scroll-area/scroll-area.component.ts +7 -6
- package/src/app/lib/components/ui/segmented/segmented-item.component.ts +1 -0
- package/src/app/lib/components/ui/segmented/segmented.component.ts +1 -0
- package/src/app/lib/components/ui/select/select-content.component.ts +35 -15
- package/src/app/lib/components/ui/select/select-context.ts +10 -0
- package/src/app/lib/components/ui/select/select-item.component.ts +25 -7
- package/src/app/lib/components/ui/select/select-trigger.component.ts +6 -13
- package/src/app/lib/components/ui/select/select.component.ts +46 -0
- package/src/app/lib/components/ui/sheet/sheet-content.component.ts +23 -6
- package/src/app/lib/components/ui/slider/slider.component.ts +2 -2
- package/src/app/lib/components/ui/sonner/index.ts +2 -0
- package/src/app/lib/components/ui/sonner/sonner.component.ts +70 -0
- package/src/app/lib/components/ui/switch/switch.component.ts +1 -14
- package/src/app/lib/components/ui/tabs/tabs-list.component.ts +20 -0
- package/src/app/lib/components/ui/tabs/tabs-trigger.component.ts +0 -1
- package/src/app/lib/components/ui/textarea/textarea.component.ts +110 -10
- package/src/app/lib/components/ui/toast/toast.service.ts +1 -1
- package/src/app/lib/components/ui/toggle/toggle.component.ts +12 -6
- package/src/app/lib/components/ui/tooltip/tooltip-content.component.ts +141 -17
- package/src/app/lib/components/ui/tooltip/tooltip-context.ts +3 -1
- package/src/app/lib/components/ui/tooltip/tooltip-provider.component.ts +1 -1
- package/src/app/lib/components/ui/tooltip/tooltip-trigger.component.ts +5 -2
- package/src/app/lib/components/ui/tooltip/tooltip.component.ts +3 -1
|
@@ -1,31 +1,91 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
AfterViewInit,
|
|
4
|
+
ChangeDetectionStrategy,
|
|
5
|
+
Component,
|
|
6
|
+
computed,
|
|
7
|
+
ElementRef,
|
|
8
|
+
forwardRef,
|
|
9
|
+
inject,
|
|
10
|
+
input,
|
|
11
|
+
signal,
|
|
12
|
+
} from '@angular/core';
|
|
13
|
+
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
3
14
|
|
|
4
15
|
/**
|
|
5
16
|
* Textarea component that applies shadcn textarea styles.
|
|
17
|
+
* Implements ControlValueAccessor for Angular Forms integration.
|
|
18
|
+
* Applied as an attribute on a native <textarea> element.
|
|
6
19
|
*
|
|
7
20
|
* @example
|
|
8
21
|
* <!-- Basic textarea -->
|
|
9
|
-
* <Textarea placeholder="
|
|
10
|
-
*
|
|
11
|
-
* <!-- With rows -->
|
|
12
|
-
* <Textarea rows="5" placeholder="Description"></Textarea>
|
|
22
|
+
* <textarea Textarea placeholder="Type your message here."></textarea>
|
|
13
23
|
*
|
|
14
24
|
* <!-- Disabled -->
|
|
15
|
-
* <Textarea disabled placeholder="Disabled"></
|
|
25
|
+
* <textarea Textarea disabled placeholder="Disabled"></textarea>
|
|
26
|
+
*
|
|
27
|
+
* <!-- Auto-resize -->
|
|
28
|
+
* <textarea Textarea [autoResize]="true" placeholder="Grows as you type..."></textarea>
|
|
29
|
+
*
|
|
30
|
+
* <!-- With reactive forms -->
|
|
31
|
+
* <textarea Textarea formControlName="message" placeholder="Enter message"></textarea>
|
|
16
32
|
*/
|
|
17
33
|
@Component({
|
|
18
|
-
selector: 'Textarea',
|
|
19
|
-
template:
|
|
34
|
+
selector: 'textarea[Textarea]',
|
|
35
|
+
template: '',
|
|
20
36
|
host: {
|
|
21
37
|
'[class]': 'computedClass()',
|
|
22
|
-
'
|
|
38
|
+
'[disabled]': 'isDisabled()',
|
|
39
|
+
'[id]': 'id()',
|
|
40
|
+
'[name]': 'name()',
|
|
41
|
+
'[placeholder]': 'placeholder()',
|
|
42
|
+
'[rows]': 'rows()',
|
|
43
|
+
'[attr.data-slot]': '"textarea"',
|
|
44
|
+
'[attr.data-auto-resize]': 'autoResize() || null',
|
|
45
|
+
'[attr.aria-invalid]': 'ariaInvalid() || null',
|
|
46
|
+
'[attr.aria-describedby]': 'ariaDescribedBy() || null',
|
|
47
|
+
'(input)': 'onInput($event)',
|
|
48
|
+
'(blur)': 'onTouched()',
|
|
23
49
|
},
|
|
50
|
+
providers: [
|
|
51
|
+
{
|
|
52
|
+
provide: NG_VALUE_ACCESSOR,
|
|
53
|
+
useExisting: forwardRef(() => Textarea),
|
|
54
|
+
multi: true,
|
|
55
|
+
},
|
|
56
|
+
],
|
|
24
57
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
25
58
|
})
|
|
26
|
-
export class Textarea {
|
|
59
|
+
export class Textarea implements ControlValueAccessor, AfterViewInit {
|
|
60
|
+
/** Placeholder text */
|
|
61
|
+
readonly placeholder = input<string>('');
|
|
62
|
+
/** Textarea id attribute */
|
|
63
|
+
readonly id = input<string>('');
|
|
64
|
+
/** Textarea name attribute */
|
|
65
|
+
readonly name = input<string>('');
|
|
66
|
+
/** Number of visible text rows */
|
|
67
|
+
readonly rows = input<number>(3);
|
|
68
|
+
/** Whether the textarea is disabled */
|
|
69
|
+
readonly disabled = input(false, {
|
|
70
|
+
transform: (value: boolean | string) => value === '' || value === true || value === 'true',
|
|
71
|
+
});
|
|
72
|
+
/** Aria-invalid state for error display */
|
|
73
|
+
readonly ariaInvalid = input<boolean | undefined>(undefined, { alias: 'aria-invalid' });
|
|
74
|
+
/** Aria-describedby for accessibility */
|
|
75
|
+
readonly ariaDescribedBy = input<string | undefined>(undefined, { alias: 'aria-describedby' });
|
|
27
76
|
/** Additional CSS classes to apply */
|
|
28
77
|
readonly class = input<string>('');
|
|
78
|
+
/** When true, the textarea height adjusts automatically as the user types */
|
|
79
|
+
readonly autoResize = input<boolean>(false);
|
|
80
|
+
|
|
81
|
+
private readonly _elementRef = inject(ElementRef<HTMLTextAreaElement>);
|
|
82
|
+
|
|
83
|
+
/** Internal disabled state (set by ControlValueAccessor) */
|
|
84
|
+
protected readonly isDisabled = signal<boolean>(false);
|
|
85
|
+
|
|
86
|
+
/** ControlValueAccessor callbacks */
|
|
87
|
+
private onChange: (value: string) => void = () => {};
|
|
88
|
+
protected onTouched: () => void = () => {};
|
|
29
89
|
|
|
30
90
|
/** Computed class combining base styles and custom classes */
|
|
31
91
|
protected readonly computedClass = computed(() =>
|
|
@@ -33,7 +93,47 @@ export class Textarea {
|
|
|
33
93
|
'placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
|
34
94
|
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]',
|
|
35
95
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
|
96
|
+
'resize-none data-[auto-resize=true]:resize-none',
|
|
36
97
|
this.class(),
|
|
37
98
|
),
|
|
38
99
|
);
|
|
100
|
+
|
|
101
|
+
ngAfterViewInit(): void {
|
|
102
|
+
// Ensure element is available after hydration
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ControlValueAccessor implementation
|
|
106
|
+
writeValue(value: string): void {
|
|
107
|
+
const el = this._elementRef.nativeElement;
|
|
108
|
+
el.value = value ?? '';
|
|
109
|
+
if (this.autoResize()) {
|
|
110
|
+
el.style.height = 'auto';
|
|
111
|
+
el.style.height = el.scrollHeight + 'px';
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
registerOnChange(fn: (value: string) => void): void {
|
|
115
|
+
this.onChange = fn;
|
|
116
|
+
}
|
|
117
|
+
registerOnTouched(fn: () => void): void {
|
|
118
|
+
this.onTouched = fn;
|
|
119
|
+
}
|
|
120
|
+
setDisabledState(isDisabled: boolean): void {
|
|
121
|
+
this.isDisabled.set(isDisabled);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Focus the textarea element */
|
|
125
|
+
focus(): void {
|
|
126
|
+
this._elementRef.nativeElement.focus();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Handle input events */
|
|
130
|
+
protected onInput(event: Event): void {
|
|
131
|
+
const target = event.target as HTMLTextAreaElement;
|
|
132
|
+
this.onChange(target.value);
|
|
133
|
+
if (this.autoResize()) {
|
|
134
|
+
const el = this._elementRef.nativeElement;
|
|
135
|
+
el.style.height = 'auto';
|
|
136
|
+
el.style.height = el.scrollHeight + 'px';
|
|
137
|
+
}
|
|
138
|
+
}
|
|
39
139
|
}
|
|
@@ -178,7 +178,7 @@ export class ToastService {
|
|
|
178
178
|
this._toasts.update((toasts) => [...toasts, newToast]);
|
|
179
179
|
|
|
180
180
|
const duration = toast.duration ?? 4000;
|
|
181
|
-
if (duration > 0) {
|
|
181
|
+
if (duration > 0 && typeof window !== 'undefined') {
|
|
182
182
|
setTimeout(() => this.dismiss(id), duration);
|
|
183
183
|
}
|
|
184
184
|
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
input,
|
|
8
8
|
model,
|
|
9
9
|
output,
|
|
10
|
+
signal,
|
|
10
11
|
} from '@angular/core';
|
|
11
12
|
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms';
|
|
12
13
|
import { toggleVariants, type ToggleVariants } from './toggle-variants';
|
|
@@ -97,8 +98,8 @@ export type ToggleProps = {
|
|
|
97
98
|
type: 'button',
|
|
98
99
|
'[attr.aria-pressed]': 'pressed()',
|
|
99
100
|
'[attr.data-state]': 'state()',
|
|
100
|
-
'[attr.data-disabled]': '
|
|
101
|
-
'[attr.disabled]': '
|
|
101
|
+
'[attr.data-disabled]': 'isDisabled() ? "" : null',
|
|
102
|
+
'[attr.disabled]': 'isDisabled() ? "" : null',
|
|
102
103
|
'(click)': 'toggle()',
|
|
103
104
|
'data-slot': 'toggle',
|
|
104
105
|
},
|
|
@@ -136,6 +137,9 @@ export class Toggle implements ControlValueAccessor {
|
|
|
136
137
|
/** Additional CSS classes to apply */
|
|
137
138
|
readonly class = input<string>('');
|
|
138
139
|
|
|
140
|
+
/** Whether the toggle is effectively disabled (input or Angular Forms) */
|
|
141
|
+
protected readonly isDisabled = computed(() => this.disabled() || this.isFormsDisabled());
|
|
142
|
+
|
|
139
143
|
/** Current state for data attribute */
|
|
140
144
|
protected readonly state = computed((): ToggleState => (this.pressed() ? 'on' : 'off'));
|
|
141
145
|
/** Computed class combining variants and custom classes */
|
|
@@ -149,17 +153,19 @@ export class Toggle implements ControlValueAccessor {
|
|
|
149
153
|
),
|
|
150
154
|
);
|
|
151
155
|
|
|
156
|
+
/** Tracks disabled state set by Angular Forms (.disable() / .enable()) */
|
|
157
|
+
private readonly isFormsDisabled = signal<boolean>(false);
|
|
158
|
+
|
|
152
159
|
/** ControlValueAccessor callbacks */
|
|
153
160
|
private onChange: (value: boolean) => void = () => {};
|
|
154
161
|
private onTouched: () => void = () => {};
|
|
155
|
-
setDisabledState
|
|
156
|
-
|
|
157
|
-
// Angular forms will call this but we use the input binding
|
|
162
|
+
setDisabledState(isDisabled: boolean): void {
|
|
163
|
+
this.isFormsDisabled.set(isDisabled);
|
|
158
164
|
}
|
|
159
165
|
|
|
160
166
|
/** Toggle the pressed state */
|
|
161
167
|
toggle(): void {
|
|
162
|
-
if (!this.
|
|
168
|
+
if (!this.isDisabled()) {
|
|
163
169
|
const newValue = !this.pressed();
|
|
164
170
|
this.pressed.set(newValue);
|
|
165
171
|
this.onChange(newValue);
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
-
import { cn, Presence } from '@/lib/utils';
|
|
2
|
-
import {
|
|
1
|
+
import { Align, cn, computePosition, getTransformOrigin, Presence, Side } from '@/lib/utils';
|
|
2
|
+
import {
|
|
3
|
+
afterNextRender,
|
|
4
|
+
ChangeDetectionStrategy,
|
|
5
|
+
Component,
|
|
6
|
+
computed,
|
|
7
|
+
effect,
|
|
8
|
+
ElementRef,
|
|
9
|
+
inject,
|
|
10
|
+
Injector,
|
|
11
|
+
input,
|
|
12
|
+
signal,
|
|
13
|
+
} from '@angular/core';
|
|
3
14
|
import { TOOLTIP_CONTEXT, TooltipAlign, TooltipSide } from './tooltip-context';
|
|
4
15
|
|
|
5
16
|
export type TooltipContentState = 'open' | 'closed';
|
|
@@ -34,7 +45,8 @@ export interface TooltipContentProps {
|
|
|
34
45
|
*
|
|
35
46
|
* @description
|
|
36
47
|
* TooltipContent displays the actual tooltip content. It supports positioning
|
|
37
|
-
* on different sides and alignments relative to the trigger
|
|
48
|
+
* on different sides and alignments relative to the trigger using fixed positioning
|
|
49
|
+
* via computePosition to avoid clipping inside overflow:hidden ancestors.
|
|
38
50
|
*
|
|
39
51
|
* ## Features
|
|
40
52
|
* - Configurable side (top, right, bottom, left)
|
|
@@ -42,10 +54,14 @@ export interface TooltipContentProps {
|
|
|
42
54
|
* - Customizable offset from trigger
|
|
43
55
|
* - Smooth enter/exit animations
|
|
44
56
|
* - Uses Presence for proper exit animations
|
|
57
|
+
* - Fixed positioning — not clipped by overflow:hidden ancestors
|
|
58
|
+
* - Escape key to dismiss
|
|
59
|
+
* - disableHoverableContent support
|
|
45
60
|
*
|
|
46
61
|
* ## Accessibility
|
|
47
62
|
* - `role="tooltip"` on the element
|
|
48
63
|
* - Unique ID for aria-describedby relationship with trigger
|
|
64
|
+
* - Escape key dismisses tooltip (WCAG 1.4.13)
|
|
49
65
|
*
|
|
50
66
|
* @example Basic usage
|
|
51
67
|
* ```html
|
|
@@ -81,10 +97,12 @@ export interface TooltipContentProps {
|
|
|
81
97
|
<div
|
|
82
98
|
[class]="computedClass()"
|
|
83
99
|
[attr.data-state]="state()"
|
|
84
|
-
[attr.data-side]="
|
|
85
|
-
[attr.data-align]="
|
|
100
|
+
[attr.data-side]="computedSide()"
|
|
101
|
+
[attr.data-align]="computedAlign()"
|
|
102
|
+
[style]="positionStyles()"
|
|
86
103
|
role="tooltip"
|
|
87
104
|
[id]="context.tooltipId"
|
|
105
|
+
(mouseenter)="onMouseEnter()"
|
|
88
106
|
>
|
|
89
107
|
<ng-content />
|
|
90
108
|
</div>
|
|
@@ -93,10 +111,35 @@ export interface TooltipContentProps {
|
|
|
93
111
|
host: {
|
|
94
112
|
'attr.data-slot': '"tooltip-content"',
|
|
95
113
|
class: 'contents',
|
|
114
|
+
'(document:keydown.escape)': 'onEscape()',
|
|
96
115
|
},
|
|
97
116
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
98
117
|
})
|
|
99
118
|
export class TooltipContent {
|
|
119
|
+
constructor() {
|
|
120
|
+
// Recalculate position when open state changes (browser-only via afterNextRender)
|
|
121
|
+
effect(() => {
|
|
122
|
+
const isOpen = this.context.open();
|
|
123
|
+
if (isOpen) {
|
|
124
|
+
this.isPositioned.set(false);
|
|
125
|
+
afterNextRender(
|
|
126
|
+
() => {
|
|
127
|
+
this.schedulePositionUpdate();
|
|
128
|
+
},
|
|
129
|
+
{ injector: this._injector },
|
|
130
|
+
);
|
|
131
|
+
} else {
|
|
132
|
+
this.cancelScheduledPositionUpdate();
|
|
133
|
+
this.isPositioned.set(false);
|
|
134
|
+
this.positionStyles.set({
|
|
135
|
+
position: 'fixed',
|
|
136
|
+
top: '-9999px',
|
|
137
|
+
left: '-9999px',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
100
143
|
/** The preferred side of the trigger to render against */
|
|
101
144
|
readonly side = input<TooltipSide>('top');
|
|
102
145
|
/** The distance in pixels from the trigger */
|
|
@@ -108,29 +151,110 @@ export class TooltipContent {
|
|
|
108
151
|
/** Additional CSS classes */
|
|
109
152
|
readonly class = input<string>('');
|
|
110
153
|
|
|
154
|
+
private readonly _elementRef = inject(ElementRef);
|
|
155
|
+
private readonly _injector = inject(Injector);
|
|
156
|
+
|
|
111
157
|
protected readonly context = inject(TOOLTIP_CONTEXT);
|
|
112
158
|
|
|
159
|
+
protected readonly isPositioned = signal(false);
|
|
160
|
+
/** Computed position after collision detection */
|
|
161
|
+
protected readonly computedSide = signal<Side>('top');
|
|
162
|
+
protected readonly computedAlign = signal<Align>('center');
|
|
163
|
+
protected readonly positionStyles = signal<Record<string, string>>({
|
|
164
|
+
position: 'fixed',
|
|
165
|
+
top: '-9999px',
|
|
166
|
+
left: '-9999px',
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
private positionFrameId: number | null = null;
|
|
170
|
+
|
|
113
171
|
/** Current state: open or closed */
|
|
114
172
|
protected readonly state = computed<TooltipContentState>(() =>
|
|
115
173
|
this.context.open() ? 'open' : 'closed',
|
|
116
174
|
);
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
left: 'right-full top-1/2 -translate-y-1/2 mr-2',
|
|
122
|
-
right: 'left-full top-1/2 -translate-y-1/2 ml-2',
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
return cn(
|
|
126
|
-
'absolute z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md',
|
|
175
|
+
|
|
176
|
+
protected readonly computedClass = computed(() =>
|
|
177
|
+
cn(
|
|
178
|
+
'z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md',
|
|
127
179
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
128
180
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
129
181
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
130
182
|
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
|
|
131
183
|
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
132
|
-
|
|
184
|
+
!this.isPositioned() && 'opacity-0 pointer-events-none',
|
|
133
185
|
this.class(),
|
|
186
|
+
),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
onMouseEnter(): void {
|
|
190
|
+
if (this.context.disableHoverableContent?.()) {
|
|
191
|
+
this.context.setOpen(false);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
protected onEscape(): void {
|
|
196
|
+
if (this.context.open()) {
|
|
197
|
+
this.context.setOpen(false);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
private schedulePositionUpdate(): void {
|
|
202
|
+
this.cancelScheduledPositionUpdate();
|
|
203
|
+
|
|
204
|
+
this.positionFrameId = requestAnimationFrame(() => {
|
|
205
|
+
this.updatePosition();
|
|
206
|
+
|
|
207
|
+
this.positionFrameId = requestAnimationFrame(() => {
|
|
208
|
+
this.updatePosition();
|
|
209
|
+
this.positionFrameId = null;
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
private cancelScheduledPositionUpdate(): void {
|
|
215
|
+
if (this.positionFrameId !== null) {
|
|
216
|
+
cancelAnimationFrame(this.positionFrameId);
|
|
217
|
+
this.positionFrameId = null;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
private updatePosition(): void {
|
|
222
|
+
const triggerElement = this.context.triggerRef?.();
|
|
223
|
+
const contentElement = this._elementRef.nativeElement.querySelector(
|
|
224
|
+
'[role="tooltip"]',
|
|
225
|
+
) as HTMLElement;
|
|
226
|
+
|
|
227
|
+
if (!triggerElement || !contentElement) return;
|
|
228
|
+
|
|
229
|
+
const triggerRect = triggerElement.getBoundingClientRect();
|
|
230
|
+
const contentRect = contentElement.getBoundingClientRect();
|
|
231
|
+
const overlayWidth = Math.round(contentRect.width || 100);
|
|
232
|
+
const overlayHeight = Math.round(contentRect.height || 32);
|
|
233
|
+
|
|
234
|
+
const result = computePosition(
|
|
235
|
+
triggerRect,
|
|
236
|
+
{ width: overlayWidth, height: overlayHeight },
|
|
237
|
+
{
|
|
238
|
+
side: this.side() as Side,
|
|
239
|
+
align: this.align() as Align,
|
|
240
|
+
sideOffset: this.sideOffset(),
|
|
241
|
+
alignOffset: this.alignOffset(),
|
|
242
|
+
avoidCollisions: true,
|
|
243
|
+
collisionPadding: 8,
|
|
244
|
+
},
|
|
134
245
|
);
|
|
135
|
-
|
|
246
|
+
|
|
247
|
+
this.computedSide.set(result.side);
|
|
248
|
+
this.computedAlign.set(result.align);
|
|
249
|
+
|
|
250
|
+
// Set position styles with transform origin for animations
|
|
251
|
+
const transformOrigin = getTransformOrigin(result.side, result.align);
|
|
252
|
+
this.positionStyles.set({
|
|
253
|
+
position: 'fixed',
|
|
254
|
+
top: result.styles.top || '',
|
|
255
|
+
left: result.styles.left || '',
|
|
256
|
+
'--radix-tooltip-content-transform-origin': transformOrigin,
|
|
257
|
+
});
|
|
258
|
+
this.isPositioned.set(true);
|
|
259
|
+
}
|
|
136
260
|
}
|
|
@@ -9,13 +9,15 @@ export interface TooltipContextValue {
|
|
|
9
9
|
/** Set open state */
|
|
10
10
|
setOpen: (open: boolean) => void;
|
|
11
11
|
/** Delay before showing tooltip (ms) */
|
|
12
|
-
delayDuration: number;
|
|
12
|
+
delayDuration: () => number;
|
|
13
13
|
/** Skip delay duration when quickly hovering between tooltips (ms) */
|
|
14
14
|
skipDelayDuration: number;
|
|
15
15
|
/** Unique ID for aria-describedby relationship */
|
|
16
16
|
tooltipId: string;
|
|
17
17
|
/** Whether hoverable content is disabled */
|
|
18
18
|
disableHoverableContent: () => boolean;
|
|
19
|
+
/** Reference to the trigger element for fixed positioning */
|
|
20
|
+
triggerRef?: WritableSignal<HTMLElement | null>;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export const TOOLTIP_CONTEXT = new InjectionToken<TooltipContextValue>('TOOLTIP_CONTEXT');
|
|
@@ -73,7 +73,7 @@ export class TooltipProvider implements TooltipContextValue {
|
|
|
73
73
|
/** Unique ID for aria-describedby relationship */
|
|
74
74
|
readonly tooltipId = `tooltip-provider-${++tooltipIdCounter}`;
|
|
75
75
|
/** The duration from when the pointer enters the trigger until the tooltip opens */
|
|
76
|
-
readonly delayDuration = 700;
|
|
76
|
+
readonly delayDuration = input<number>(700);
|
|
77
77
|
/** How much time a user has to enter another trigger without incurring a delay again */
|
|
78
78
|
readonly skipDelayDuration = 300;
|
|
79
79
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ChangeDetectionStrategy, Component, inject, input, OnDestroy } from '@angular/core';
|
|
1
|
+
import { ChangeDetectionStrategy, Component, ElementRef, inject, input, OnDestroy } from '@angular/core';
|
|
2
2
|
import { TOOLTIP_CONTEXT } from './tooltip-context';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -70,6 +70,7 @@ export class TooltipTrigger implements OnDestroy {
|
|
|
70
70
|
readonly asChild = input<boolean>(false);
|
|
71
71
|
|
|
72
72
|
protected readonly context = inject(TOOLTIP_CONTEXT);
|
|
73
|
+
private readonly _elementRef = inject(ElementRef);
|
|
73
74
|
|
|
74
75
|
private showTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
75
76
|
private hideTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
@@ -80,9 +81,10 @@ export class TooltipTrigger implements OnDestroy {
|
|
|
80
81
|
|
|
81
82
|
onMouseEnter(): void {
|
|
82
83
|
this.clearTimeouts();
|
|
84
|
+
this.context.triggerRef?.set(this._elementRef.nativeElement);
|
|
83
85
|
this.showTimeout = setTimeout(() => {
|
|
84
86
|
this.context.setOpen(true);
|
|
85
|
-
}, this.context.delayDuration);
|
|
87
|
+
}, this.context.delayDuration());
|
|
86
88
|
}
|
|
87
89
|
onMouseLeave(): void {
|
|
88
90
|
this.clearTimeouts();
|
|
@@ -93,6 +95,7 @@ export class TooltipTrigger implements OnDestroy {
|
|
|
93
95
|
}
|
|
94
96
|
onFocus(): void {
|
|
95
97
|
// Show immediately on focus for keyboard users
|
|
98
|
+
this.context.triggerRef?.set(this._elementRef.nativeElement);
|
|
96
99
|
this.context.setOpen(true);
|
|
97
100
|
}
|
|
98
101
|
onBlur(): void {
|
|
@@ -126,9 +126,11 @@ export class Tooltip implements TooltipContextValue {
|
|
|
126
126
|
readonly disableHoverableContent = input<boolean>(false);
|
|
127
127
|
|
|
128
128
|
readonly open = signal(false);
|
|
129
|
+
/** Reference to the trigger element for fixed positioning */
|
|
130
|
+
readonly triggerRef = signal<HTMLElement | null>(null);
|
|
129
131
|
|
|
130
132
|
/** The duration from when the pointer enters the trigger until the tooltip opens */
|
|
131
|
-
readonly delayDuration = 700;
|
|
133
|
+
readonly delayDuration = input<number>(700);
|
|
132
134
|
/** How much time a user has to enter another trigger without incurring a delay again */
|
|
133
135
|
readonly skipDelayDuration = 300;
|
|
134
136
|
/** Unique ID for aria-describedby relationship */
|