@ng-cn/core 1.0.17 → 1.0.18
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 +21 -20
- package/src/app/lib/components/ui/avatar/ui-avatar.component.ts +4 -0
- package/src/app/lib/components/ui/calendar/calendar.component.ts +5 -1
- 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 +48 -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/date-picker/date-picker.component.ts +1 -0
- package/src/app/lib/components/ui/dialog/dialog-content.component.ts +26 -19
- 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 +2 -2
- 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/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/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/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/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 +22 -5
- 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 +18 -0
- package/src/app/lib/components/ui/tabs/tabs-trigger.component.ts +0 -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,12 +1,16 @@
|
|
|
1
|
-
import { cn, Presence } from '@/lib/utils';
|
|
1
|
+
import { Align, cn, computePosition, getTransformOrigin, Presence, Side } from '@/lib/utils';
|
|
2
2
|
import {
|
|
3
|
+
afterNextRender,
|
|
3
4
|
ChangeDetectionStrategy,
|
|
4
5
|
Component,
|
|
5
6
|
computed,
|
|
7
|
+
effect,
|
|
6
8
|
ElementRef,
|
|
7
9
|
inject,
|
|
10
|
+
Injector,
|
|
8
11
|
input,
|
|
9
12
|
OnDestroy,
|
|
13
|
+
signal,
|
|
10
14
|
} from '@angular/core';
|
|
11
15
|
import { HOVER_CARD_CONTEXT, HoverCardAlign, HoverCardSide } from './hover-card-context';
|
|
12
16
|
|
|
@@ -38,31 +42,6 @@ export interface HoverCardContentProps {
|
|
|
38
42
|
* HoverCardContent displays the preview content. It stays open when
|
|
39
43
|
* hovered, allowing users to interact with the content.
|
|
40
44
|
*
|
|
41
|
-
* ## Features
|
|
42
|
-
* - Stays open when content is hovered
|
|
43
|
-
* - Configurable side and alignment
|
|
44
|
-
* - Smooth animations
|
|
45
|
-
* - Escape key to dismiss
|
|
46
|
-
*
|
|
47
|
-
* ## Accessibility
|
|
48
|
-
* - `role="dialog"` on the content
|
|
49
|
-
* - Focusable content items
|
|
50
|
-
* - Escape returns focus to trigger
|
|
51
|
-
*
|
|
52
|
-
* @example Basic usage
|
|
53
|
-
* ```html
|
|
54
|
-
* <HoverCardContent>
|
|
55
|
-
* <p>Preview content</p>
|
|
56
|
-
* </HoverCardContent>
|
|
57
|
-
* ```
|
|
58
|
-
*
|
|
59
|
-
* @example With positioning
|
|
60
|
-
* ```html
|
|
61
|
-
* <HoverCardContent side="right" align="start">
|
|
62
|
-
* <p>Right-aligned content</p>
|
|
63
|
-
* </HoverCardContent>
|
|
64
|
-
* ```
|
|
65
|
-
*
|
|
66
45
|
* @data-attributes
|
|
67
46
|
* - `data-state` - 'open' | 'closed'
|
|
68
47
|
* - `data-side` - 'top' | 'right' | 'bottom' | 'left'
|
|
@@ -78,9 +57,10 @@ export interface HoverCardContentProps {
|
|
|
78
57
|
[attr.aria-modal]="false"
|
|
79
58
|
tabindex="-1"
|
|
80
59
|
[class]="computedClass()"
|
|
60
|
+
[style]="positionStyles()"
|
|
81
61
|
[attr.data-state]="state()"
|
|
82
|
-
[attr.data-side]="
|
|
83
|
-
[attr.data-align]="
|
|
62
|
+
[attr.data-side]="computedSide()"
|
|
63
|
+
[attr.data-align]="computedAlign()"
|
|
84
64
|
data-slot="hover-card-content"
|
|
85
65
|
(mouseenter)="onMouseEnter()"
|
|
86
66
|
(mouseleave)="onMouseLeave()"
|
|
@@ -99,6 +79,25 @@ export interface HoverCardContentProps {
|
|
|
99
79
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
100
80
|
})
|
|
101
81
|
export class HoverCardContent implements OnDestroy {
|
|
82
|
+
constructor() {
|
|
83
|
+
effect(() => {
|
|
84
|
+
const isOpen = this.context.open();
|
|
85
|
+
if (isOpen) {
|
|
86
|
+
this.isPositioned.set(false);
|
|
87
|
+
afterNextRender(
|
|
88
|
+
() => {
|
|
89
|
+
this.schedulePositionUpdate();
|
|
90
|
+
},
|
|
91
|
+
{ injector: this._injector },
|
|
92
|
+
);
|
|
93
|
+
} else {
|
|
94
|
+
this.cancelScheduledPositionUpdate();
|
|
95
|
+
this.isPositioned.set(false);
|
|
96
|
+
this.positionStyles.set({ position: 'fixed', top: '-9999px', left: '-9999px' });
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
102
101
|
/** The preferred side of the trigger to render against */
|
|
103
102
|
readonly side = input<HoverCardSide>('bottom');
|
|
104
103
|
/** The distance in pixels from the trigger */
|
|
@@ -109,80 +108,129 @@ export class HoverCardContent implements OnDestroy {
|
|
|
109
108
|
readonly class = input<string>('');
|
|
110
109
|
|
|
111
110
|
private readonly _elementRef = inject(ElementRef<HTMLElement>);
|
|
111
|
+
private readonly _injector = inject(Injector);
|
|
112
112
|
|
|
113
113
|
protected readonly context = inject(HOVER_CARD_CONTEXT);
|
|
114
114
|
|
|
115
|
-
protected readonly computedClass = computed(() =>
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
bottom: 'top-full mt-2',
|
|
119
|
-
left: 'right-full mr-2',
|
|
120
|
-
right: 'left-full ml-2',
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
const alignClasses = {
|
|
124
|
-
start: 'left-0',
|
|
125
|
-
center: 'left-1/2 -translate-x-1/2',
|
|
126
|
-
end: 'right-0',
|
|
127
|
-
};
|
|
128
|
-
|
|
129
|
-
return cn(
|
|
130
|
-
'absolute z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none',
|
|
115
|
+
protected readonly computedClass = computed(() =>
|
|
116
|
+
cn(
|
|
117
|
+
'fixed z-50 w-64 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none',
|
|
131
118
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
132
119
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
133
120
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
134
121
|
'data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2',
|
|
135
122
|
'data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2',
|
|
136
|
-
|
|
137
|
-
this.side() === 'top' || this.side() === 'bottom' ? alignClasses[this.align()] : '',
|
|
123
|
+
!this.isPositioned() && 'pointer-events-none opacity-0',
|
|
138
124
|
this.class(),
|
|
139
|
-
)
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
protected readonly positionStyles = signal<Record<string, string>>({
|
|
129
|
+
position: 'fixed',
|
|
130
|
+
top: '-9999px',
|
|
131
|
+
left: '-9999px',
|
|
140
132
|
});
|
|
133
|
+
protected readonly isPositioned = signal(false);
|
|
134
|
+
protected readonly computedSide = signal<Side>('bottom');
|
|
135
|
+
protected readonly computedAlign = signal<Align>('center');
|
|
141
136
|
|
|
142
|
-
private closeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
143
137
|
/** Current state: open or closed */
|
|
144
138
|
protected readonly state = computed<HoverCardContentState>(() =>
|
|
145
139
|
this.context.open() ? 'open' : 'closed',
|
|
146
140
|
);
|
|
147
141
|
|
|
142
|
+
private closeTimeout: ReturnType<typeof setTimeout> | null = null;
|
|
143
|
+
private positionFrameId: number | null = null;
|
|
144
|
+
|
|
148
145
|
ngOnDestroy(): void {
|
|
149
|
-
this.
|
|
146
|
+
this.clearCloseTimeout();
|
|
147
|
+
this.cancelScheduledPositionUpdate();
|
|
150
148
|
}
|
|
151
149
|
|
|
152
150
|
onMouseEnter(): void {
|
|
153
|
-
this.
|
|
151
|
+
this.clearCloseTimeout();
|
|
154
152
|
}
|
|
155
153
|
onMouseLeave(): void {
|
|
156
154
|
this.closeTimeout = setTimeout(() => {
|
|
157
155
|
this.context.setOpen(false);
|
|
158
|
-
}, this.context.closeDelay);
|
|
156
|
+
}, this.context.closeDelay());
|
|
159
157
|
}
|
|
160
158
|
onFocusIn(): void {
|
|
161
|
-
this.
|
|
159
|
+
this.clearCloseTimeout();
|
|
162
160
|
}
|
|
163
161
|
onFocusOut(event: FocusEvent): void {
|
|
164
162
|
const relatedTarget = event.relatedTarget as HTMLElement | null;
|
|
165
|
-
const trigger = this.
|
|
163
|
+
const trigger = this.context.triggerRef();
|
|
166
164
|
|
|
167
|
-
// Check if focus moved to trigger or stayed within content
|
|
168
165
|
if (relatedTarget && (trigger === relatedTarget || trigger?.contains(relatedTarget))) {
|
|
169
166
|
return;
|
|
170
167
|
}
|
|
171
168
|
|
|
172
169
|
this.closeTimeout = setTimeout(() => {
|
|
173
170
|
this.context.setOpen(false);
|
|
174
|
-
}, this.context.closeDelay);
|
|
171
|
+
}, this.context.closeDelay());
|
|
175
172
|
}
|
|
176
173
|
onEscape(): void {
|
|
177
174
|
this.context.setOpen(false);
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
175
|
+
this.context.triggerRef()?.focus();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
private schedulePositionUpdate(): void {
|
|
179
|
+
this.cancelScheduledPositionUpdate();
|
|
180
|
+
this.positionFrameId = requestAnimationFrame(() => {
|
|
181
|
+
this.updatePosition();
|
|
182
|
+
this.positionFrameId = requestAnimationFrame(() => {
|
|
183
|
+
this.updatePosition();
|
|
184
|
+
this.positionFrameId = null;
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
private cancelScheduledPositionUpdate(): void {
|
|
189
|
+
if (this.positionFrameId !== null) {
|
|
190
|
+
cancelAnimationFrame(this.positionFrameId);
|
|
191
|
+
this.positionFrameId = null;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
private updatePosition(): void {
|
|
195
|
+
const triggerElement = this.context.triggerRef();
|
|
196
|
+
const contentElement = this._elementRef.nativeElement.querySelector(
|
|
197
|
+
'[role="dialog"]',
|
|
181
198
|
) as HTMLElement;
|
|
182
|
-
|
|
199
|
+
|
|
200
|
+
if (!triggerElement || !contentElement) return;
|
|
201
|
+
|
|
202
|
+
const triggerRect = triggerElement.getBoundingClientRect();
|
|
203
|
+
const contentRect = contentElement.getBoundingClientRect();
|
|
204
|
+
const overlayWidth = Math.round(contentRect.width || 256);
|
|
205
|
+
const overlayHeight = Math.round(contentRect.height || 100);
|
|
206
|
+
|
|
207
|
+
const result = computePosition(
|
|
208
|
+
triggerRect,
|
|
209
|
+
{ width: overlayWidth, height: overlayHeight },
|
|
210
|
+
{
|
|
211
|
+
side: this.side(),
|
|
212
|
+
align: this.align(),
|
|
213
|
+
sideOffset: this.sideOffset(),
|
|
214
|
+
alignOffset: 0,
|
|
215
|
+
avoidCollisions: true,
|
|
216
|
+
collisionPadding: 8,
|
|
217
|
+
},
|
|
218
|
+
);
|
|
219
|
+
|
|
220
|
+
this.computedSide.set(result.side);
|
|
221
|
+
this.computedAlign.set(result.align);
|
|
222
|
+
|
|
223
|
+
const transformOrigin = getTransformOrigin(result.side, result.align);
|
|
224
|
+
this.positionStyles.set({
|
|
225
|
+
position: 'fixed',
|
|
226
|
+
top: result.styles.top || '',
|
|
227
|
+
left: result.styles.left || '',
|
|
228
|
+
'--radix-hover-card-content-transform-origin': transformOrigin,
|
|
229
|
+
});
|
|
230
|
+
this.isPositioned.set(true);
|
|
183
231
|
}
|
|
184
232
|
|
|
185
|
-
private
|
|
233
|
+
private clearCloseTimeout(): void {
|
|
186
234
|
if (this.closeTimeout) {
|
|
187
235
|
clearTimeout(this.closeTimeout);
|
|
188
236
|
this.closeTimeout = null;
|
|
@@ -9,9 +9,11 @@ export interface HoverCardContextValue {
|
|
|
9
9
|
/** Set open state */
|
|
10
10
|
setOpen: (open: boolean) => void;
|
|
11
11
|
/** The duration from when the pointer enters the trigger until the hover card opens (ms) */
|
|
12
|
-
openDelay: number;
|
|
12
|
+
openDelay: () => number;
|
|
13
13
|
/** The duration from when the pointer leaves the trigger/content until the hover card closes (ms) */
|
|
14
|
-
closeDelay: number;
|
|
14
|
+
closeDelay: () => number;
|
|
15
|
+
/** Reference to the trigger element for fixed positioning */
|
|
16
|
+
triggerRef: WritableSignal<HTMLElement | null>;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export const HOVER_CARD_CONTEXT = new InjectionToken<HoverCardContextValue>('HOVER_CARD_CONTEXT');
|
|
@@ -96,18 +96,20 @@ export class HoverCardTrigger implements OnDestroy {
|
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
onMouseEnter(): void {
|
|
99
|
+
this.context.triggerRef.set(this._elementRef.nativeElement);
|
|
99
100
|
this.clearTimeouts();
|
|
100
101
|
this.openTimeout = setTimeout(() => {
|
|
101
102
|
this.context.setOpen(true);
|
|
102
|
-
}, this.context.openDelay);
|
|
103
|
+
}, this.context.openDelay());
|
|
103
104
|
}
|
|
104
105
|
onMouseLeave(): void {
|
|
105
106
|
this.clearTimeouts();
|
|
106
107
|
this.closeTimeout = setTimeout(() => {
|
|
107
108
|
this.context.setOpen(false);
|
|
108
|
-
}, this.context.closeDelay);
|
|
109
|
+
}, this.context.closeDelay());
|
|
109
110
|
}
|
|
110
111
|
onFocus(): void {
|
|
112
|
+
this.context.triggerRef.set(this._elementRef.nativeElement);
|
|
111
113
|
this.clearTimeouts();
|
|
112
114
|
// Open immediately on focus for keyboard users
|
|
113
115
|
this.context.setOpen(true);
|
|
@@ -127,7 +129,7 @@ export class HoverCardTrigger implements OnDestroy {
|
|
|
127
129
|
this.clearTimeouts();
|
|
128
130
|
this.closeTimeout = setTimeout(() => {
|
|
129
131
|
this.context.setOpen(false);
|
|
130
|
-
}, this.context.closeDelay);
|
|
132
|
+
}, this.context.closeDelay());
|
|
131
133
|
}
|
|
132
134
|
onKeyDown(event: Event): void {
|
|
133
135
|
event.preventDefault();
|
|
@@ -5,6 +5,8 @@ import {
|
|
|
5
5
|
input,
|
|
6
6
|
output,
|
|
7
7
|
signal,
|
|
8
|
+
Signal,
|
|
9
|
+
WritableSignal,
|
|
8
10
|
} from '@angular/core';
|
|
9
11
|
import { HOVER_CARD_CONTEXT, type HoverCardContextValue } from './hover-card-context';
|
|
10
12
|
|
|
@@ -93,7 +95,7 @@ export interface HoverCardProps {
|
|
|
93
95
|
template: `<ng-content />`,
|
|
94
96
|
host: {
|
|
95
97
|
'attr.data-slot': '"hover-card"',
|
|
96
|
-
class: '
|
|
98
|
+
class: 'inline-block',
|
|
97
99
|
},
|
|
98
100
|
providers: [
|
|
99
101
|
{
|
|
@@ -120,10 +122,13 @@ export class HoverCard implements HoverCardContextValue {
|
|
|
120
122
|
|
|
121
123
|
readonly open = signal(false);
|
|
122
124
|
|
|
125
|
+
/** Reference to the trigger element for fixed positioning */
|
|
126
|
+
readonly triggerRef: WritableSignal<HTMLElement | null> = signal<HTMLElement | null>(null);
|
|
127
|
+
|
|
123
128
|
/** The duration from when the pointer enters the trigger until the hover card opens (ms) */
|
|
124
|
-
readonly openDelay = 700;
|
|
129
|
+
readonly openDelay = input<number>(700);
|
|
125
130
|
/** The duration from when the pointer leaves the trigger/content until the hover card closes (ms) */
|
|
126
|
-
readonly closeDelay = 300;
|
|
131
|
+
readonly closeDelay = input<number>(300);
|
|
127
132
|
|
|
128
133
|
setOpen(open: boolean): void {
|
|
129
134
|
if (this.controlledOpen() === undefined) {
|
|
@@ -14,6 +14,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
14
14
|
selector: 'InputGroupAddon',
|
|
15
15
|
template: `<ng-content />`,
|
|
16
16
|
host: {
|
|
17
|
+
'attr.data-slot': '"input-group-addon"',
|
|
17
18
|
'[class]': 'computedClass()',
|
|
18
19
|
},
|
|
19
20
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -10,6 +10,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
10
10
|
selector: 'InputGroupInput',
|
|
11
11
|
template: ``,
|
|
12
12
|
host: {
|
|
13
|
+
'attr.data-slot': '"input-group-input"',
|
|
13
14
|
'[class]': 'computedClass()',
|
|
14
15
|
},
|
|
15
16
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -30,6 +30,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
30
30
|
selector: 'InputGroup',
|
|
31
31
|
template: `<ng-content />`,
|
|
32
32
|
host: {
|
|
33
|
+
'attr.data-slot': '"input-group"',
|
|
33
34
|
'[class]': 'computedClass()',
|
|
34
35
|
},
|
|
35
36
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -114,7 +114,7 @@ export class MenubarContent implements OnDestroy {
|
|
|
114
114
|
if (content) {
|
|
115
115
|
this.menuItems = Array.from(
|
|
116
116
|
content.querySelectorAll(
|
|
117
|
-
'[role="menuitem"]:not([
|
|
117
|
+
'[role="menuitem"]:not([data-disabled=""]), [role="menuitemcheckbox"]:not([data-disabled=""]), [role="menuitemradio"]:not([data-disabled=""])',
|
|
118
118
|
),
|
|
119
119
|
);
|
|
120
120
|
}
|
|
@@ -18,7 +18,13 @@ import { NAVIGATION_MENU_CONTEXT, NAVIGATION_MENU_ITEM_CONTEXT } from './navigat
|
|
|
18
18
|
imports: [Presence],
|
|
19
19
|
template: `
|
|
20
20
|
<Presence [present]="itemContext.open()">
|
|
21
|
-
<div
|
|
21
|
+
<div
|
|
22
|
+
[class]="computedClass()"
|
|
23
|
+
[attr.id]="itemContext.contentId"
|
|
24
|
+
[attr.data-state]="itemContext.open() ? 'open' : 'closed'"
|
|
25
|
+
[attr.aria-labelledby]="itemContext.triggerId"
|
|
26
|
+
role="region"
|
|
27
|
+
>
|
|
22
28
|
<ng-content />
|
|
23
29
|
</div>
|
|
24
30
|
</Presence>
|
|
@@ -22,6 +22,16 @@ export interface NavigationMenuContextValue {
|
|
|
22
22
|
activeItem: WritableSignal<string | null>;
|
|
23
23
|
/** Layout orientation of the menu */
|
|
24
24
|
orientation: WritableSignal<NavigationMenuOrientation>;
|
|
25
|
+
/** Ordered list of registered trigger element IDs */
|
|
26
|
+
triggerIds: WritableSignal<string[]>;
|
|
27
|
+
/** Register a trigger ID (called by NavigationMenuTrigger on init) */
|
|
28
|
+
registerTrigger: (triggerId: string) => void;
|
|
29
|
+
/** Unregister a trigger ID (called by NavigationMenuTrigger on destroy) */
|
|
30
|
+
unregisterTrigger: (triggerId: string) => void;
|
|
31
|
+
/** Move DOM focus to the next trigger in document order */
|
|
32
|
+
focusNextTrigger: (currentTriggerId: string) => void;
|
|
33
|
+
/** Move DOM focus to the previous trigger in document order */
|
|
34
|
+
focusPreviousTrigger: (currentTriggerId: string) => void;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
37
|
export const NAVIGATION_MENU_CONTEXT = new InjectionToken<NavigationMenuContextValue>(
|
|
@@ -38,6 +48,10 @@ export const NAVIGATION_MENU_CONTEXT = new InjectionToken<NavigationMenuContextV
|
|
|
38
48
|
export interface NavigationMenuItemContextValue {
|
|
39
49
|
/** Unique identifier for this item */
|
|
40
50
|
itemId: string;
|
|
51
|
+
/** DOM id for the trigger element (used for aria-controls / aria-labelledby) */
|
|
52
|
+
triggerId: string;
|
|
53
|
+
/** DOM id for the content element (used for aria-controls) */
|
|
54
|
+
contentId: string;
|
|
41
55
|
/** Whether this item's content is open */
|
|
42
56
|
open: WritableSignal<boolean>;
|
|
43
57
|
}
|
|
@@ -17,10 +17,15 @@ let itemIdCounter = 0;
|
|
|
17
17
|
providers: [
|
|
18
18
|
{
|
|
19
19
|
provide: NAVIGATION_MENU_ITEM_CONTEXT,
|
|
20
|
-
useFactory: (): NavigationMenuItemContextValue =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
20
|
+
useFactory: (): NavigationMenuItemContextValue => {
|
|
21
|
+
const id = itemIdCounter++;
|
|
22
|
+
return {
|
|
23
|
+
itemId: `nav-item-${id}`,
|
|
24
|
+
triggerId: `nav-trigger-${id}`,
|
|
25
|
+
contentId: `nav-content-${id}`,
|
|
26
|
+
open: signal(false),
|
|
27
|
+
};
|
|
28
|
+
},
|
|
24
29
|
},
|
|
25
30
|
],
|
|
26
31
|
host: {
|
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
import { cn } from '@/lib/utils';
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
ChangeDetectionStrategy,
|
|
4
|
+
Component,
|
|
5
|
+
computed,
|
|
6
|
+
inject,
|
|
7
|
+
input,
|
|
8
|
+
OnDestroy,
|
|
9
|
+
OnInit,
|
|
10
|
+
} from '@angular/core';
|
|
3
11
|
import { ChevronDown, LucideAngularModule } from 'lucide-angular';
|
|
4
12
|
import { NAVIGATION_MENU_CONTEXT, NAVIGATION_MENU_ITEM_CONTEXT } from './navigation-menu-context';
|
|
5
13
|
import { navigationMenuTriggerStyle } from './navigation-menu-trigger-style';
|
|
@@ -22,13 +30,19 @@ import { navigationMenuTriggerStyle } from './navigation-menu-trigger-style';
|
|
|
22
30
|
host: {
|
|
23
31
|
'attr.data-slot': '"navigation-menu-trigger"',
|
|
24
32
|
'[class]': 'computedClass()',
|
|
33
|
+
'[attr.id]': 'itemContext.triggerId',
|
|
25
34
|
'[attr.data-state]': 'itemContext.open() ? "open" : "closed"',
|
|
35
|
+
'[attr.role]': '"button"',
|
|
36
|
+
'[attr.aria-expanded]': 'itemContext.open()',
|
|
37
|
+
'[attr.aria-haspopup]': '"menu"',
|
|
38
|
+
'[attr.aria-controls]': 'itemContext.contentId',
|
|
26
39
|
'(click)': 'toggle()',
|
|
27
40
|
'(mouseenter)': 'onMouseEnter()',
|
|
41
|
+
'(keydown)': 'onKeyDown($event)',
|
|
28
42
|
},
|
|
29
43
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
30
44
|
})
|
|
31
|
-
export class NavigationMenuTrigger {
|
|
45
|
+
export class NavigationMenuTrigger implements OnInit, OnDestroy {
|
|
32
46
|
/** Additional CSS classes */
|
|
33
47
|
readonly class = input<string>('');
|
|
34
48
|
|
|
@@ -41,6 +55,14 @@ export class NavigationMenuTrigger {
|
|
|
41
55
|
|
|
42
56
|
protected readonly ChevronDownIcon = ChevronDown;
|
|
43
57
|
|
|
58
|
+
ngOnInit(): void {
|
|
59
|
+
this.context.registerTrigger(this.itemContext.triggerId);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
ngOnDestroy(): void {
|
|
63
|
+
this.context.unregisterTrigger(this.itemContext.triggerId);
|
|
64
|
+
}
|
|
65
|
+
|
|
44
66
|
protected toggle(): void {
|
|
45
67
|
this.itemContext.open.update((v) => !v);
|
|
46
68
|
if (this.itemContext.open()) {
|
|
@@ -49,6 +71,7 @@ export class NavigationMenuTrigger {
|
|
|
49
71
|
this.context.activeItem.set(null);
|
|
50
72
|
}
|
|
51
73
|
}
|
|
74
|
+
|
|
52
75
|
protected onMouseEnter(): void {
|
|
53
76
|
const activeItem = this.context.activeItem();
|
|
54
77
|
if (activeItem && activeItem !== this.itemContext.itemId) {
|
|
@@ -56,4 +79,48 @@ export class NavigationMenuTrigger {
|
|
|
56
79
|
this.itemContext.open.set(true);
|
|
57
80
|
}
|
|
58
81
|
}
|
|
82
|
+
|
|
83
|
+
protected onKeyDown(event: KeyboardEvent): void {
|
|
84
|
+
switch (event.key) {
|
|
85
|
+
case 'Enter':
|
|
86
|
+
case ' ':
|
|
87
|
+
event.preventDefault();
|
|
88
|
+
this.toggle();
|
|
89
|
+
if (this.itemContext.open()) {
|
|
90
|
+
this.focusFirstContentItem();
|
|
91
|
+
}
|
|
92
|
+
break;
|
|
93
|
+
case 'ArrowDown':
|
|
94
|
+
event.preventDefault();
|
|
95
|
+
if (!this.itemContext.open()) {
|
|
96
|
+
this.itemContext.open.set(true);
|
|
97
|
+
this.context.activeItem.set(this.itemContext.itemId);
|
|
98
|
+
}
|
|
99
|
+
this.focusFirstContentItem();
|
|
100
|
+
break;
|
|
101
|
+
case 'ArrowRight':
|
|
102
|
+
event.preventDefault();
|
|
103
|
+
this.context.focusNextTrigger(this.itemContext.triggerId);
|
|
104
|
+
break;
|
|
105
|
+
case 'ArrowLeft':
|
|
106
|
+
event.preventDefault();
|
|
107
|
+
this.context.focusPreviousTrigger(this.itemContext.triggerId);
|
|
108
|
+
break;
|
|
109
|
+
case 'Escape':
|
|
110
|
+
if (this.itemContext.open()) {
|
|
111
|
+
this.itemContext.open.set(false);
|
|
112
|
+
this.context.activeItem.set(null);
|
|
113
|
+
document.getElementById(this.itemContext.triggerId)?.focus();
|
|
114
|
+
}
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private focusFirstContentItem(): void {
|
|
120
|
+
setTimeout(() => {
|
|
121
|
+
const content = document.getElementById(this.itemContext.contentId);
|
|
122
|
+
const focusable = content?.querySelector<HTMLElement>('a, button, [tabindex]');
|
|
123
|
+
focusable?.focus();
|
|
124
|
+
}, 10);
|
|
125
|
+
}
|
|
59
126
|
}
|
|
@@ -7,6 +7,37 @@ import {
|
|
|
7
7
|
} from './navigation-menu-context';
|
|
8
8
|
import { NavigationMenuViewport } from './navigation-menu-viewport.component';
|
|
9
9
|
|
|
10
|
+
function createNavigationMenuContext(): NavigationMenuContextValue {
|
|
11
|
+
const triggerIds = signal<string[]>([]);
|
|
12
|
+
return {
|
|
13
|
+
activeItem: signal(null),
|
|
14
|
+
orientation: signal('horizontal'),
|
|
15
|
+
triggerIds,
|
|
16
|
+
registerTrigger: (triggerId: string) => {
|
|
17
|
+
triggerIds.update((ids) => [...ids, triggerId]);
|
|
18
|
+
},
|
|
19
|
+
unregisterTrigger: (triggerId: string) => {
|
|
20
|
+
triggerIds.update((ids) => ids.filter((id) => id !== triggerId));
|
|
21
|
+
},
|
|
22
|
+
focusNextTrigger: (currentTriggerId: string) => {
|
|
23
|
+
const ids = triggerIds();
|
|
24
|
+
const idx = ids.indexOf(currentTriggerId);
|
|
25
|
+
const nextId = idx < ids.length - 1 ? ids[idx + 1] : ids[0];
|
|
26
|
+
if (nextId) {
|
|
27
|
+
document.getElementById(nextId)?.focus();
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
focusPreviousTrigger: (currentTriggerId: string) => {
|
|
31
|
+
const ids = triggerIds();
|
|
32
|
+
const idx = ids.indexOf(currentTriggerId);
|
|
33
|
+
const prevId = idx > 0 ? ids[idx - 1] : ids[ids.length - 1];
|
|
34
|
+
if (prevId) {
|
|
35
|
+
document.getElementById(prevId)?.focus();
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
10
41
|
/**
|
|
11
42
|
* Props for the NavigationMenu component
|
|
12
43
|
*/
|
|
@@ -121,10 +152,7 @@ export interface NavigationMenuProps {
|
|
|
121
152
|
providers: [
|
|
122
153
|
{
|
|
123
154
|
provide: NAVIGATION_MENU_CONTEXT,
|
|
124
|
-
useFactory:
|
|
125
|
-
activeItem: signal(null),
|
|
126
|
-
orientation: signal('horizontal'),
|
|
127
|
-
}),
|
|
155
|
+
useFactory: createNavigationMenuContext,
|
|
128
156
|
},
|
|
129
157
|
],
|
|
130
158
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -29,12 +29,14 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
29
29
|
host: {
|
|
30
30
|
'attr.data-slot': '"pagination"',
|
|
31
31
|
role: 'navigation',
|
|
32
|
-
'[attr.aria-label]': '
|
|
32
|
+
'[attr.aria-label]': 'ariaLabel()',
|
|
33
33
|
'[class]': 'computedClass()',
|
|
34
34
|
},
|
|
35
35
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
36
36
|
})
|
|
37
37
|
export class Pagination {
|
|
38
|
+
/** Accessible label for the navigation landmark */
|
|
39
|
+
readonly ariaLabel = input<string>('Pagination');
|
|
38
40
|
/** Additional CSS classes */
|
|
39
41
|
readonly class = input<string>('');
|
|
40
42
|
|
|
@@ -100,6 +100,7 @@ export interface PopoverContentProps {
|
|
|
100
100
|
[attr.data-align]="computedAlign()"
|
|
101
101
|
[style]="mergedStyles()"
|
|
102
102
|
role="dialog"
|
|
103
|
+
tabindex="-1"
|
|
103
104
|
[attr.aria-modal]="context.modal() || null"
|
|
104
105
|
>
|
|
105
106
|
<ng-content />
|
|
@@ -277,5 +278,15 @@ export class PopoverContent {
|
|
|
277
278
|
'--radix-popover-content-transform-origin': transformOrigin,
|
|
278
279
|
});
|
|
279
280
|
this.isPositioned.set(true);
|
|
281
|
+
|
|
282
|
+
// Move focus into the popover for keyboard accessibility
|
|
283
|
+
setTimeout(() => {
|
|
284
|
+
const dialog = this._elementRef.nativeElement.querySelector('[role="dialog"]') as HTMLElement;
|
|
285
|
+
if (!dialog) return;
|
|
286
|
+
const firstFocusable = dialog.querySelector<HTMLElement>(
|
|
287
|
+
'button:not([disabled]):not([data-disabled=""]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])',
|
|
288
|
+
);
|
|
289
|
+
(firstFocusable ?? dialog).focus({ preventScroll: true });
|
|
290
|
+
}, 0);
|
|
280
291
|
}
|
|
281
292
|
}
|
|
@@ -16,6 +16,8 @@ export interface PopoverContextValue {
|
|
|
16
16
|
triggerRef?: Signal<HTMLElement | null>;
|
|
17
17
|
/** Set the trigger element reference */
|
|
18
18
|
setTriggerRef?: (element: HTMLElement | null) => void;
|
|
19
|
+
/** Unique ID for aria-controls relationship */
|
|
20
|
+
contentId: string;
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
export const POPOVER_CONTEXT = new InjectionToken<PopoverContextValue>('POPOVER_CONTEXT');
|
|
@@ -10,6 +10,8 @@ import { POPOVER_CONTEXT, type PopoverContextValue } from './popover-context';
|
|
|
10
10
|
|
|
11
11
|
export type PopoverState = 'open' | 'closed';
|
|
12
12
|
|
|
13
|
+
let idCounter = 0;
|
|
14
|
+
|
|
13
15
|
/**
|
|
14
16
|
* Props for the Popover component
|
|
15
17
|
*/
|
|
@@ -128,6 +130,8 @@ export class Popover implements PopoverContextValue {
|
|
|
128
130
|
readonly controlledOpen = input<boolean | undefined>(undefined, { alias: 'open' });
|
|
129
131
|
|
|
130
132
|
readonly open = signal(false);
|
|
133
|
+
/** Unique ID for aria-controls relationship */
|
|
134
|
+
readonly contentId = `popover-content-${++idCounter}`;
|
|
131
135
|
/** Reference to the trigger element for positioning */
|
|
132
136
|
readonly triggerRef = signal<HTMLElement | null>(null);
|
|
133
137
|
|
|
@@ -102,9 +102,8 @@ export type ProgressProps = {
|
|
|
102
102
|
'[attr.aria-label]': 'ariaLabel()',
|
|
103
103
|
'[attr.aria-valuemin]': '0',
|
|
104
104
|
'[attr.aria-valuemax]': 'max()',
|
|
105
|
-
'[attr.aria-valuenow]': 'value()',
|
|
105
|
+
'[attr.aria-valuenow]': 'value() !== null ? value() : null',
|
|
106
106
|
'[attr.aria-valuetext]': 'computedValueText()',
|
|
107
|
-
'[attr.aria-live]': '"polite"',
|
|
108
107
|
'[attr.data-state]': 'state()',
|
|
109
108
|
'[attr.data-value]': 'value()',
|
|
110
109
|
'[attr.data-max]': 'max()',
|