@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ng-cn/core",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.18",
|
|
4
4
|
"description": "Beautifully designed Angular components built with Tailwind CSS v4 - The official Angular port of shadcn/ui",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"angular",
|
|
@@ -58,6 +58,8 @@
|
|
|
58
58
|
]
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
|
+
"@angular-devkit/core": "^21.2.5",
|
|
62
|
+
"@angular-devkit/schematics": "^21.2.5",
|
|
61
63
|
"@angular/cdk": "^21.2.4",
|
|
62
64
|
"@angular/common": "^21.2.6",
|
|
63
65
|
"@angular/compiler": "^21.2.6",
|
|
@@ -74,18 +76,17 @@
|
|
|
74
76
|
"express": "^5.1.0",
|
|
75
77
|
"lucide-angular": "^1.0.0",
|
|
76
78
|
"ng-apexcharts": "^2.3.0",
|
|
79
|
+
"ngx-sonner": "^3.1.0",
|
|
77
80
|
"postcss": "^8.5.8",
|
|
78
81
|
"rxjs": "~7.8.0",
|
|
79
82
|
"shiki": "^4.0.2",
|
|
80
83
|
"tailwind-merge": "^3.5.0",
|
|
81
84
|
"tailwindcss": "^4.2.2",
|
|
82
|
-
"tslib": "^2.3.0"
|
|
83
|
-
"@angular-devkit/core": "^21.2.5",
|
|
84
|
-
"@angular-devkit/schematics": "^21.2.5"
|
|
85
|
+
"tslib": "^2.3.0"
|
|
85
86
|
},
|
|
86
87
|
"devDependencies": {
|
|
87
|
-
"@analogjs/vitest-angular": "^2.3.1",
|
|
88
88
|
"@analogjs/vite-plugin-angular": "^2.2.0",
|
|
89
|
+
"@analogjs/vitest-angular": "^2.3.1",
|
|
89
90
|
"@angular/build": "^21.2.5",
|
|
90
91
|
"@angular/cli": "^21.2.5",
|
|
91
92
|
"@angular/compiler-cli": "^21.2.6",
|
|
@@ -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,13 +10,9 @@ import {
|
|
|
10
10
|
HostListener,
|
|
11
11
|
inject,
|
|
12
12
|
input,
|
|
13
|
-
signal,
|
|
14
13
|
} from '@angular/core';
|
|
15
14
|
import { ALERT_DIALOG_CONTEXT } from './alert-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
|
* AlertDialogContent component - the modal content of the alert dialog.
|
|
22
18
|
* Matches shadcn/ui React AlertDialogContent exactly.
|
|
@@ -26,12 +22,14 @@ const EXIT_ANIMATION_MS = 200;
|
|
|
26
22
|
* - Overlay/backdrop click does NOT close the dialog
|
|
27
23
|
* - Focus is trapped within the dialog
|
|
28
24
|
* - User must explicitly click Cancel or Action to close
|
|
25
|
+
* - Exit animations handled by Presence component (no setTimeout needed)
|
|
26
|
+
* - Focus restored on any programmatic close (Action/Cancel/Escape)
|
|
29
27
|
*/
|
|
30
28
|
@Component({
|
|
31
29
|
selector: 'AlertDialogContent',
|
|
32
|
-
imports: [FocusTrapDirective],
|
|
30
|
+
imports: [FocusTrapDirective, Presence],
|
|
33
31
|
template: `
|
|
34
|
-
|
|
32
|
+
<Presence [present]="context.isOpen()">
|
|
35
33
|
<!-- Overlay - does NOT close on click -->
|
|
36
34
|
<div
|
|
37
35
|
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"
|
|
@@ -55,7 +53,7 @@ const EXIT_ANIMATION_MS = 200;
|
|
|
55
53
|
>
|
|
56
54
|
<ng-content />
|
|
57
55
|
</div>
|
|
58
|
-
|
|
56
|
+
</Presence>
|
|
59
57
|
`,
|
|
60
58
|
host: {
|
|
61
59
|
'attr.data-slot': '"alert-dialog-content"',
|
|
@@ -65,21 +63,22 @@ const EXIT_ANIMATION_MS = 200;
|
|
|
65
63
|
})
|
|
66
64
|
export class AlertDialogContent {
|
|
67
65
|
constructor() {
|
|
66
|
+
let wasOpen = false;
|
|
67
|
+
|
|
68
68
|
effect(() => {
|
|
69
69
|
const isOpen = this.context.isOpen();
|
|
70
70
|
this._cdr.markForCheck();
|
|
71
71
|
|
|
72
72
|
if (isOpen) {
|
|
73
|
-
|
|
73
|
+
wasOpen = true;
|
|
74
74
|
this.lockBodyScroll();
|
|
75
75
|
} else {
|
|
76
76
|
this.unlockBodyScroll();
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this._cdr.markForCheck();
|
|
81
|
-
}, EXIT_ANIMATION_MS);
|
|
77
|
+
// Restore focus whenever dialog closes (covers Action/Cancel/Escape paths)
|
|
78
|
+
if (wasOpen) {
|
|
79
|
+
this.restoreFocus();
|
|
82
80
|
}
|
|
81
|
+
wasOpen = false;
|
|
83
82
|
}
|
|
84
83
|
});
|
|
85
84
|
|
|
@@ -95,7 +94,6 @@ export class AlertDialogContent {
|
|
|
95
94
|
private readonly _cdr = inject(ChangeDetectorRef);
|
|
96
95
|
|
|
97
96
|
protected readonly context = inject(ALERT_DIALOG_CONTEXT);
|
|
98
|
-
protected readonly shouldRender = signal(false);
|
|
99
97
|
|
|
100
98
|
protected readonly computedClass = computed(() =>
|
|
101
99
|
cn(
|
|
@@ -111,29 +109,32 @@ export class AlertDialogContent {
|
|
|
111
109
|
);
|
|
112
110
|
|
|
113
111
|
private previousBodyOverflow = '';
|
|
112
|
+
private previousBodyPaddingRight = '';
|
|
114
113
|
|
|
115
114
|
@HostListener('document:keydown.escape')
|
|
116
115
|
onEscapeKey(): void {
|
|
117
116
|
if (this.context.isOpen()) {
|
|
118
|
-
this.
|
|
117
|
+
this.context.setOpen(false);
|
|
119
118
|
}
|
|
120
119
|
}
|
|
121
120
|
|
|
122
121
|
private lockBodyScroll(): void {
|
|
123
122
|
if (typeof document !== 'undefined') {
|
|
123
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
124
124
|
this.previousBodyOverflow = document.body.style.overflow;
|
|
125
|
+
this.previousBodyPaddingRight = document.body.style.paddingRight;
|
|
125
126
|
document.body.style.overflow = 'hidden';
|
|
127
|
+
if (scrollbarWidth > 0) {
|
|
128
|
+
document.body.style.paddingRight = scrollbarWidth + 'px';
|
|
129
|
+
}
|
|
126
130
|
}
|
|
127
131
|
}
|
|
128
132
|
private unlockBodyScroll(): void {
|
|
129
133
|
if (typeof document !== 'undefined') {
|
|
130
134
|
document.body.style.overflow = this.previousBodyOverflow;
|
|
135
|
+
document.body.style.paddingRight = this.previousBodyPaddingRight;
|
|
131
136
|
}
|
|
132
137
|
}
|
|
133
|
-
private close(): void {
|
|
134
|
-
this.restoreFocus();
|
|
135
|
-
this.context.setOpen(false);
|
|
136
|
-
}
|
|
137
138
|
private restoreFocus(): void {
|
|
138
139
|
const triggerEl = this.context.getTriggerElement();
|
|
139
140
|
if (triggerEl) {
|
|
@@ -14,6 +14,10 @@ import { Avatar } from './avatar.component';
|
|
|
14
14
|
selector: 'ui-avatar',
|
|
15
15
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
16
16
|
imports: [Avatar, AvatarImage, AvatarFallback],
|
|
17
|
+
host: {
|
|
18
|
+
'attr.data-slot': '"ui-avatar"',
|
|
19
|
+
style: 'display: contents',
|
|
20
|
+
},
|
|
17
21
|
template: `
|
|
18
22
|
<Avatar [class]="class()">
|
|
19
23
|
@if (src()) {
|
|
@@ -26,8 +26,12 @@ import { buttonVariants } from '../button';
|
|
|
26
26
|
@Component({
|
|
27
27
|
selector: 'Calendar',
|
|
28
28
|
imports: [LucideAngularModule],
|
|
29
|
+
host: {
|
|
30
|
+
'attr.data-slot': '"calendar"',
|
|
31
|
+
'[class]': 'computedClass()',
|
|
32
|
+
},
|
|
29
33
|
template: `
|
|
30
|
-
<div
|
|
34
|
+
<div role="application" [attr.aria-label]="ariaLabel()">
|
|
31
35
|
<!-- Header with navigation -->
|
|
32
36
|
<div class="w-full">
|
|
33
37
|
<div class="w-full space-y-4">
|
|
@@ -24,6 +24,7 @@ import { CHART_CONTEXT, type ChartConfig, type ChartContext } from './chart-cont
|
|
|
24
24
|
selector: 'ChartContainer',
|
|
25
25
|
template: `<ng-content />`,
|
|
26
26
|
host: {
|
|
27
|
+
'attr.data-slot': '"chart-container"',
|
|
27
28
|
'[class]': 'computedClass()',
|
|
28
29
|
'[style]': 'chartStyles()',
|
|
29
30
|
'data-chart': '',
|
|
@@ -6,11 +6,11 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
6
6
|
*/
|
|
7
7
|
@Component({
|
|
8
8
|
selector: 'ChartLegend',
|
|
9
|
-
template:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
template: `<ng-content />`,
|
|
10
|
+
host: {
|
|
11
|
+
'attr.data-slot': '"chart-legend"',
|
|
12
|
+
'[class]': 'computedClass()',
|
|
13
|
+
},
|
|
14
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
15
|
})
|
|
16
16
|
export class ChartLegend {
|
|
@@ -6,11 +6,11 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
6
6
|
*/
|
|
7
7
|
@Component({
|
|
8
8
|
selector: 'ChartTooltipContent',
|
|
9
|
-
template:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
template: `<ng-content />`,
|
|
10
|
+
host: {
|
|
11
|
+
'attr.data-slot': '"chart-tooltip-content"',
|
|
12
|
+
'[class]': 'computedClass()',
|
|
13
|
+
},
|
|
14
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
15
|
})
|
|
16
16
|
export class ChartTooltipContent {
|
|
@@ -6,11 +6,11 @@ import { ChangeDetectionStrategy, Component, computed, input } from '@angular/co
|
|
|
6
6
|
*/
|
|
7
7
|
@Component({
|
|
8
8
|
selector: 'ChartTooltip',
|
|
9
|
-
template:
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
template: `<ng-content />`,
|
|
10
|
+
host: {
|
|
11
|
+
'attr.data-slot': '"chart-tooltip"',
|
|
12
|
+
'[class]': 'computedClass()',
|
|
13
|
+
},
|
|
14
14
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
15
15
|
})
|
|
16
16
|
export class ChartTooltip {
|
|
@@ -22,7 +22,8 @@ import { COLLAPSIBLE_CONTEXT } from './collapsible-context';
|
|
|
22
22
|
'[class]': 'computedClass()',
|
|
23
23
|
'[attr.data-state]': 'collapsible.isOpen() ? "open" : "closed"',
|
|
24
24
|
'[attr.data-disabled]': 'collapsible.disabled() ? "" : null',
|
|
25
|
-
'[attr.
|
|
25
|
+
'[attr.id]': 'collapsible.contentId',
|
|
26
|
+
'[attr.inert]': '!collapsible.isOpen() || null',
|
|
26
27
|
},
|
|
27
28
|
styles: [
|
|
28
29
|
`
|
|
@@ -17,6 +17,7 @@ import { COLLAPSIBLE_CONTEXT } from './collapsible-context';
|
|
|
17
17
|
'[attr.data-state]': 'collapsible.isOpen() ? "open" : "closed"',
|
|
18
18
|
'[attr.data-disabled]': 'collapsible.disabled() ? "" : null',
|
|
19
19
|
'[attr.aria-expanded]': 'collapsible.isOpen()',
|
|
20
|
+
'[attr.aria-controls]': 'collapsible.contentId',
|
|
20
21
|
'[attr.disabled]': 'collapsible.disabled() ? true : null',
|
|
21
22
|
'(click)': 'onClick()',
|
|
22
23
|
'(keydown.enter)': 'onClick()',
|
|
@@ -77,6 +77,9 @@ export class Collapsible implements CollapsibleContext {
|
|
|
77
77
|
|
|
78
78
|
protected readonly computedClass = computed(() => cn('', this.class()));
|
|
79
79
|
|
|
80
|
+
/** Stable ID linking the trigger (aria-controls) to the content (id) */
|
|
81
|
+
readonly contentId = `collapsible-content-${Math.random().toString(36).slice(2)}`;
|
|
82
|
+
|
|
80
83
|
/** Internal state for open/closed */
|
|
81
84
|
private readonly _isOpen = signal<boolean>(false);
|
|
82
85
|
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
effect,
|
|
8
8
|
ElementRef,
|
|
9
9
|
inject,
|
|
10
|
+
Injector,
|
|
10
11
|
input,
|
|
11
12
|
OnDestroy,
|
|
13
|
+
signal,
|
|
12
14
|
} from '@angular/core';
|
|
13
15
|
import { CONTEXT_MENU_CONTEXT } from './context-menu-context';
|
|
14
16
|
|
|
@@ -26,8 +28,8 @@ import { CONTEXT_MENU_CONTEXT } from './context-menu-context';
|
|
|
26
28
|
[class]="computedClass()"
|
|
27
29
|
[attr.data-state]="context.open() ? 'open' : 'closed'"
|
|
28
30
|
[style.position]="'fixed'"
|
|
29
|
-
[style.left.px]="
|
|
30
|
-
[style.top.px]="
|
|
31
|
+
[style.left.px]="displayPosition().x"
|
|
32
|
+
[style.top.px]="displayPosition().y"
|
|
31
33
|
role="menu"
|
|
32
34
|
aria-orientation="vertical"
|
|
33
35
|
tabindex="-1"
|
|
@@ -40,7 +42,7 @@ import { CONTEXT_MENU_CONTEXT } from './context-menu-context';
|
|
|
40
42
|
host: {
|
|
41
43
|
'attr.data-slot': '"context-menu-content"',
|
|
42
44
|
class: 'contents',
|
|
43
|
-
'(document:click)': 'onDocumentClick()',
|
|
45
|
+
'(document:click)': 'onDocumentClick($event)',
|
|
44
46
|
'(document:keydown.escape)': 'onEscapeKey()',
|
|
45
47
|
'(document:contextmenu)': 'onAnotherContextMenu()',
|
|
46
48
|
},
|
|
@@ -48,19 +50,28 @@ import { CONTEXT_MENU_CONTEXT } from './context-menu-context';
|
|
|
48
50
|
})
|
|
49
51
|
export class ContextMenuContent implements OnDestroy {
|
|
50
52
|
constructor() {
|
|
51
|
-
//
|
|
53
|
+
// Clamp position and focus first item when menu opens
|
|
52
54
|
effect(() => {
|
|
53
55
|
if (this.context.open()) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
this.
|
|
59
|
-
|
|
60
|
-
this.
|
|
61
|
-
this.
|
|
62
|
-
|
|
63
|
-
|
|
56
|
+
this.isPositioned.set(false);
|
|
57
|
+
afterNextRender(
|
|
58
|
+
() => {
|
|
59
|
+
this.clampPosition();
|
|
60
|
+
this.isPositioned.set(true);
|
|
61
|
+
this.updateMenuItems();
|
|
62
|
+
const focusedIdx = this.context.focusedIndex();
|
|
63
|
+
if (focusedIdx >= 0 && this.menuItems[focusedIdx]) {
|
|
64
|
+
this.menuItems[focusedIdx].focus();
|
|
65
|
+
} else if (this.menuItems.length > 0) {
|
|
66
|
+
this.menuItems[0].focus();
|
|
67
|
+
this.context.focusedIndex.set(0);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
{ injector: this._injector },
|
|
71
|
+
);
|
|
72
|
+
} else {
|
|
73
|
+
this.isPositioned.set(false);
|
|
74
|
+
this.clampedPos.set(null);
|
|
64
75
|
}
|
|
65
76
|
});
|
|
66
77
|
|
|
@@ -74,15 +85,21 @@ export class ContextMenuContent implements OnDestroy {
|
|
|
74
85
|
readonly class = input<string>('');
|
|
75
86
|
|
|
76
87
|
private readonly _elementRef = inject(ElementRef);
|
|
88
|
+
private readonly _injector = inject(Injector);
|
|
77
89
|
|
|
78
90
|
protected readonly context = inject(CONTEXT_MENU_CONTEXT);
|
|
79
91
|
|
|
92
|
+
protected readonly isPositioned = signal(false);
|
|
93
|
+
protected readonly clampedPos = signal<{ x: number; y: number } | null>(null);
|
|
94
|
+
protected readonly displayPosition = computed(() => this.clampedPos() ?? this.context.position());
|
|
95
|
+
|
|
80
96
|
protected readonly computedClass = computed(() =>
|
|
81
97
|
cn(
|
|
82
98
|
'z-50 min-w-[12rem] overflow-hidden rounded-xl border bg-popover p-2 text-popover-foreground shadow-lg',
|
|
83
99
|
'data-[state=open]:animate-in data-[state=closed]:animate-out',
|
|
84
100
|
'data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0',
|
|
85
101
|
'data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95',
|
|
102
|
+
!this.isPositioned() && 'pointer-events-none opacity-0',
|
|
86
103
|
this.class(),
|
|
87
104
|
),
|
|
88
105
|
);
|
|
@@ -97,12 +114,22 @@ export class ContextMenuContent implements OnDestroy {
|
|
|
97
114
|
}
|
|
98
115
|
}
|
|
99
116
|
|
|
117
|
+
private clampPosition(): void {
|
|
118
|
+
const menu = this._elementRef.nativeElement.querySelector('[role="menu"]') as HTMLElement;
|
|
119
|
+
if (!menu) return;
|
|
120
|
+
const pos = this.context.position();
|
|
121
|
+
const rect = menu.getBoundingClientRect();
|
|
122
|
+
const padding = 8;
|
|
123
|
+
const x = Math.max(padding, Math.min(pos.x, window.innerWidth - rect.width - padding));
|
|
124
|
+
const y = Math.max(padding, Math.min(pos.y, window.innerHeight - rect.height - padding));
|
|
125
|
+
this.clampedPos.set({ x, y });
|
|
126
|
+
}
|
|
100
127
|
private updateMenuItems(): void {
|
|
101
128
|
const content = this._elementRef.nativeElement.querySelector('[role="menu"]');
|
|
102
129
|
if (content) {
|
|
103
130
|
this.menuItems = Array.from(
|
|
104
131
|
content.querySelectorAll(
|
|
105
|
-
'[role="menuitem"]:not([aria-disabled="true"]):not([data-disabled])',
|
|
132
|
+
':is([role="menuitem"],[role="menuitemcheckbox"],[role="menuitemradio"]):not([aria-disabled="true"]):not([data-disabled=""])',
|
|
106
133
|
),
|
|
107
134
|
);
|
|
108
135
|
}
|
|
@@ -194,8 +221,12 @@ export class ContextMenuContent implements OnDestroy {
|
|
|
194
221
|
triggerEl.focus();
|
|
195
222
|
}
|
|
196
223
|
}
|
|
197
|
-
protected onDocumentClick(): void {
|
|
198
|
-
|
|
224
|
+
protected onDocumentClick(event: MouseEvent): void {
|
|
225
|
+
const target = event.target as HTMLElement;
|
|
226
|
+
const host = this._elementRef.nativeElement;
|
|
227
|
+
if (!host.contains(target)) {
|
|
228
|
+
this.close();
|
|
229
|
+
}
|
|
199
230
|
}
|
|
200
231
|
protected onEscapeKey(): void {
|
|
201
232
|
this.close();
|
|
@@ -43,9 +43,11 @@ export class ContextMenuSubContent {
|
|
|
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 { CONTEXT_MENU_SUB_CONTEXT } from './context-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,6 +51,30 @@ export class ContextMenuSubTrigger {
|
|
|
46
51
|
this.subContext.open.set(true);
|
|
47
52
|
}
|
|
48
53
|
protected onMouseLeave(): void {
|
|
49
|
-
|
|
54
|
+
setTimeout(() => {
|
|
55
|
+
if (!this.subContext.isMouseInSubContent()) {
|
|
56
|
+
this.subContext.open.set(false);
|
|
57
|
+
}
|
|
58
|
+
}, 100);
|
|
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('ContextMenuSub')
|
|
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
|
+
}
|
|
50
79
|
}
|
|
51
80
|
}
|
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
|
|
9
9
|
export interface ContextMenuSubContext {
|
|
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 CONTEXT_MENU_SUB_CONTEXT = new InjectionToken<ContextMenuSubContext>(
|
|
@@ -26,6 +28,7 @@ export const CONTEXT_MENU_SUB_CONTEXT = new InjectionToken<ContextMenuSubContext
|
|
|
26
28
|
provide: CONTEXT_MENU_SUB_CONTEXT,
|
|
27
29
|
useFactory: (): ContextMenuSubContext => ({
|
|
28
30
|
open: signal(false),
|
|
31
|
+
isMouseInSubContent: signal(false),
|
|
29
32
|
}),
|
|
30
33
|
},
|
|
31
34
|
],
|