@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,4 +1,4 @@
|
|
|
1
|
-
import { cn } from '@/lib/utils';
|
|
1
|
+
import { cn, Presence } from '@/lib/utils';
|
|
2
2
|
import { FocusTrapDirective } from '@/lib/utils/accessibility';
|
|
3
3
|
import {
|
|
4
4
|
ChangeDetectionStrategy,
|
|
@@ -10,22 +10,25 @@ import {
|
|
|
10
10
|
HostListener,
|
|
11
11
|
inject,
|
|
12
12
|
input,
|
|
13
|
-
signal,
|
|
14
13
|
} from '@angular/core';
|
|
15
14
|
import { DIALOG_CONTEXT } from './dialog-context';
|
|
16
15
|
|
|
17
|
-
/** Animation duration in ms — must match Tailwind's duration-200 */
|
|
18
|
-
const EXIT_ANIMATION_MS = 200;
|
|
19
|
-
|
|
20
16
|
/**
|
|
21
17
|
* DialogContent component - the content of the dialog.
|
|
22
18
|
* Matches shadcn/ui React DialogContent exactly.
|
|
19
|
+
*
|
|
20
|
+
* Features:
|
|
21
|
+
* - Escape key closes the dialog
|
|
22
|
+
* - Overlay click closes the dialog
|
|
23
|
+
* - Focus is trapped within the dialog
|
|
24
|
+
* - Exit animations handled by Presence component (no setTimeout needed)
|
|
25
|
+
* - Focus restored on any close path (overlay click, close button, Escape, programmatic)
|
|
23
26
|
*/
|
|
24
27
|
@Component({
|
|
25
28
|
selector: 'DialogContent',
|
|
26
|
-
imports: [FocusTrapDirective],
|
|
29
|
+
imports: [FocusTrapDirective, Presence],
|
|
27
30
|
template: `
|
|
28
|
-
|
|
31
|
+
<Presence [present]="context.isOpen()">
|
|
29
32
|
<!-- Overlay -->
|
|
30
33
|
<div
|
|
31
34
|
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 duration-200"
|
|
@@ -38,7 +41,7 @@ const EXIT_ANIMATION_MS = 200;
|
|
|
38
41
|
hlmFocusTrap
|
|
39
42
|
[trapFocus]="context.isOpen()"
|
|
40
43
|
[autoFocus]="true"
|
|
41
|
-
[restoreFocus]="
|
|
44
|
+
[restoreFocus]="false"
|
|
42
45
|
[initialFocus]="initialFocus()"
|
|
43
46
|
[class]="computedClass()"
|
|
44
47
|
[attr.data-state]="context.isOpen() ? 'open' : 'closed'"
|
|
@@ -76,7 +79,7 @@ const EXIT_ANIMATION_MS = 200;
|
|
|
76
79
|
</button>
|
|
77
80
|
}
|
|
78
81
|
</div>
|
|
79
|
-
|
|
82
|
+
</Presence>
|
|
80
83
|
`,
|
|
81
84
|
host: {
|
|
82
85
|
'attr.data-slot': '"dialog-content"',
|
|
@@ -86,22 +89,22 @@ const EXIT_ANIMATION_MS = 200;
|
|
|
86
89
|
})
|
|
87
90
|
export class DialogContent {
|
|
88
91
|
constructor() {
|
|
92
|
+
let wasOpen = false;
|
|
93
|
+
|
|
89
94
|
effect(() => {
|
|
90
95
|
const isOpen = this.context.isOpen();
|
|
91
96
|
this._cdr.markForCheck();
|
|
92
97
|
|
|
93
98
|
if (isOpen) {
|
|
94
|
-
|
|
99
|
+
wasOpen = true;
|
|
95
100
|
this.lockBodyScroll();
|
|
96
101
|
} else {
|
|
97
102
|
this.unlockBodyScroll();
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
this.shouldRender.set(false);
|
|
102
|
-
this._cdr.markForCheck();
|
|
103
|
-
}, EXIT_ANIMATION_MS);
|
|
103
|
+
// Restore focus on any close path (overlay click, close button, Escape, programmatic)
|
|
104
|
+
if (wasOpen) {
|
|
105
|
+
this.restoreFocus();
|
|
104
106
|
}
|
|
107
|
+
wasOpen = false;
|
|
105
108
|
}
|
|
106
109
|
});
|
|
107
110
|
|
|
@@ -121,8 +124,6 @@ export class DialogContent {
|
|
|
121
124
|
|
|
122
125
|
readonly context = inject(DIALOG_CONTEXT);
|
|
123
126
|
|
|
124
|
-
protected readonly shouldRender = signal(false);
|
|
125
|
-
|
|
126
127
|
protected readonly computedClass = computed(() =>
|
|
127
128
|
cn(
|
|
128
129
|
'fixed left-[50%] top-[50%] z-50 grid w-full max-w-lg translate-x-[-50%] translate-y-[-50%] gap-4 border bg-background text-foreground p-6 shadow-lg duration-200',
|
|
@@ -137,6 +138,7 @@ export class DialogContent {
|
|
|
137
138
|
);
|
|
138
139
|
|
|
139
140
|
private previousBodyOverflow = '';
|
|
141
|
+
private previousBodyPaddingRight = '';
|
|
140
142
|
|
|
141
143
|
@HostListener('document:keydown.escape')
|
|
142
144
|
onEscapeKey(): void {
|
|
@@ -154,17 +156,22 @@ export class DialogContent {
|
|
|
154
156
|
|
|
155
157
|
private lockBodyScroll(): void {
|
|
156
158
|
if (typeof document !== 'undefined') {
|
|
159
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
157
160
|
this.previousBodyOverflow = document.body.style.overflow;
|
|
161
|
+
this.previousBodyPaddingRight = document.body.style.paddingRight;
|
|
158
162
|
document.body.style.overflow = 'hidden';
|
|
163
|
+
if (scrollbarWidth > 0) {
|
|
164
|
+
document.body.style.paddingRight = scrollbarWidth + 'px';
|
|
165
|
+
}
|
|
159
166
|
}
|
|
160
167
|
}
|
|
161
168
|
private unlockBodyScroll(): void {
|
|
162
169
|
if (typeof document !== 'undefined') {
|
|
163
170
|
document.body.style.overflow = this.previousBodyOverflow;
|
|
171
|
+
document.body.style.paddingRight = this.previousBodyPaddingRight;
|
|
164
172
|
}
|
|
165
173
|
}
|
|
166
174
|
private close(): void {
|
|
167
|
-
this.restoreFocus();
|
|
168
175
|
this.context.setOpen(false);
|
|
169
176
|
}
|
|
170
177
|
private restoreFocus(): void {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { InjectionToken, signal } from '@angular/core';
|
|
2
|
+
|
|
3
|
+
export type Direction = 'ltr' | 'rtl';
|
|
4
|
+
|
|
5
|
+
export interface DirectionContext {
|
|
6
|
+
dir: ReturnType<typeof signal<Direction>>;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export const DIRECTION_CONTEXT = new InjectionToken<DirectionContext>('DirectionContext');
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, effect, forwardRef, input, signal } from '@angular/core';
|
|
2
|
+
import {
|
|
3
|
+
DIRECTION_CONTEXT,
|
|
4
|
+
type Direction as DirectionType,
|
|
5
|
+
type DirectionContext,
|
|
6
|
+
} from './direction-context';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Direction component - provides RTL/LTR direction context to descendants.
|
|
10
|
+
* Equivalent to Radix UI's DirectionProvider.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* <Direction dir="rtl">
|
|
14
|
+
* <!-- All components inside will be RTL -->
|
|
15
|
+
* <Select>...</Select>
|
|
16
|
+
* </Direction>
|
|
17
|
+
*/
|
|
18
|
+
@Component({
|
|
19
|
+
selector: 'Direction',
|
|
20
|
+
template: `<ng-content />`,
|
|
21
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
22
|
+
host: {
|
|
23
|
+
'attr.data-slot': '"direction"',
|
|
24
|
+
'[attr.dir]': 'dir()',
|
|
25
|
+
style: 'display: contents',
|
|
26
|
+
},
|
|
27
|
+
providers: [
|
|
28
|
+
{
|
|
29
|
+
provide: DIRECTION_CONTEXT,
|
|
30
|
+
useFactory: (component: Direction) => component.context,
|
|
31
|
+
deps: [forwardRef(() => Direction)],
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
})
|
|
35
|
+
export class Direction {
|
|
36
|
+
/** Text direction */
|
|
37
|
+
readonly dir = input<DirectionType>('ltr');
|
|
38
|
+
|
|
39
|
+
readonly context: DirectionContext = {
|
|
40
|
+
dir: signal<DirectionType>(this.dir()),
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
constructor() {
|
|
44
|
+
effect(() => {
|
|
45
|
+
this.context.dir.set(this.dir());
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
Injector,
|
|
12
12
|
input,
|
|
13
13
|
OnDestroy,
|
|
14
|
+
signal,
|
|
14
15
|
viewChild,
|
|
15
16
|
} from '@angular/core';
|
|
16
17
|
import { DRAWER_CONTEXT } from './drawer-context';
|
|
@@ -35,6 +36,7 @@ import { DRAWER_CONTEXT } from './drawer-context';
|
|
|
35
36
|
<div
|
|
36
37
|
#contentEl
|
|
37
38
|
[class]="computedClass()"
|
|
39
|
+
[style]="swipeStyle()"
|
|
38
40
|
[attr.data-state]="context.open() ? 'open' : 'closed'"
|
|
39
41
|
role="dialog"
|
|
40
42
|
aria-modal="true"
|
|
@@ -44,6 +46,9 @@ import { DRAWER_CONTEXT } from './drawer-context';
|
|
|
44
46
|
hlmFocusTrap
|
|
45
47
|
[trapFocus]="context.open()"
|
|
46
48
|
(keydown.escape)="onEscapeKey()"
|
|
49
|
+
(touchstart)="onTouchStart($event)"
|
|
50
|
+
(touchmove)="onTouchMove($event)"
|
|
51
|
+
(touchend)="onTouchEnd()"
|
|
47
52
|
>
|
|
48
53
|
<!-- Handle -->
|
|
49
54
|
<div class="mx-auto mt-4 h-2 w-[100px] rounded-full bg-muted"></div>
|
|
@@ -105,6 +110,17 @@ export class DrawerContent implements OnDestroy {
|
|
|
105
110
|
);
|
|
106
111
|
});
|
|
107
112
|
|
|
113
|
+
protected readonly swipeDelta = signal(0);
|
|
114
|
+
private touchStartCoord = 0;
|
|
115
|
+
|
|
116
|
+
protected readonly swipeStyle = computed(() => {
|
|
117
|
+
const delta = this.swipeDelta();
|
|
118
|
+
if (delta === 0) return '';
|
|
119
|
+
const dir = this.context.direction;
|
|
120
|
+
if (dir === 'bottom' || dir === 'top') return { transform: `translateY(${delta}px)` };
|
|
121
|
+
return { transform: `translateX(${delta}px)` };
|
|
122
|
+
});
|
|
123
|
+
|
|
108
124
|
/** Previous body overflow style for restoration */
|
|
109
125
|
private previousBodyOverflow = '';
|
|
110
126
|
|
|
@@ -119,6 +135,34 @@ export class DrawerContent implements OnDestroy {
|
|
|
119
135
|
onEscapeKey(): void {
|
|
120
136
|
this.context.setOpen(false);
|
|
121
137
|
}
|
|
138
|
+
onTouchStart(event: TouchEvent): void {
|
|
139
|
+
const touch = event.touches[0];
|
|
140
|
+
const dir = this.context.direction;
|
|
141
|
+
this.touchStartCoord = dir === 'bottom' || dir === 'top' ? touch.clientY : touch.clientX;
|
|
142
|
+
}
|
|
143
|
+
onTouchMove(event: TouchEvent): void {
|
|
144
|
+
const touch = event.touches[0];
|
|
145
|
+
const dir = this.context.direction;
|
|
146
|
+
const coord = dir === 'bottom' || dir === 'top' ? touch.clientY : touch.clientX;
|
|
147
|
+
const rawDelta = coord - this.touchStartCoord;
|
|
148
|
+
// Only allow dragging in the "away" direction
|
|
149
|
+
const isAway =
|
|
150
|
+
(dir === 'bottom' && rawDelta > 0) ||
|
|
151
|
+
(dir === 'top' && rawDelta < 0) ||
|
|
152
|
+
(dir === 'right' && rawDelta > 0) ||
|
|
153
|
+
(dir === 'left' && rawDelta < 0);
|
|
154
|
+
this.swipeDelta.set(isAway ? rawDelta : 0);
|
|
155
|
+
}
|
|
156
|
+
onTouchEnd(): void {
|
|
157
|
+
const threshold = 80;
|
|
158
|
+
if (Math.abs(this.swipeDelta()) >= threshold) {
|
|
159
|
+
this.swipeDelta.set(0);
|
|
160
|
+
this.context.setOpen(false);
|
|
161
|
+
} else {
|
|
162
|
+
this.swipeDelta.set(0);
|
|
163
|
+
}
|
|
164
|
+
this.touchStartCoord = 0;
|
|
165
|
+
}
|
|
122
166
|
|
|
123
167
|
private focusFirstElement(): void {
|
|
124
168
|
setTimeout(() => {
|
|
@@ -111,7 +111,7 @@ export class DropdownMenuContent implements OnDestroy {
|
|
|
111
111
|
/** Additional CSS classes */
|
|
112
112
|
readonly class = input<string>('');
|
|
113
113
|
/** Positioning strategy: 'absolute' stays within parent, 'fixed' escapes overflow containers */
|
|
114
|
-
readonly strategy = input<'absolute' | 'fixed'>('
|
|
114
|
+
readonly strategy = input<'absolute' | 'fixed'>('fixed');
|
|
115
115
|
|
|
116
116
|
private readonly _elementRef = inject(ElementRef);
|
|
117
117
|
|
|
@@ -198,7 +198,7 @@ export class DropdownMenuContent implements OnDestroy {
|
|
|
198
198
|
if (content) {
|
|
199
199
|
this.menuItems = Array.from(
|
|
200
200
|
content.querySelectorAll(
|
|
201
|
-
'[role="menuitem"]:not([aria-disabled="true"]):not([data-disabled])',
|
|
201
|
+
'[role="menuitem"]:not([aria-disabled="true"]):not([data-disabled=""])',
|
|
202
202
|
),
|
|
203
203
|
);
|
|
204
204
|
}
|
|
@@ -43,9 +43,11 @@ export class DropdownMenuSubContent {
|
|
|
43
43
|
);
|
|
44
44
|
|
|
45
45
|
protected onMouseEnter(): void {
|
|
46
|
+
this.subContext.isMouseInSubContent.set(true);
|
|
46
47
|
this.subContext.open.set(true);
|
|
47
48
|
}
|
|
48
49
|
protected onMouseLeave(): void {
|
|
50
|
+
this.subContext.isMouseInSubContent.set(false);
|
|
49
51
|
this.subContext.open.set(false);
|
|
50
52
|
}
|
|
51
53
|
}
|
|
@@ -19,7 +19,12 @@ import { DROPDOWN_MENU_SUB_CONTEXT } from './dropdown-menu-sub.component';
|
|
|
19
19
|
'[class]': 'computedClass()',
|
|
20
20
|
'(mouseenter)': 'onMouseEnter()',
|
|
21
21
|
'(mouseleave)': 'onMouseLeave()',
|
|
22
|
+
'(keydown)': 'onKeyDown($event)',
|
|
22
23
|
'[attr.data-state]': 'subContext.open() ? "open" : "closed"',
|
|
24
|
+
'role': 'menuitem',
|
|
25
|
+
'[attr.aria-haspopup]': '"menu"',
|
|
26
|
+
'[attr.aria-expanded]': 'subContext.open()',
|
|
27
|
+
'[attr.tabindex]': '"-1"',
|
|
23
28
|
},
|
|
24
29
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
25
30
|
})
|
|
@@ -46,9 +51,30 @@ export class DropdownMenuSubTrigger {
|
|
|
46
51
|
this.subContext.open.set(true);
|
|
47
52
|
}
|
|
48
53
|
protected onMouseLeave(): void {
|
|
49
|
-
// Delay closing to allow mouse to move to sub-content
|
|
50
54
|
setTimeout(() => {
|
|
51
|
-
|
|
55
|
+
if (!this.subContext.isMouseInSubContent()) {
|
|
56
|
+
this.subContext.open.set(false);
|
|
57
|
+
}
|
|
52
58
|
}, 100);
|
|
53
59
|
}
|
|
60
|
+
protected onKeyDown(event: KeyboardEvent): void {
|
|
61
|
+
if (event.key === 'ArrowRight' || event.key === 'Enter' || event.key === ' ') {
|
|
62
|
+
event.preventDefault();
|
|
63
|
+
event.stopPropagation();
|
|
64
|
+
this.subContext.open.set(true);
|
|
65
|
+
// Focus first focusable item in sub-content after it renders
|
|
66
|
+
setTimeout(() => {
|
|
67
|
+
const subContent = (event.target as HTMLElement)
|
|
68
|
+
.closest('DropdownMenuSub')
|
|
69
|
+
?.querySelector<HTMLElement>('[role="menu"] [role="menuitem"]:not([aria-disabled="true"]):not([data-disabled=""])');
|
|
70
|
+
if (subContent) {
|
|
71
|
+
subContent.focus();
|
|
72
|
+
}
|
|
73
|
+
}, 10);
|
|
74
|
+
}
|
|
75
|
+
if (event.key === 'ArrowLeft') {
|
|
76
|
+
event.preventDefault();
|
|
77
|
+
this.subContext.open.set(false);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
54
80
|
}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export interface DropdownMenuSubContext {
|
|
10
10
|
open: WritableSignal<boolean>;
|
|
11
|
+
/** True while the mouse is hovering over the sub-content panel */
|
|
12
|
+
isMouseInSubContent: WritableSignal<boolean>;
|
|
11
13
|
}
|
|
12
14
|
|
|
13
15
|
export const DROPDOWN_MENU_SUB_CONTEXT = new InjectionToken<DropdownMenuSubContext>(
|
|
@@ -26,6 +28,7 @@ export const DROPDOWN_MENU_SUB_CONTEXT = new InjectionToken<DropdownMenuSubConte
|
|
|
26
28
|
provide: DROPDOWN_MENU_SUB_CONTEXT,
|
|
27
29
|
useFactory: (): DropdownMenuSubContext => ({
|
|
28
30
|
open: signal(false),
|
|
31
|
+
isMouseInSubContent: signal(false),
|
|
29
32
|
}),
|
|
30
33
|
},
|
|
31
34
|
],
|
|
@@ -36,6 +36,31 @@ export class DropdownMenuTrigger {
|
|
|
36
36
|
this.context.open.set(true);
|
|
37
37
|
this.context.focusedIndex.set(0);
|
|
38
38
|
break;
|
|
39
|
+
case 'ArrowUp':
|
|
40
|
+
event.preventDefault();
|
|
41
|
+
this.context.triggerElement.set(this._elementRef.nativeElement);
|
|
42
|
+
this.context.open.set(true);
|
|
43
|
+
// Set focusedIndex to -1 so the content effect opens normally,
|
|
44
|
+
// then after it renders and focuses the first item we move to last.
|
|
45
|
+
this.context.focusedIndex.set(-1);
|
|
46
|
+
setTimeout(() => {
|
|
47
|
+
// After the content's own setTimeout(0) has run and focused item[0],
|
|
48
|
+
// query the menu items and focus the last one.
|
|
49
|
+
const menu = document.querySelector('[data-slot="dropdown-menu-content"] [role="menu"]');
|
|
50
|
+
if (menu) {
|
|
51
|
+
const items = Array.from(
|
|
52
|
+
menu.querySelectorAll<HTMLElement>(
|
|
53
|
+
'[role="menuitem"]:not([aria-disabled="true"]):not([data-disabled=""])',
|
|
54
|
+
),
|
|
55
|
+
);
|
|
56
|
+
if (items.length > 0) {
|
|
57
|
+
const lastIndex = items.length - 1;
|
|
58
|
+
items[lastIndex].focus();
|
|
59
|
+
this.context.focusedIndex.set(lastIndex);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}, 10);
|
|
63
|
+
break;
|
|
39
64
|
case 'Enter':
|
|
40
65
|
case ' ':
|
|
41
66
|
event.preventDefault();
|
|
@@ -8,6 +8,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
8
8
|
selector: 'EmptyAction',
|
|
9
9
|
template: `<ng-content />`,
|
|
10
10
|
host: {
|
|
11
|
+
'attr.data-slot': '"empty-action"',
|
|
11
12
|
'[class]': 'computedClass()',
|
|
12
13
|
},
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -8,6 +8,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
8
8
|
selector: 'EmptyDescription',
|
|
9
9
|
template: `<ng-content />`,
|
|
10
10
|
host: {
|
|
11
|
+
'attr.data-slot': '"empty-description"',
|
|
11
12
|
'[class]': 'computedClass()',
|
|
12
13
|
},
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -8,6 +8,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
8
8
|
selector: 'EmptyIcon',
|
|
9
9
|
template: `<ng-content />`,
|
|
10
10
|
host: {
|
|
11
|
+
'attr.data-slot': '"empty-icon"',
|
|
11
12
|
'[class]': 'computedClass()',
|
|
12
13
|
},
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -19,7 +20,7 @@ export class EmptyIcon {
|
|
|
19
20
|
/** Computed class combining base styles and custom classes */
|
|
20
21
|
protected readonly computedClass = computed(() =>
|
|
21
22
|
cn(
|
|
22
|
-
'mx-auto flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground [&>svg]:size-6',
|
|
23
|
+
'mx-auto flex size-12 items-center justify-center rounded-full bg-muted text-muted-foreground [&>svg]:size-6 [&>lucide-icon]:size-6',
|
|
23
24
|
this.class(),
|
|
24
25
|
),
|
|
25
26
|
);
|
|
@@ -8,6 +8,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
8
8
|
selector: 'EmptyTitle',
|
|
9
9
|
template: `<ng-content />`,
|
|
10
10
|
host: {
|
|
11
|
+
'attr.data-slot': '"empty-title"',
|
|
11
12
|
'[class]': 'computedClass()',
|
|
12
13
|
},
|
|
13
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -30,6 +30,7 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
30
30
|
selector: 'Empty',
|
|
31
31
|
template: `<ng-content />`,
|
|
32
32
|
host: {
|
|
33
|
+
'attr.data-slot': '"empty"',
|
|
33
34
|
'[class]': 'computedClass()',
|
|
34
35
|
},
|
|
35
36
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
@@ -14,7 +14,7 @@ import { FORM_FIELD_CONTEXT } from './form-context';
|
|
|
14
14
|
host: {
|
|
15
15
|
'[class]': 'computedClass()',
|
|
16
16
|
'[attr.id]': 'fieldContext?.formDescriptionId()',
|
|
17
|
-
'data-slot': 'form-description',
|
|
17
|
+
'attr.data-slot': '"form-description"',
|
|
18
18
|
},
|
|
19
19
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
20
20
|
})
|
|
@@ -26,6 +26,6 @@ export class FormDescription {
|
|
|
26
26
|
|
|
27
27
|
/** Computed class combining base styles and custom classes */
|
|
28
28
|
protected readonly computedClass = computed(() =>
|
|
29
|
-
cn('text-muted-foreground text-[
|
|
29
|
+
cn('text-muted-foreground text-[length:var(--font-size-description)]', this.class()),
|
|
30
30
|
);
|
|
31
31
|
}
|