@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
|
@@ -98,15 +98,16 @@ export class ScrollArea {
|
|
|
98
98
|
|
|
99
99
|
protected readonly computedClass = computed(() => cn('relative overflow-hidden', this.class()));
|
|
100
100
|
|
|
101
|
-
protected readonly viewportClass = computed(() =>
|
|
102
|
-
|
|
101
|
+
protected readonly viewportClass = computed(() => {
|
|
102
|
+
const type = this.type();
|
|
103
|
+
return cn(
|
|
103
104
|
'h-full w-full rounded-[inherit]',
|
|
104
105
|
'[&>div]:!block',
|
|
105
|
-
// Hide native scrollbar
|
|
106
|
+
// Hide native scrollbar (custom ScrollBar component provides visual feedback)
|
|
106
107
|
'[&::-webkit-scrollbar]:hidden',
|
|
107
108
|
'[-ms-overflow-style:none]',
|
|
108
109
|
'[scrollbar-width:none]',
|
|
109
|
-
'overflow-auto',
|
|
110
|
-
)
|
|
111
|
-
);
|
|
110
|
+
type === 'always' ? 'overflow-scroll' : 'overflow-auto',
|
|
111
|
+
);
|
|
112
|
+
});
|
|
112
113
|
}
|
|
@@ -13,6 +13,7 @@ import { segmentedItemVariants } from './segmented-variants';
|
|
|
13
13
|
selector: 'SegmentedItem',
|
|
14
14
|
template: `<ng-content />`,
|
|
15
15
|
host: {
|
|
16
|
+
'attr.data-slot': '"segmented-item"',
|
|
16
17
|
'[class]': 'computedClass()',
|
|
17
18
|
'[attr.role]': 'itemRole()',
|
|
18
19
|
'[attr.aria-selected]': 'itemRole() === "tab" ? isSelected() : null',
|
|
@@ -33,6 +33,7 @@ import { segmentedVariants, type SegmentedVariants } from './segmented-variants'
|
|
|
33
33
|
selector: 'Segmented',
|
|
34
34
|
template: `<ng-content />`,
|
|
35
35
|
host: {
|
|
36
|
+
'attr.data-slot': '"segmented"',
|
|
36
37
|
'[class]': 'computedClass()',
|
|
37
38
|
role: 'tablist',
|
|
38
39
|
'[attr.aria-orientation]': '"horizontal"',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { cn } from '@/lib/utils';
|
|
1
|
+
import { cn, Presence } from '@/lib/utils';
|
|
2
2
|
import { ChangeDetectionStrategy, Component, computed, inject, input } from '@angular/core';
|
|
3
3
|
import { SELECT_CONTEXT } from './select-context';
|
|
4
4
|
|
|
@@ -9,25 +9,29 @@ import { SELECT_CONTEXT } from './select-context';
|
|
|
9
9
|
*/
|
|
10
10
|
@Component({
|
|
11
11
|
selector: 'SelectContent',
|
|
12
|
+
imports: [Presence],
|
|
12
13
|
template: `
|
|
13
|
-
<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
<Presence [present]="context?.open() ?? false">
|
|
15
|
+
<div
|
|
16
|
+
[class]="dropdownClass()"
|
|
17
|
+
[attr.id]="context?.contentId"
|
|
18
|
+
[attr.data-state]="context?.open() ? 'open' : 'closed'"
|
|
19
|
+
[attr.data-side]="side()"
|
|
20
|
+
[attr.data-align]="align()"
|
|
21
|
+
[attr.aria-activedescendant]="focusedItemId()"
|
|
22
|
+
role="listbox"
|
|
23
|
+
(keydown.escape)="onEscape()"
|
|
24
|
+
(keydown)="onKeydown($event)"
|
|
25
|
+
>
|
|
26
|
+
<div [class]="viewportClass()">
|
|
27
|
+
<ng-content />
|
|
28
|
+
</div>
|
|
25
29
|
</div>
|
|
26
|
-
</
|
|
30
|
+
</Presence>
|
|
27
31
|
`,
|
|
28
32
|
host: {
|
|
29
33
|
class: 'contents',
|
|
30
|
-
'data-slot': 'select-content',
|
|
34
|
+
'attr.data-slot': '"select-content"',
|
|
31
35
|
},
|
|
32
36
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
33
37
|
})
|
|
@@ -59,6 +63,15 @@ export class SelectContent {
|
|
|
59
63
|
/** Viewport class */
|
|
60
64
|
protected readonly viewportClass = computed(() => cn('max-h-60 overflow-y-auto p-1'));
|
|
61
65
|
|
|
66
|
+
/** ID of the currently focused item for aria-activedescendant */
|
|
67
|
+
protected readonly focusedItemId = computed(() => {
|
|
68
|
+
if (!this.context) return null;
|
|
69
|
+
const values = this.context.itemValues();
|
|
70
|
+
const focusedIndex = this.context.focusedIndex();
|
|
71
|
+
const focusedValue = values[focusedIndex];
|
|
72
|
+
return focusedValue ? `select-item-${focusedValue}` : null;
|
|
73
|
+
});
|
|
74
|
+
|
|
62
75
|
protected onEscape(): void {
|
|
63
76
|
this.context?.setOpen(false);
|
|
64
77
|
const trigger = this.context?.triggerElement();
|
|
@@ -66,4 +79,11 @@ export class SelectContent {
|
|
|
66
79
|
setTimeout(() => trigger.focus());
|
|
67
80
|
}
|
|
68
81
|
}
|
|
82
|
+
|
|
83
|
+
/** Handle printable character keys for typeahead search */
|
|
84
|
+
onKeydown(event: KeyboardEvent): void {
|
|
85
|
+
if (event.key.length === 1 && !event.ctrlKey && !event.metaKey && !event.altKey) {
|
|
86
|
+
this.context?.handleTypeahead(event.key);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
69
89
|
}
|
|
@@ -31,6 +31,16 @@ export interface SelectContext {
|
|
|
31
31
|
required?: () => boolean;
|
|
32
32
|
/** Name for form submission */
|
|
33
33
|
name?: () => string;
|
|
34
|
+
/** Map of item value → display label for typeahead */
|
|
35
|
+
itemLabels: WritableSignal<Map<string, string>>;
|
|
36
|
+
/** Accumulated typeahead characters */
|
|
37
|
+
typeaheadBuffer: WritableSignal<string>;
|
|
38
|
+
/** Timeout handle for clearing the typeahead buffer */
|
|
39
|
+
typeaheadTimeout: WritableSignal<ReturnType<typeof setTimeout> | null>;
|
|
40
|
+
/** Handle a typed character for typeahead search */
|
|
41
|
+
handleTypeahead: (char: string) => void;
|
|
42
|
+
/** Focus the first item whose label starts with query (case-insensitive) */
|
|
43
|
+
focusMatchingItem: (query: string) => void;
|
|
34
44
|
}
|
|
35
45
|
|
|
36
46
|
export interface SelectGroupContext {
|
|
@@ -47,6 +47,7 @@ import { SELECT_CONTEXT } from './select-context';
|
|
|
47
47
|
host: {
|
|
48
48
|
'[class]': 'computedClass()',
|
|
49
49
|
role: 'option',
|
|
50
|
+
'[attr.id]': 'itemId()',
|
|
50
51
|
'[attr.aria-selected]': 'isSelected()',
|
|
51
52
|
'[attr.data-state]': 'isSelected() ? "checked" : "unchecked"',
|
|
52
53
|
'[attr.data-disabled]': 'disabled() ? "" : null',
|
|
@@ -55,7 +56,7 @@ import { SELECT_CONTEXT } from './select-context';
|
|
|
55
56
|
'[attr.tabindex]': 'disabled() ? -1 : 0',
|
|
56
57
|
'(click)': 'select()',
|
|
57
58
|
'(keydown)': 'onKeyDown($event)',
|
|
58
|
-
'data-slot': 'select-item',
|
|
59
|
+
'attr.data-slot': '"select-item"',
|
|
59
60
|
},
|
|
60
61
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
61
62
|
})
|
|
@@ -72,6 +73,9 @@ export class SelectItem implements OnInit, OnDestroy {
|
|
|
72
73
|
|
|
73
74
|
private readonly _context = inject(SELECT_CONTEXT, { optional: true });
|
|
74
75
|
|
|
76
|
+
/** Stable DOM id for aria-activedescendant */
|
|
77
|
+
readonly itemId = computed(() => `select-item-${this.value()}`);
|
|
78
|
+
|
|
75
79
|
/** Whether this item is selected */
|
|
76
80
|
protected readonly isSelected = computed(() => {
|
|
77
81
|
return this._context?.value() === this.value();
|
|
@@ -94,17 +98,28 @@ export class SelectItem implements OnInit, OnDestroy {
|
|
|
94
98
|
});
|
|
95
99
|
|
|
96
100
|
ngOnInit(): void {
|
|
97
|
-
// Register this item
|
|
101
|
+
// Register this item's value
|
|
98
102
|
this._context?.itemValues.update((values) => {
|
|
99
103
|
if (!values.includes(this.value())) {
|
|
100
104
|
return [...values, this.value()];
|
|
101
105
|
}
|
|
102
106
|
return values;
|
|
103
107
|
});
|
|
108
|
+
// Register this item's label for typeahead
|
|
109
|
+
this._context?.itemLabels.update((m) => {
|
|
110
|
+
const label = this.textContent()?.nativeElement?.textContent?.trim() || this.value();
|
|
111
|
+
m.set(this.value(), label);
|
|
112
|
+
return m;
|
|
113
|
+
});
|
|
104
114
|
}
|
|
105
115
|
ngOnDestroy(): void {
|
|
106
|
-
// Unregister this item
|
|
116
|
+
// Unregister this item's value
|
|
107
117
|
this._context?.itemValues.update((values) => values.filter((v) => v !== this.value()));
|
|
118
|
+
// Unregister this item's label
|
|
119
|
+
this._context?.itemLabels.update((m) => {
|
|
120
|
+
m.delete(this.value());
|
|
121
|
+
return m;
|
|
122
|
+
});
|
|
108
123
|
}
|
|
109
124
|
|
|
110
125
|
/** Handle keyboard navigation */
|
|
@@ -117,17 +132,20 @@ export class SelectItem implements OnInit, OnDestroy {
|
|
|
117
132
|
event.preventDefault();
|
|
118
133
|
this.select();
|
|
119
134
|
break;
|
|
120
|
-
case 'ArrowDown':
|
|
135
|
+
case 'ArrowDown': {
|
|
121
136
|
event.preventDefault();
|
|
122
137
|
const currentIndex = this._context.focusedIndex();
|
|
123
138
|
const itemCount = this._context.itemValues().length;
|
|
124
|
-
this._context.focusItem(
|
|
139
|
+
this._context.focusItem((currentIndex + 1) % itemCount);
|
|
125
140
|
break;
|
|
126
|
-
|
|
141
|
+
}
|
|
142
|
+
case 'ArrowUp': {
|
|
127
143
|
event.preventDefault();
|
|
128
144
|
const idx = this._context.focusedIndex();
|
|
129
|
-
this._context.
|
|
145
|
+
const count = this._context.itemValues().length;
|
|
146
|
+
this._context.focusItem(idx > 0 ? idx - 1 : count - 1);
|
|
130
147
|
break;
|
|
148
|
+
}
|
|
131
149
|
case 'Home':
|
|
132
150
|
event.preventDefault();
|
|
133
151
|
this._context.focusItem(0);
|
|
@@ -57,7 +57,7 @@ import { SELECT_CONTEXT } from './select-context';
|
|
|
57
57
|
`,
|
|
58
58
|
host: {
|
|
59
59
|
class: 'contents',
|
|
60
|
-
'data-slot': 'select-trigger',
|
|
60
|
+
'attr.data-slot': '"select-trigger"',
|
|
61
61
|
},
|
|
62
62
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
63
63
|
})
|
|
@@ -114,35 +114,28 @@ export class SelectTrigger {
|
|
|
114
114
|
case 'ArrowDown':
|
|
115
115
|
event.preventDefault();
|
|
116
116
|
if (!this.context.open()) {
|
|
117
|
-
// Save trigger element
|
|
118
117
|
const button = this.triggerButton()?.nativeElement;
|
|
119
|
-
if (button)
|
|
120
|
-
this.context.triggerElement.set(button);
|
|
121
|
-
}
|
|
118
|
+
if (button) this.context.triggerElement.set(button);
|
|
122
119
|
this.context.setOpen(true);
|
|
123
120
|
setTimeout(() => this.context?.focusItem(0));
|
|
124
121
|
} else {
|
|
125
|
-
// Move to next item
|
|
126
122
|
const currentIndex = this.context.focusedIndex();
|
|
127
123
|
const itemCount = this.context.itemValues().length;
|
|
128
|
-
this.context.focusItem(
|
|
124
|
+
this.context.focusItem((currentIndex + 1) % itemCount);
|
|
129
125
|
}
|
|
130
126
|
break;
|
|
131
127
|
case 'ArrowUp':
|
|
132
128
|
event.preventDefault();
|
|
133
129
|
if (!this.context.open()) {
|
|
134
|
-
// Save trigger element
|
|
135
130
|
const button = this.triggerButton()?.nativeElement;
|
|
136
|
-
if (button)
|
|
137
|
-
this.context.triggerElement.set(button);
|
|
138
|
-
}
|
|
131
|
+
if (button) this.context.triggerElement.set(button);
|
|
139
132
|
this.context.setOpen(true);
|
|
140
133
|
const lastIndex = this.context.itemValues().length - 1;
|
|
141
134
|
setTimeout(() => this.context?.focusItem(lastIndex));
|
|
142
135
|
} else {
|
|
143
|
-
// Move to previous item
|
|
144
136
|
const currentIndex = this.context.focusedIndex();
|
|
145
|
-
this.context.
|
|
137
|
+
const itemCount = this.context.itemValues().length;
|
|
138
|
+
this.context.focusItem(currentIndex > 0 ? currentIndex - 1 : itemCount - 1);
|
|
146
139
|
}
|
|
147
140
|
break;
|
|
148
141
|
case 'Escape':
|
|
@@ -199,6 +199,11 @@ export class Select {
|
|
|
199
199
|
private readonly _isDisabled = signal<boolean>(false);
|
|
200
200
|
|
|
201
201
|
private readonly ariaIds = this._ariaIdService.generateMenuIds('select');
|
|
202
|
+
/** Internal typeahead signals */
|
|
203
|
+
private readonly _itemLabels = signal<Map<string, string>>(new Map());
|
|
204
|
+
private readonly _typeaheadBuffer = signal<string>('');
|
|
205
|
+
private readonly _typeaheadTimeout = signal<ReturnType<typeof setTimeout> | null>(null);
|
|
206
|
+
|
|
202
207
|
/** Context for child components */
|
|
203
208
|
readonly context: SelectContext = {
|
|
204
209
|
value: this._value,
|
|
@@ -232,6 +237,11 @@ export class Select {
|
|
|
232
237
|
}
|
|
233
238
|
},
|
|
234
239
|
focusItem: (index: number) => this.focusItemByIndex(index),
|
|
240
|
+
itemLabels: this._itemLabels,
|
|
241
|
+
typeaheadBuffer: this._typeaheadBuffer,
|
|
242
|
+
typeaheadTimeout: this._typeaheadTimeout,
|
|
243
|
+
handleTypeahead: (char: string) => this.handleTypeahead(char),
|
|
244
|
+
focusMatchingItem: (query: string) => this.focusMatchingItem(query),
|
|
235
245
|
};
|
|
236
246
|
|
|
237
247
|
/** Focus an item by index */
|
|
@@ -248,4 +258,40 @@ export class Select {
|
|
|
248
258
|
selectItem.focus();
|
|
249
259
|
}
|
|
250
260
|
}
|
|
261
|
+
|
|
262
|
+
/** Handle typeahead: accumulate char, clear after 500 ms, then focus matching item */
|
|
263
|
+
private handleTypeahead(char: string): void {
|
|
264
|
+
// Clear existing timeout
|
|
265
|
+
const existingTimeout = this._typeaheadTimeout();
|
|
266
|
+
if (existingTimeout !== null) {
|
|
267
|
+
clearTimeout(existingTimeout);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const newBuffer = this._typeaheadBuffer() + char;
|
|
271
|
+
this._typeaheadBuffer.set(newBuffer);
|
|
272
|
+
|
|
273
|
+
const timeout = setTimeout(() => {
|
|
274
|
+
this._typeaheadBuffer.set('');
|
|
275
|
+
this._typeaheadTimeout.set(null);
|
|
276
|
+
}, 500);
|
|
277
|
+
this._typeaheadTimeout.set(timeout);
|
|
278
|
+
|
|
279
|
+
this.focusMatchingItem(newBuffer);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/** Focus the first item whose label starts with query (case-insensitive) */
|
|
283
|
+
private focusMatchingItem(query: string): void {
|
|
284
|
+
const values = this.context.itemValues();
|
|
285
|
+
const labels = this._itemLabels();
|
|
286
|
+
const lowerQuery = query.toLowerCase();
|
|
287
|
+
|
|
288
|
+
const matchIndex = values.findIndex((v) => {
|
|
289
|
+
const label = labels.get(v) ?? v;
|
|
290
|
+
return label.toLowerCase().startsWith(lowerQuery);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
if (matchIndex !== -1) {
|
|
294
|
+
this.focusItemByIndex(matchIndex);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
251
297
|
}
|
|
@@ -83,13 +83,21 @@ import { sheetVariants, type SheetVariants } from './sheet-variants';
|
|
|
83
83
|
})
|
|
84
84
|
export class SheetContent implements OnDestroy {
|
|
85
85
|
constructor() {
|
|
86
|
-
|
|
86
|
+
let wasOpen = false;
|
|
87
|
+
|
|
88
|
+
// Handle body scroll lock and focus restoration based on open state
|
|
87
89
|
effect(() => {
|
|
88
90
|
const isOpen = this.context.open();
|
|
89
91
|
if (isOpen) {
|
|
92
|
+
wasOpen = true;
|
|
90
93
|
this.lockBodyScroll();
|
|
91
94
|
} else {
|
|
92
95
|
this.unlockBodyScroll();
|
|
96
|
+
// Restore focus whenever sheet closes (covers close button, overlay, Escape, programmatic)
|
|
97
|
+
if (wasOpen) {
|
|
98
|
+
this.restoreFocus();
|
|
99
|
+
}
|
|
100
|
+
wasOpen = false;
|
|
93
101
|
}
|
|
94
102
|
});
|
|
95
103
|
}
|
|
@@ -105,13 +113,14 @@ export class SheetContent implements OnDestroy {
|
|
|
105
113
|
cn(sheetVariants({ side: this.side() }), this.class()),
|
|
106
114
|
);
|
|
107
115
|
|
|
108
|
-
/** Previous body overflow for restoration */
|
|
116
|
+
/** Previous body overflow/padding for restoration */
|
|
109
117
|
private previousBodyOverflow = '';
|
|
118
|
+
private previousBodyPaddingRight = '';
|
|
110
119
|
|
|
111
120
|
ngOnDestroy(): void {
|
|
112
121
|
// Restore body scroll
|
|
113
122
|
this.unlockBodyScroll();
|
|
114
|
-
// Restore focus to trigger element
|
|
123
|
+
// Restore focus to trigger element (fallback if not already called via effect)
|
|
115
124
|
this.restoreFocus();
|
|
116
125
|
}
|
|
117
126
|
|
|
@@ -120,7 +129,10 @@ export class SheetContent implements OnDestroy {
|
|
|
120
129
|
this.close();
|
|
121
130
|
}
|
|
122
131
|
onEscapeKey(): void {
|
|
123
|
-
|
|
132
|
+
// Guard: only close when the sheet is actually open (not during exit animation)
|
|
133
|
+
if (this.context.open()) {
|
|
134
|
+
this.close();
|
|
135
|
+
}
|
|
124
136
|
}
|
|
125
137
|
onClose(): void {
|
|
126
138
|
this.close();
|
|
@@ -128,17 +140,22 @@ export class SheetContent implements OnDestroy {
|
|
|
128
140
|
|
|
129
141
|
private lockBodyScroll(): void {
|
|
130
142
|
if (typeof document !== 'undefined') {
|
|
143
|
+
const scrollbarWidth = window.innerWidth - document.documentElement.clientWidth;
|
|
131
144
|
this.previousBodyOverflow = document.body.style.overflow;
|
|
145
|
+
this.previousBodyPaddingRight = document.body.style.paddingRight;
|
|
132
146
|
document.body.style.overflow = 'hidden';
|
|
147
|
+
if (scrollbarWidth > 0) {
|
|
148
|
+
document.body.style.paddingRight = scrollbarWidth + 'px';
|
|
149
|
+
}
|
|
133
150
|
}
|
|
134
151
|
}
|
|
135
152
|
private unlockBodyScroll(): void {
|
|
136
153
|
if (typeof document !== 'undefined') {
|
|
137
154
|
document.body.style.overflow = this.previousBodyOverflow;
|
|
155
|
+
document.body.style.paddingRight = this.previousBodyPaddingRight;
|
|
138
156
|
}
|
|
139
157
|
}
|
|
140
158
|
private close(): void {
|
|
141
|
-
this.restoreFocus();
|
|
142
159
|
this.context.setOpen(false);
|
|
143
160
|
}
|
|
144
161
|
private restoreFocus(): void {
|
|
@@ -346,10 +346,10 @@ export class Slider implements ControlValueAccessor {
|
|
|
346
346
|
newValue -= stepValue * increment;
|
|
347
347
|
break;
|
|
348
348
|
case 'ArrowUp':
|
|
349
|
-
newValue += stepValue;
|
|
349
|
+
newValue += stepValue * (this.inverted() ? -1 : 1);
|
|
350
350
|
break;
|
|
351
351
|
case 'ArrowDown':
|
|
352
|
-
newValue -= stepValue;
|
|
352
|
+
newValue -= stepValue * (this.inverted() ? -1 : 1);
|
|
353
353
|
break;
|
|
354
354
|
case 'PageUp':
|
|
355
355
|
newValue += largeStep;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { ChangeDetectionStrategy, Component, input } from '@angular/core';
|
|
2
|
+
import { NgxSonnerToaster } from 'ngx-sonner';
|
|
3
|
+
|
|
4
|
+
export { toast } from 'ngx-sonner';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Sonner component - toast notification toaster.
|
|
8
|
+
* Wraps ngx-sonner's Toaster with shadcn/ui default styling.
|
|
9
|
+
* Place once near the root of your app.
|
|
10
|
+
*
|
|
11
|
+
* @example
|
|
12
|
+
* <!-- In app root template -->
|
|
13
|
+
* <Sonner />
|
|
14
|
+
*
|
|
15
|
+
* <!-- With custom position -->
|
|
16
|
+
* <Sonner position="top-right" />
|
|
17
|
+
*
|
|
18
|
+
* <!-- Then trigger toasts anywhere -->
|
|
19
|
+
* import { toast } from '@/lib/components/ui/sonner';
|
|
20
|
+
* toast('Hello world');
|
|
21
|
+
* toast.success('Saved!');
|
|
22
|
+
* toast.error('Something went wrong');
|
|
23
|
+
*/
|
|
24
|
+
@Component({
|
|
25
|
+
selector: 'Sonner',
|
|
26
|
+
imports: [NgxSonnerToaster],
|
|
27
|
+
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
28
|
+
host: {
|
|
29
|
+
'attr.data-slot': '"sonner"',
|
|
30
|
+
style: 'display: contents',
|
|
31
|
+
},
|
|
32
|
+
template: `
|
|
33
|
+
<ngx-sonner-toaster
|
|
34
|
+
[theme]="theme()"
|
|
35
|
+
[position]="position()"
|
|
36
|
+
[richColors]="richColors()"
|
|
37
|
+
[expand]="expand()"
|
|
38
|
+
[duration]="duration()"
|
|
39
|
+
[visibleToasts]="visibleToasts()"
|
|
40
|
+
[closeButton]="closeButton()"
|
|
41
|
+
[offset]="offset()"
|
|
42
|
+
[dir]="dir()"
|
|
43
|
+
[class]="toasterClass()"
|
|
44
|
+
/>
|
|
45
|
+
`,
|
|
46
|
+
})
|
|
47
|
+
export class Sonner {
|
|
48
|
+
/** Visual theme */
|
|
49
|
+
readonly theme = input<'light' | 'dark' | 'system'>('system');
|
|
50
|
+
/** Position of the toaster */
|
|
51
|
+
readonly position = input<
|
|
52
|
+
'top-left' | 'top-center' | 'top-right' | 'bottom-left' | 'bottom-center' | 'bottom-right'
|
|
53
|
+
>('bottom-right');
|
|
54
|
+
/** Use rich colors for success/error/warning/info */
|
|
55
|
+
readonly richColors = input<boolean>(false);
|
|
56
|
+
/** Expand toasts by default */
|
|
57
|
+
readonly expand = input<boolean>(false);
|
|
58
|
+
/** Default duration in ms */
|
|
59
|
+
readonly duration = input<number>(4000);
|
|
60
|
+
/** Max visible toasts */
|
|
61
|
+
readonly visibleToasts = input<number>(3);
|
|
62
|
+
/** Show close button on each toast */
|
|
63
|
+
readonly closeButton = input<boolean>(false);
|
|
64
|
+
/** Offset from edge */
|
|
65
|
+
readonly offset = input<string | number | null>(null);
|
|
66
|
+
/** Text direction */
|
|
67
|
+
readonly dir = input<'ltr' | 'rtl' | 'auto'>('auto');
|
|
68
|
+
/** Additional CSS classes passed to the toaster */
|
|
69
|
+
readonly toasterClass = input<string>('toaster group');
|
|
70
|
+
}
|
|
@@ -103,9 +103,7 @@ export type SwitchProps = {
|
|
|
103
103
|
[attr.data-disabled]="isDisabled() ? '' : null"
|
|
104
104
|
[disabled]="isDisabled()"
|
|
105
105
|
[class]="trackClass()"
|
|
106
|
-
[style.backgroundColor]="checked() ? checkedBgColor() : 'var(--color-input)'"
|
|
107
106
|
(click)="toggle()"
|
|
108
|
-
(keydown)="onKeyDown($event)"
|
|
109
107
|
>
|
|
110
108
|
<span data-slot="switch-thumb" [class]="thumbClass()" [attr.data-state]="state()"></span>
|
|
111
109
|
</button>
|
|
@@ -125,7 +123,7 @@ export type SwitchProps = {
|
|
|
125
123
|
`,
|
|
126
124
|
host: {
|
|
127
125
|
'[class]': 'computedClass()',
|
|
128
|
-
'data-slot': 'switch',
|
|
126
|
+
'attr.data-slot': '"switch"',
|
|
129
127
|
},
|
|
130
128
|
providers: [
|
|
131
129
|
{
|
|
@@ -168,9 +166,6 @@ export class Switch implements ControlValueAccessor {
|
|
|
168
166
|
readonly class = input<string>('');
|
|
169
167
|
/** Additional CSS classes to apply to the inner track button */
|
|
170
168
|
readonly buttonClass = input<string>('');
|
|
171
|
-
/** Background color for checked state (hex, rgb, or CSS color name) */
|
|
172
|
-
readonly checkedBgColor = input<string>('rgb(59, 130, 246)');
|
|
173
|
-
|
|
174
169
|
/** Current state for data attribute */
|
|
175
170
|
protected readonly state = computed(
|
|
176
171
|
(): SwitchState => (this.checked() ? 'checked' : 'unchecked'),
|
|
@@ -246,12 +241,4 @@ export class Switch implements ControlValueAccessor {
|
|
|
246
241
|
}
|
|
247
242
|
}
|
|
248
243
|
|
|
249
|
-
/** Handle keyboard events */
|
|
250
|
-
protected onKeyDown(event: KeyboardEvent): void {
|
|
251
|
-
// Switch should only respond to Space (Enter is handled by button default)
|
|
252
|
-
if (event.key === ' ') {
|
|
253
|
-
// Let the click handler deal with it
|
|
254
|
-
return;
|
|
255
|
-
}
|
|
256
|
-
}
|
|
257
244
|
}
|
|
@@ -187,6 +187,12 @@ export class TabsList {
|
|
|
187
187
|
|
|
188
188
|
if (handled) {
|
|
189
189
|
event.preventDefault();
|
|
190
|
+
|
|
191
|
+
// Skip disabled tabs — scan in the movement direction
|
|
192
|
+
const direction =
|
|
193
|
+
event.key === 'ArrowLeft' || event.key === 'ArrowUp' || event.key === 'End' ? -1 : 1;
|
|
194
|
+
newIndex = this.findEnabledIndex(tabValues, newIndex, direction);
|
|
195
|
+
|
|
190
196
|
const newValue = tabValues[newIndex];
|
|
191
197
|
|
|
192
198
|
// In automatic mode, activate on focus; in manual mode, just focus
|
|
@@ -201,6 +207,18 @@ export class TabsList {
|
|
|
201
207
|
}
|
|
202
208
|
}
|
|
203
209
|
|
|
210
|
+
private findEnabledIndex(tabValues: string[], startIndex: number, direction: 1 | -1): number {
|
|
211
|
+
const length = tabValues.length;
|
|
212
|
+
let index = startIndex;
|
|
213
|
+
for (let i = 0; i < length; i++) {
|
|
214
|
+
const tabId = this.tabs.getTabId(tabValues[index]);
|
|
215
|
+
const el = document.getElementById(tabId);
|
|
216
|
+
if (!el?.hasAttribute('data-disabled')) return index;
|
|
217
|
+
index = ((index + direction) + length) % length;
|
|
218
|
+
}
|
|
219
|
+
return startIndex;
|
|
220
|
+
}
|
|
221
|
+
|
|
204
222
|
private updateIndicator(): void {
|
|
205
223
|
const activeValue = this.tabs.value();
|
|
206
224
|
if (!activeValue) return;
|
|
@@ -83,7 +83,6 @@ export interface TabsTriggerProps {
|
|
|
83
83
|
'[attr.aria-controls]': 'panelId()',
|
|
84
84
|
'[attr.aria-disabled]': 'disabled() || null',
|
|
85
85
|
'[attr.tabindex]': 'isActive() ? 0 : -1',
|
|
86
|
-
'[attr.disabled]': 'disabled() ? "" : null',
|
|
87
86
|
'(click)': 'onClick()',
|
|
88
87
|
'(keydown.enter)': 'onClick()',
|
|
89
88
|
'(keydown.space)': 'onSpace($event)',
|
|
@@ -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);
|