@praxisui/tabs 9.0.56 → 9.0.57
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/README.md +10 -0
- package/ai/component-registry.json +2 -2
- package/fesm2022/praxisui-tabs.mjs +91 -5
- package/package.json +5 -5
- package/types/praxisui-tabs.d.ts +11 -0
package/README.md
CHANGED
|
@@ -73,6 +73,16 @@ export class TabsHostComponent {
|
|
|
73
73
|
|
|
74
74
|
Controlled `selectedIndex` updates the active tab without re-emitting `selectedIndexChange`, which prevents loops in dynamic-page state projections.
|
|
75
75
|
|
|
76
|
+
Nested widgets declared in `tabs[].widgets` or `nav.links[].widgets` expose their
|
|
77
|
+
registered outputs automatically through the contextual `widgetEvent` envelope.
|
|
78
|
+
Do not repeat component outputs in each nested widget definition only to make a
|
|
79
|
+
`UiCompositionPlan` link work. The envelope preserves the container/tab path and
|
|
80
|
+
`childWidgetKey`, allowing Dynamic Page to resolve canonical `nestedPath` links.
|
|
81
|
+
|
|
82
|
+
When Dynamic Page reprojects a config object solely to deliver a nested input,
|
|
83
|
+
Tabs preserves the active tab by stable tab/link id. An explicitly authored
|
|
84
|
+
`group.selectedIndex` or `nav.selectedIndex` change remains authoritative.
|
|
85
|
+
|
|
76
86
|
For navigation where the host renders the active content externally, set `config.group.renderBody = false` in group mode or `config.nav.renderBody = false` in nav mode. The component still owns the Material tab header/link header, selected index, keyboard navigation and selection ARIA, but it does not render internal tab/nav bodies or empty states. Hosts should render the external active panel next to the tabs and give that panel a stable accessible label that matches the active section.
|
|
77
87
|
|
|
78
88
|
## Metadata Shape
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": "1.0.0",
|
|
3
|
-
"generatedAt": "2026-09-
|
|
3
|
+
"generatedAt": "2026-09-01T14:12:48.929Z",
|
|
4
4
|
"packageName": "@praxisui/tabs",
|
|
5
|
-
"packageVersion": "9.0.
|
|
5
|
+
"packageVersion": "9.0.57",
|
|
6
6
|
"sourceRegistry": "praxis-component-registry-ingestion",
|
|
7
7
|
"sourceRegistryVersion": "1.0.0",
|
|
8
8
|
"componentCount": 1,
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { inject, Input, Inject, Component, ChangeDetectorRef, effect, EventEmitter, signal, Output, ChangeDetectionStrategy,
|
|
2
|
+
import { inject, Input, Inject, Component, ChangeDetectorRef, effect, EventEmitter, signal, ViewChild, Output, ChangeDetectionStrategy, ENVIRONMENT_INITIALIZER } from '@angular/core';
|
|
3
3
|
import { ActivatedRoute } from '@angular/router';
|
|
4
4
|
import * as i1$1 from '@angular/common';
|
|
5
5
|
import { CommonModule } from '@angular/common';
|
|
6
6
|
import * as i3 from '@angular/material/tabs';
|
|
7
|
-
import { MatTabsModule } from '@angular/material/tabs';
|
|
7
|
+
import { MatTabsModule, MatTabGroup } from '@angular/material/tabs';
|
|
8
8
|
import * as i4$1 from '@angular/material/icon';
|
|
9
9
|
import { MatIconModule } from '@angular/material/icon';
|
|
10
10
|
import * as i10 from '@angular/material/tooltip';
|
|
@@ -3182,6 +3182,8 @@ class PraxisTabs {
|
|
|
3182
3182
|
groupLoaded = new Set();
|
|
3183
3183
|
navLoaded = new Set();
|
|
3184
3184
|
controlledSelectedIndex;
|
|
3185
|
+
destroyed = false;
|
|
3186
|
+
groupComponent;
|
|
3185
3187
|
destroy$ = new Subject();
|
|
3186
3188
|
widgetDefinitionCache = new WeakMap();
|
|
3187
3189
|
generatedContentForm = new FormGroup({});
|
|
@@ -3200,8 +3202,10 @@ class PraxisTabs {
|
|
|
3200
3202
|
// previous inputs in that case (for example, a newly selected parent
|
|
3201
3203
|
// identity projected into a related-resource outlet).
|
|
3202
3204
|
this.widgetDefinitionCache = new WeakMap();
|
|
3203
|
-
//
|
|
3204
|
-
|
|
3205
|
+
// Runtime composition can replace the config object only to project a
|
|
3206
|
+
// nested child input. Preserve the user's active tab when the incoming
|
|
3207
|
+
// document did not author a different selectedIndex explicitly.
|
|
3208
|
+
this.syncSelectionFromConfigChange(changes['config']);
|
|
3205
3209
|
this.syncGeneratedContentForm();
|
|
3206
3210
|
// Persist when tabsId provided and the instance is storage-owned.
|
|
3207
3211
|
this.persistConfig(this.config);
|
|
@@ -3209,6 +3213,7 @@ class PraxisTabs {
|
|
|
3209
3213
|
}
|
|
3210
3214
|
}
|
|
3211
3215
|
ngOnDestroy() {
|
|
3216
|
+
this.destroyed = true;
|
|
3212
3217
|
this.assistantSessions.removeContextSession(this.buildAiAssistantContextSnapshot().identity);
|
|
3213
3218
|
this.aiAssistantStateSubscription?.unsubscribe();
|
|
3214
3219
|
this.destroy$.next();
|
|
@@ -3253,8 +3258,45 @@ class PraxisTabs {
|
|
|
3253
3258
|
const entry = this.visibleTabEntries()[index];
|
|
3254
3259
|
if (!entry)
|
|
3255
3260
|
return;
|
|
3261
|
+
if (entry.index === this.selectedIndexSignal())
|
|
3262
|
+
return;
|
|
3256
3263
|
this.onSelectedIndexChange(entry.index);
|
|
3257
3264
|
}
|
|
3265
|
+
/**
|
|
3266
|
+
* Material commits a pointer-selected tab during its next content-check cycle.
|
|
3267
|
+
* A Tabs instance hosted by DynamicWidgetLoader can receive a composition
|
|
3268
|
+
* reprojection in that same cycle, leaving Material's pending index uncommitted.
|
|
3269
|
+
* Resolve the semantic tab identity at the container boundary so pointer
|
|
3270
|
+
* selection remains deterministic without changing keyboard focus semantics.
|
|
3271
|
+
*/
|
|
3272
|
+
onGroupPointerClick(event) {
|
|
3273
|
+
const tabElement = event.composedPath().find((candidate) => (candidate instanceof HTMLElement && candidate.getAttribute('role') === 'tab'));
|
|
3274
|
+
if (!tabElement || tabElement.getAttribute('aria-disabled') === 'true')
|
|
3275
|
+
return;
|
|
3276
|
+
const entries = this.visibleTabEntries();
|
|
3277
|
+
const tabId = tabElement.id;
|
|
3278
|
+
const byId = tabId
|
|
3279
|
+
? entries.findIndex((entry) => entry.tab.id === tabId)
|
|
3280
|
+
: -1;
|
|
3281
|
+
const ariaPosition = Number(tabElement.getAttribute('aria-posinset')) - 1;
|
|
3282
|
+
const visibleIndex = byId >= 0
|
|
3283
|
+
? byId
|
|
3284
|
+
: Number.isInteger(ariaPosition) ? ariaPosition : -1;
|
|
3285
|
+
if (visibleIndex < 0 || visibleIndex >= entries.length)
|
|
3286
|
+
return;
|
|
3287
|
+
this.onVisibleTabIndexChange(visibleIndex);
|
|
3288
|
+
// Dynamic widget hosts can reconcile inputs outside the regular parent
|
|
3289
|
+
// change-detection traversal. Material records its pending selection in
|
|
3290
|
+
// the target click handler, so finish the local check after bubbling ends.
|
|
3291
|
+
queueMicrotask(() => {
|
|
3292
|
+
if (!this.destroyed) {
|
|
3293
|
+
// Commit Material's delayed selection before rendering nested content;
|
|
3294
|
+
// a child widget may fail independently while loading remote metadata.
|
|
3295
|
+
this.groupComponent?.ngAfterContentChecked();
|
|
3296
|
+
this.cdr.detectChanges();
|
|
3297
|
+
}
|
|
3298
|
+
});
|
|
3299
|
+
}
|
|
3258
3300
|
onNavClick(i) {
|
|
3259
3301
|
if (!this.config?.nav?.links?.length)
|
|
3260
3302
|
return;
|
|
@@ -3893,6 +3935,41 @@ class PraxisTabs {
|
|
|
3893
3935
|
if (linksLength > 0)
|
|
3894
3936
|
this.navLoaded.add(navIndex);
|
|
3895
3937
|
}
|
|
3938
|
+
syncSelectionFromConfigChange(change) {
|
|
3939
|
+
if (change.firstChange) {
|
|
3940
|
+
this.syncSelectionFromConfig();
|
|
3941
|
+
return;
|
|
3942
|
+
}
|
|
3943
|
+
const previous = change.previousValue;
|
|
3944
|
+
const current = change.currentValue;
|
|
3945
|
+
const previousGroupIndex = this.selectedIndexSignal();
|
|
3946
|
+
const previousNavIndex = this.currentNavIndex();
|
|
3947
|
+
const previousTabId = previous?.tabs?.[previousGroupIndex]?.id;
|
|
3948
|
+
const previousNavId = previous?.nav?.links?.[previousNavIndex]?.id;
|
|
3949
|
+
const groupSelectionAuthored = previous?.group?.selectedIndex !== current?.group?.selectedIndex;
|
|
3950
|
+
const navSelectionAuthored = previous?.nav?.selectedIndex !== current?.nav?.selectedIndex;
|
|
3951
|
+
this.syncSelectionFromConfig();
|
|
3952
|
+
if (!groupSelectionAuthored && (current?.tabs?.length ?? 0) > 0) {
|
|
3953
|
+
const preservedGroupIndex = previousTabId
|
|
3954
|
+
? current.tabs.findIndex((tab) => tab.id === previousTabId)
|
|
3955
|
+
: this.clampIndex(previousGroupIndex, current.tabs.length);
|
|
3956
|
+
const nextGroupIndex = preservedGroupIndex >= 0
|
|
3957
|
+
? preservedGroupIndex
|
|
3958
|
+
: this.clampIndex(previousGroupIndex, current.tabs.length);
|
|
3959
|
+
this.selectedIndexSignal.set(nextGroupIndex);
|
|
3960
|
+
this.groupLoaded.add(nextGroupIndex);
|
|
3961
|
+
}
|
|
3962
|
+
if (!navSelectionAuthored && (current?.nav?.links?.length ?? 0) > 0) {
|
|
3963
|
+
const preservedNavIndex = previousNavId
|
|
3964
|
+
? current.nav.links.findIndex((link) => link.id === previousNavId)
|
|
3965
|
+
: this.clampIndex(previousNavIndex, current.nav.links.length);
|
|
3966
|
+
const nextNavIndex = preservedNavIndex >= 0
|
|
3967
|
+
? preservedNavIndex
|
|
3968
|
+
: this.clampIndex(previousNavIndex, current.nav.links.length);
|
|
3969
|
+
this.currentNavIndex.set(nextNavIndex);
|
|
3970
|
+
this.navLoaded.add(nextNavIndex);
|
|
3971
|
+
}
|
|
3972
|
+
}
|
|
3896
3973
|
pruneLoadedIndexes(indexes, size) {
|
|
3897
3974
|
for (const index of Array.from(indexes)) {
|
|
3898
3975
|
if (index < 0 || index >= size) {
|
|
@@ -4237,7 +4314,7 @@ class PraxisTabs {
|
|
|
4237
4314
|
return JSON.parse(JSON.stringify(config));
|
|
4238
4315
|
}
|
|
4239
4316
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.2.14", ngImport: i0, type: PraxisTabs, deps: [], target: i0.ɵɵFactoryTarget.Component });
|
|
4240
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisTabs, isStandalone: true, selector: "praxis-tabs", inputs: { config: "config", tabsId: "tabsId", componentInstanceId: "componentInstanceId", configPersistenceStrategy: "configPersistenceStrategy", selectedIndex: "selectedIndex", enableCustomization: "enableCustomization", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", configChange: "configChange", widgetEvent: "widgetEvent" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], usesOnChanges: true, ngImport: i0, template: `
|
|
4317
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "21.2.14", type: PraxisTabs, isStandalone: true, selector: "praxis-tabs", inputs: { config: "config", tabsId: "tabsId", componentInstanceId: "componentInstanceId", configPersistenceStrategy: "configPersistenceStrategy", selectedIndex: "selectedIndex", enableCustomization: "enableCustomization", form: "form", context: "context" }, outputs: { animationDone: "animationDone", focusChange: "focusChange", selectedIndexChange: "selectedIndexChange", selectedTabChange: "selectedTabChange", indexFocused: "indexFocused", selectFocusedIndex: "selectFocusedIndex", configChange: "configChange", widgetEvent: "widgetEvent" }, providers: [providePraxisI18nConfig(PRAXIS_TABS_I18N_CONFIG)], viewQueries: [{ propertyName: "groupComponent", first: true, predicate: MatTabGroup, descendants: true }], usesOnChanges: true, ngImport: i0, template: `
|
|
4241
4318
|
<div
|
|
4242
4319
|
class="praxis-tabs-root"
|
|
4243
4320
|
[class.density-compact]="config?.appearance?.density === 'compact'"
|
|
@@ -4387,6 +4464,7 @@ class PraxisTabs {
|
|
|
4387
4464
|
<ng-container
|
|
4388
4465
|
[dynamicWidgetLoader]="resolveWidgetDefinition(w)"
|
|
4389
4466
|
[context]="context || {}"
|
|
4467
|
+
[autoWireOutputs]="true"
|
|
4390
4468
|
(widgetEvent)="emitWidgetEvent(linkEventPath(l.id, currentNavIndex()), $event)"
|
|
4391
4469
|
></ng-container>
|
|
4392
4470
|
</div>
|
|
@@ -4427,6 +4505,7 @@ class PraxisTabs {
|
|
|
4427
4505
|
(focusChange)="focusChange.emit($event)"
|
|
4428
4506
|
(selectedIndexChange)="onVisibleTabIndexChange($event)"
|
|
4429
4507
|
(selectedTabChange)="selectedTabChange.emit($event)"
|
|
4508
|
+
(click)="onGroupPointerClick($event)"
|
|
4430
4509
|
class="praxis-tabs-group"
|
|
4431
4510
|
>
|
|
4432
4511
|
@for (entry of visibleTabEntries(); track trackVisibleTab(i, entry); let i = $index) {
|
|
@@ -4493,6 +4572,7 @@ class PraxisTabs {
|
|
|
4493
4572
|
<ng-container
|
|
4494
4573
|
[dynamicWidgetLoader]="resolveWidgetDefinition(w)"
|
|
4495
4574
|
[context]="context || {}"
|
|
4575
|
+
[autoWireOutputs]="true"
|
|
4496
4576
|
(widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
|
|
4497
4577
|
></ng-container>
|
|
4498
4578
|
</div>
|
|
@@ -4708,6 +4788,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4708
4788
|
<ng-container
|
|
4709
4789
|
[dynamicWidgetLoader]="resolveWidgetDefinition(w)"
|
|
4710
4790
|
[context]="context || {}"
|
|
4791
|
+
[autoWireOutputs]="true"
|
|
4711
4792
|
(widgetEvent)="emitWidgetEvent(linkEventPath(l.id, currentNavIndex()), $event)"
|
|
4712
4793
|
></ng-container>
|
|
4713
4794
|
</div>
|
|
@@ -4748,6 +4829,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4748
4829
|
(focusChange)="focusChange.emit($event)"
|
|
4749
4830
|
(selectedIndexChange)="onVisibleTabIndexChange($event)"
|
|
4750
4831
|
(selectedTabChange)="selectedTabChange.emit($event)"
|
|
4832
|
+
(click)="onGroupPointerClick($event)"
|
|
4751
4833
|
class="praxis-tabs-group"
|
|
4752
4834
|
>
|
|
4753
4835
|
@for (entry of visibleTabEntries(); track trackVisibleTab(i, entry); let i = $index) {
|
|
@@ -4814,6 +4896,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4814
4896
|
<ng-container
|
|
4815
4897
|
[dynamicWidgetLoader]="resolveWidgetDefinition(w)"
|
|
4816
4898
|
[context]="context || {}"
|
|
4899
|
+
[autoWireOutputs]="true"
|
|
4817
4900
|
(widgetEvent)="emitWidgetEvent(tabEventPath(entry.tab.id, entry.index), $event)"
|
|
4818
4901
|
></ng-container>
|
|
4819
4902
|
</div>
|
|
@@ -4895,6 +4978,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.2.14", ngImpo
|
|
|
4895
4978
|
type: Output
|
|
4896
4979
|
}], widgetEvent: [{
|
|
4897
4980
|
type: Output
|
|
4981
|
+
}], groupComponent: [{
|
|
4982
|
+
type: ViewChild,
|
|
4983
|
+
args: [MatTabGroup]
|
|
4898
4984
|
}] } });
|
|
4899
4985
|
|
|
4900
4986
|
class PraxisTabsWidgetConfigEditor {
|
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@praxisui/tabs",
|
|
3
|
-
"version": "9.0.
|
|
3
|
+
"version": "9.0.57",
|
|
4
4
|
"description": "Configurable tabs (group and nav) for Praxis UI with metadata-driven content and runtime editor.",
|
|
5
5
|
"peerDependencies": {
|
|
6
6
|
"@angular/common": "^21.0.0",
|
|
7
7
|
"@angular/core": "^21.0.0",
|
|
8
8
|
"@angular/material": "^21.0.0",
|
|
9
9
|
"@angular/cdk": "^21.0.0",
|
|
10
|
-
"@praxisui/core": "^9.0.
|
|
11
|
-
"@praxisui/dynamic-fields": "^9.0.
|
|
12
|
-
"@praxisui/settings-panel": "^9.0.
|
|
10
|
+
"@praxisui/core": "^9.0.57",
|
|
11
|
+
"@praxisui/dynamic-fields": "^9.0.57",
|
|
12
|
+
"@praxisui/settings-panel": "^9.0.57",
|
|
13
13
|
"@angular/forms": "^21.0.0",
|
|
14
14
|
"@angular/router": "^21.0.0",
|
|
15
|
-
"@praxisui/ai": "^9.0.
|
|
15
|
+
"@praxisui/ai": "^9.0.57",
|
|
16
16
|
"rxjs": "~7.8.0"
|
|
17
17
|
},
|
|
18
18
|
"dependencies": {
|
package/types/praxisui-tabs.d.ts
CHANGED
|
@@ -206,6 +206,8 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
|
|
|
206
206
|
private groupLoaded;
|
|
207
207
|
private navLoaded;
|
|
208
208
|
private controlledSelectedIndex?;
|
|
209
|
+
private destroyed;
|
|
210
|
+
private groupComponent?;
|
|
209
211
|
private readonly destroy$;
|
|
210
212
|
private widgetDefinitionCache;
|
|
211
213
|
private readonly generatedContentForm;
|
|
@@ -227,6 +229,14 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
|
|
|
227
229
|
protected selectedVisibleTabIndex(): number;
|
|
228
230
|
protected effectiveContentForm(): FormGroup;
|
|
229
231
|
protected onVisibleTabIndexChange(index: number): void;
|
|
232
|
+
/**
|
|
233
|
+
* Material commits a pointer-selected tab during its next content-check cycle.
|
|
234
|
+
* A Tabs instance hosted by DynamicWidgetLoader can receive a composition
|
|
235
|
+
* reprojection in that same cycle, leaving Material's pending index uncommitted.
|
|
236
|
+
* Resolve the semantic tab identity at the container boundary so pointer
|
|
237
|
+
* selection remains deterministic without changing keyboard focus semantics.
|
|
238
|
+
*/
|
|
239
|
+
protected onGroupPointerClick(event: MouseEvent): void;
|
|
230
240
|
onNavClick(i: number): void;
|
|
231
241
|
onNavDrop(event: CdkDragDrop<any>): void;
|
|
232
242
|
protected onVisibleNavDrop(event: CdkDragDrop<any>): void;
|
|
@@ -271,6 +281,7 @@ declare class PraxisTabs implements OnInit, OnChanges, OnDestroy {
|
|
|
271
281
|
private storageKey;
|
|
272
282
|
private loadStoredConfigForCurrentIdentity;
|
|
273
283
|
private syncSelectionFromConfig;
|
|
284
|
+
private syncSelectionFromConfigChange;
|
|
274
285
|
private pruneLoadedIndexes;
|
|
275
286
|
private persistConfig;
|
|
276
287
|
private usesInputFirstConfig;
|