From b195417bb23a1e6961c264e3443145168832eda9 Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Wed, 25 Feb 2026 20:10:55 -0500 Subject: [PATCH 1/3] refactor(aria/tabs): Use template reference to link tabs and panels together --- src/aria/private/tabs/tabs.ts | 22 +----------- src/aria/tabs/tab-list.ts | 23 +++++-------- src/aria/tabs/tab-panel.ts | 5 +-- src/aria/tabs/tab.ts | 10 +++--- .../tabs-active-descendant-example.html | 22 ++++++------ .../tabs-disabled-focusable-example.html | 22 ++++++------ .../tabs-disabled-skipped-example.html | 22 ++++++------ .../tabs/disabled/tabs-disabled-example.html | 22 ++++++------ .../tabs-explicit-selection-example.html | 22 ++++++------ .../aria/tabs/rtl/tabs-rtl-example.html | 22 ++++++------ .../tabs-selection-follows-focus-example.html | 22 ++++++------ .../tabs-configurable-example.html | 34 +++++++++---------- .../tabs-configurable-example.ts | 3 +- .../tabs-vertical-example.html | 22 ++++++------ 14 files changed, 122 insertions(+), 151 deletions(-) diff --git a/src/aria/private/tabs/tabs.ts b/src/aria/private/tabs/tabs.ts index 044b3f68370a..541b5d99aaff 100644 --- a/src/aria/private/tabs/tabs.ts +++ b/src/aria/private/tabs/tabs.ts @@ -30,9 +30,6 @@ export interface TabInputs /** The remote tabpanel controlled by the tab. */ tabpanel: SignalLike; - - /** The remote tabpanel unique identifier. */ - value: SignalLike; } /** A tab in a tablist. */ @@ -43,9 +40,6 @@ export class TabPattern { /** The index of the tab. */ readonly index = computed(() => this.inputs.tablist().inputs.items().indexOf(this)); - /** The remote tabpanel unique identifier. */ - readonly value: SignalLike = () => this.inputs.value(); - /** Whether the tab is disabled. */ readonly disabled: SignalLike = () => this.inputs.disabled(); @@ -87,9 +81,6 @@ export interface TabPanelInputs extends LabelControlOptionalInputs { /** The tab that controls this tabpanel. */ tab: SignalLike; - - /** A local unique identifier for the tabpanel. */ - value: SignalLike; } /** A tabpanel associated with a tab. */ @@ -97,9 +88,6 @@ export class TabPanelPattern { /** A global unique identifier for the tabpanel. */ readonly id: SignalLike = () => this.inputs.id(); - /** A local unique identifier for the tabpanel. */ - readonly value: SignalLike = () => this.inputs.value(); - /** Controls label for this tabpanel. */ readonly labelManager: LabelControl; @@ -263,19 +251,11 @@ export class TabListPattern { } } - /** Opens the tab by given value. */ - open(value: string): boolean; - /** Opens the given tab or the current active tab. */ open(tab?: TabPattern): boolean; - - open(tab: TabPattern | string | undefined): boolean { + open(tab: TabPattern | undefined): boolean { tab ??= this.activeTab(); - if (typeof tab === 'string') { - tab = this.inputs.items().find(t => t.value() === tab); - } - if (tab === undefined) return false; const success = this.expansionBehavior.open(tab); diff --git a/src/aria/tabs/tab-list.ts b/src/aria/tabs/tab-list.ts index 7e9d4b12f413..54464073e0d3 100644 --- a/src/aria/tabs/tab-list.ts +++ b/src/aria/tabs/tab-list.ts @@ -104,7 +104,7 @@ export class TabList implements OnInit, OnDestroy { readonly selectionMode = input<'follow' | 'explicit'>('follow'); /** The current selected tab. */ - readonly selectedTab = model(); + readonly selectedTab = model(); /** Whether the tablist is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); @@ -128,19 +128,19 @@ export class TabList implements OnInit, OnDestroy { }); afterRenderEffect(() => { - const tab = this._pattern.selectedTab(); - if (tab) { - this.selectedTab.set(tab.value()); + const tabPattern = this._pattern.selectedTab(); + if (tabPattern) { + const tab = [...this._unorderedTabs()].find(tab => tab._pattern === tabPattern); + this.selectedTab.set(tab); } }); afterRenderEffect(() => { - const value = this.selectedTab(); - if (value) { + const tab = this.selectedTab(); + if (tab) { this._tabPatterns().forEach(tab => tab.expanded.set(false)); - const tab = this._tabPatterns().find(t => t.value() === value); - this._pattern.selectedTab.set(tab); - tab?.expanded.set(true); + this._pattern.selectedTab.set(tab._pattern); + tab._pattern.expanded.set(true); } }); } @@ -166,9 +166,4 @@ export class TabList implements OnInit, OnDestroy { this._unorderedTabs().delete(child); this._unorderedTabs.set(new Set(this._unorderedTabs())); } - - /** Opens the tab panel with the specified value. */ - open(value: string): boolean { - return this._pattern.open(value); - } } diff --git a/src/aria/tabs/tab-panel.ts b/src/aria/tabs/tab-panel.ts index 31f594e7443c..d5d7b6558922 100644 --- a/src/aria/tabs/tab-panel.ts +++ b/src/aria/tabs/tab-panel.ts @@ -74,12 +74,9 @@ export class TabPanel implements OnInit, OnDestroy { /** The Tab UIPattern associated with the tabpanel */ private readonly _tabPattern = computed(() => - this._tabs._tabPatterns()?.find(tab => tab.value() === this.value()), + this._tabs._tabPatterns()?.find(tab => tab.inputs.tabpanel() === this._pattern), ); - /** A local unique identifier for the tabpanel. */ - readonly value = input.required(); - /** Whether the tab panel is visible. */ readonly visible = computed(() => !this._pattern.hidden()); diff --git a/src/aria/tabs/tab.ts b/src/aria/tabs/tab.ts index fcc6c07a5763..35ec4878fc0a 100644 --- a/src/aria/tabs/tab.ts +++ b/src/aria/tabs/tab.ts @@ -21,6 +21,7 @@ import { import {TabPattern} from '../private'; import {TabList} from './tab-list'; import {HasElement, TABS} from './utils'; +import {TabPanel} from './tab-panel'; /** * A selectable tab in a TabList. @@ -71,16 +72,13 @@ export class Tab implements HasElement, OnInit, OnDestroy { private readonly _tablistPattern = computed(() => this._tabList._pattern); /** The TabPanel UIPattern associated with the tab */ - private readonly _tabpanelPattern = computed(() => - this._tabs._unorderedTabpanelPatterns().find(tabpanel => tabpanel.value() === this.value()), - ); + private readonly _tabpanelPattern = computed(() => this.panel()?._pattern); + + readonly panel = input(); /** Whether a tab is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); - /** The remote tabpanel unique identifier. */ - readonly value = input.required(); - /** Whether the tab is active. */ readonly active = computed(() => this._pattern.active()); diff --git a/src/components-examples/aria/tabs/active-descendant/tabs-active-descendant-example.html b/src/components-examples/aria/tabs/active-descendant/tabs-active-descendant-example.html index 320cb123ad4c..e32ad7d3eb34 100644 --- a/src/components-examples/aria/tabs/active-descendant/tabs-active-descendant-example.html +++ b/src/components-examples/aria/tabs/active-descendant/tabs-active-descendant-example.html @@ -1,29 +1,29 @@
-
-
Tab 1
-
Tab 2
-
Tab 3
-
Tab 4
-
Tab 5
+
+
Tab 1
+
Tab 2
+
Tab 3
+
Tab 4
+
Tab 5
-
+
Panel 1
-
+
Panel 2
-
+
Panel 3
-
+
Panel 4
-
+
Panel 5
diff --git a/src/components-examples/aria/tabs/disabled-focusable/tabs-disabled-focusable-example.html b/src/components-examples/aria/tabs/disabled-focusable/tabs-disabled-focusable-example.html index cd2192fcf3cd..9c2ce53da99b 100644 --- a/src/components-examples/aria/tabs/disabled-focusable/tabs-disabled-focusable-example.html +++ b/src/components-examples/aria/tabs/disabled-focusable/tabs-disabled-focusable-example.html @@ -1,29 +1,29 @@
-
-
Tab 1
-
Tab 2
-
Tab 3
-
Tab 4
-
Tab 5
+
+
Tab 1
+
Tab 2
+
Tab 3
+
Tab 4
+
Tab 5
-
+
Panel 1
-
+
Panel 2
-
+
Panel 3
-
+
Panel 4
-
+
Panel 5
diff --git a/src/components-examples/aria/tabs/disabled-skipped/tabs-disabled-skipped-example.html b/src/components-examples/aria/tabs/disabled-skipped/tabs-disabled-skipped-example.html index e74ad12ae53b..8e541ffcd252 100644 --- a/src/components-examples/aria/tabs/disabled-skipped/tabs-disabled-skipped-example.html +++ b/src/components-examples/aria/tabs/disabled-skipped/tabs-disabled-skipped-example.html @@ -1,29 +1,29 @@
-
-
Tab 1
-
Tab 2
-
Tab 3
-
Tab 4
-
Tab 5
+
+
Tab 1
+
Tab 2
+
Tab 3
+
Tab 4
+
Tab 5
-
+
Panel 1
-
+
Panel 2
-
+
Panel 3
-
+
Panel 4
-
+
Panel 5
diff --git a/src/components-examples/aria/tabs/disabled/tabs-disabled-example.html b/src/components-examples/aria/tabs/disabled/tabs-disabled-example.html index d356a4f7f564..cccc8486e983 100644 --- a/src/components-examples/aria/tabs/disabled/tabs-disabled-example.html +++ b/src/components-examples/aria/tabs/disabled/tabs-disabled-example.html @@ -1,29 +1,29 @@
-
-
Tab 1
-
Tab 2
-
Tab 3
-
Tab 4
-
Tab 5
+
+
Tab 1
+
Tab 2
+
Tab 3
+
Tab 4
+
Tab 5
-
+
Panel 1
-
+
Panel 2
-
+
Panel 3
-
+
Panel 4
-
+
Panel 5
diff --git a/src/components-examples/aria/tabs/explicit-selection/tabs-explicit-selection-example.html b/src/components-examples/aria/tabs/explicit-selection/tabs-explicit-selection-example.html index 6ee281a4f16a..3f0db2fe4906 100644 --- a/src/components-examples/aria/tabs/explicit-selection/tabs-explicit-selection-example.html +++ b/src/components-examples/aria/tabs/explicit-selection/tabs-explicit-selection-example.html @@ -1,29 +1,29 @@
-
-
Tab 1
-
Tab 2
-
Tab 3
-
Tab 4
-
Tab 5
+
+
Tab 1
+
Tab 2
+
Tab 3
+
Tab 4
+
Tab 5
-
+
Panel 1
-
+
Panel 2
-
+
Panel 3
-
+
Panel 4
-
+
Panel 5
diff --git a/src/components-examples/aria/tabs/rtl/tabs-rtl-example.html b/src/components-examples/aria/tabs/rtl/tabs-rtl-example.html index 2c0f2cad2ee4..2987242dee33 100644 --- a/src/components-examples/aria/tabs/rtl/tabs-rtl-example.html +++ b/src/components-examples/aria/tabs/rtl/tabs-rtl-example.html @@ -1,29 +1,29 @@
-
-
Tab 1
-
Tab 2
-
Tab 3
-
Tab 4
-
Tab 5
+
+
Tab 1
+
Tab 2
+
Tab 3
+
Tab 4
+
Tab 5
-
+
Panel 1
-
+
Panel 2
-
+
Panel 3
-
+
Panel 4
-
+
Panel 5
diff --git a/src/components-examples/aria/tabs/selection-follows-focus/tabs-selection-follows-focus-example.html b/src/components-examples/aria/tabs/selection-follows-focus/tabs-selection-follows-focus-example.html index b92774de566b..7cedc1c89e43 100644 --- a/src/components-examples/aria/tabs/selection-follows-focus/tabs-selection-follows-focus-example.html +++ b/src/components-examples/aria/tabs/selection-follows-focus/tabs-selection-follows-focus-example.html @@ -1,29 +1,29 @@
-
-
Tab 1
-
Tab 2
-
Tab 3
-
Tab 4
-
Tab 5
+
+
Tab 1
+
Tab 2
+
Tab 3
+
Tab 4
+
Tab 5
-
+
Panel 1
-
+
Panel 2
-
+
Panel 3
-
+
Panel 4
-
+
Panel 5
diff --git a/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.html b/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.html index 74effd360596..d3aeccb3b6b0 100644 --- a/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.html +++ b/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.html @@ -29,12 +29,12 @@ Tab selection - - Tab 1 - Tab 2 - Tab 3 - Tab 4 - Tab 5 + + Tab 1 + Tab 2 + Tab 3 + Tab 4 + Tab 5
@@ -48,32 +48,32 @@ [orientation]="orientation" [focusMode]="focusMode" selectionMode="explicit" - [selectedTab]="tabSelection" + [selectedTab]="tabs()[tabSelectionIndex]" > -
Tab 1
-
Tab 2
-
Tab 3
-
Tab 4
-
Tab 5
+
Tab 1
+
Tab 2
+
Tab 3
+
Tab 4
+
Tab 5
-
+
Panel 1
-
+
Panel 2
-
+
Panel 3
-
+
Panel 4
-
+
Panel 5
diff --git a/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.ts b/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.ts index bf80136ee106..c127a2851b36 100644 --- a/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.ts +++ b/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.ts @@ -4,6 +4,7 @@ import {Tabs, TabList, Tab, TabPanel, TabContent} from '@angular/aria/tabs'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatSelectModule} from '@angular/material/select'; import {FormControl, ReactiveFormsModule} from '@angular/forms'; +import {computed} from '@angular/aria/private'; /** @title Configurable Tabs. */ @Component({ @@ -26,7 +27,7 @@ export class TabsConfigurableExample { orientation: 'vertical' | 'horizontal' = 'horizontal'; focusMode: 'roving' | 'activedescendant' = 'roving'; selectionMode: 'explicit' | 'follow' = 'follow'; - tabSelection = 'tab-1'; + tabSelectionIndex = 0; wrap = new FormControl(true, {nonNullable: true}); disabled = new FormControl(false, {nonNullable: true}); diff --git a/src/components-examples/aria/tabs/vertical-orientation/tabs-vertical-example.html b/src/components-examples/aria/tabs/vertical-orientation/tabs-vertical-example.html index 132f776c106c..be176ead335c 100644 --- a/src/components-examples/aria/tabs/vertical-orientation/tabs-vertical-example.html +++ b/src/components-examples/aria/tabs/vertical-orientation/tabs-vertical-example.html @@ -1,29 +1,29 @@
-
-
Tab 1
-
Tab 2
-
Tab 3
-
Tab 4
-
Tab 5
+
+
Tab 1
+
Tab 2
+
Tab 3
+
Tab 4
+
Tab 5
-
+
Panel 1
-
+
Panel 2
-
+
Panel 3
-
+
Panel 4
-
+
Panel 5
From 53a7ae92580d69a0fbd41a356d8487ae39153272 Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Wed, 25 Feb 2026 21:19:57 -0500 Subject: [PATCH 2/3] cleanup --- src/aria/tabs/tab-list.ts | 49 +++++-------------- src/aria/tabs/tab-panel.ts | 33 ++----------- src/aria/tabs/tab.ts | 32 ++++-------- src/aria/tabs/tabs.ts | 43 +--------------- src/aria/tabs/utils.ts | 26 ---------- .../tabs-configurable-example.ts | 1 - 6 files changed, 27 insertions(+), 157 deletions(-) delete mode 100644 src/aria/tabs/utils.ts diff --git a/src/aria/tabs/tab-list.ts b/src/aria/tabs/tab-list.ts index 54464073e0d3..a101437c3ff6 100644 --- a/src/aria/tabs/tab-list.ts +++ b/src/aria/tabs/tab-list.ts @@ -12,17 +12,15 @@ import { computed, Directive, ElementRef, + contentChildren, inject, input, model, signal, afterRenderEffect, - OnInit, - OnDestroy, } from '@angular/core'; -import {TabListPattern, TabPattern} from '../private'; -import {sortDirectives, TABS} from './utils'; -import type {Tab} from './tab'; +import {TabListPattern} from '../private'; +import {Tab} from './tab'; /** * A TabList container. @@ -56,27 +54,22 @@ import type {Tab} from './tab'; '(focusin)': '_onFocus()', }, }) -export class TabList implements OnInit, OnDestroy { +export class TabList { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); /** A reference to the host element. */ readonly element = this._elementRef.nativeElement as HTMLElement; - /** The parent Tabs. */ - private readonly _tabs = inject(TABS); + /** The Tabs nested inside this group. */ + private readonly _tabs = contentChildren(Tab, {descendants: true}); - /** The Tabs nested inside of the TabList. */ - private readonly _unorderedTabs = signal(new Set()); + /** The Tab UIPatterns of the child Tabs. */ + readonly _tabPatterns = computed(() => this._tabs().map(tab => tab._pattern)); /** Text direction. */ readonly textDirection = inject(Directionality).valueSignal; - /** The Tab UIPatterns of the child Tabs. */ - readonly _tabPatterns = computed(() => - [...this._unorderedTabs()].sort(sortDirectives).map(tab => tab._pattern), - ); - /** Whether the tablist is vertically or horizontally oriented. */ readonly orientation = input<'vertical' | 'horizontal'>('horizontal'); @@ -103,12 +96,12 @@ export class TabList implements OnInit, OnDestroy { */ readonly selectionMode = input<'follow' | 'explicit'>('follow'); - /** The current selected tab. */ - readonly selectedTab = model(); - /** Whether the tablist is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); + /** The current selected tab. */ + readonly selectedTab = model(); + /** The TabList UIPattern. */ readonly _pattern: TabListPattern = new TabListPattern({ ...this, @@ -130,7 +123,7 @@ export class TabList implements OnInit, OnDestroy { afterRenderEffect(() => { const tabPattern = this._pattern.selectedTab(); if (tabPattern) { - const tab = [...this._unorderedTabs()].find(tab => tab._pattern === tabPattern); + const tab = this._tabs().find(tab => tab._pattern === tabPattern); this.selectedTab.set(tab); } }); @@ -148,22 +141,4 @@ export class TabList implements OnInit, OnDestroy { _onFocus() { this._hasFocused.set(true); } - - ngOnInit() { - this._tabs._register(this); - } - - ngOnDestroy() { - this._tabs._unregister(this); - } - - _register(child: Tab) { - this._unorderedTabs().add(child); - this._unorderedTabs.set(new Set(this._unorderedTabs())); - } - - _unregister(child: Tab) { - this._unorderedTabs().delete(child); - this._unorderedTabs.set(new Set(this._unorderedTabs())); - } } diff --git a/src/aria/tabs/tab-panel.ts b/src/aria/tabs/tab-panel.ts index d5d7b6558922..adc16b0f16c8 100644 --- a/src/aria/tabs/tab-panel.ts +++ b/src/aria/tabs/tab-panel.ts @@ -7,18 +7,8 @@ */ import {_IdGenerator} from '@angular/cdk/a11y'; -import { - computed, - Directive, - ElementRef, - inject, - input, - afterRenderEffect, - OnInit, - OnDestroy, -} from '@angular/core'; -import {TabPanelPattern, DeferredContentAware} from '../private'; -import {TABS} from './utils'; +import {computed, Directive, ElementRef, inject, input, afterRenderEffect} from '@angular/core'; +import {TabPattern, TabPanelPattern, DeferredContentAware} from '../private'; /** * A TabPanel container for the resources of layered content associated with a tab. @@ -56,7 +46,7 @@ import {TABS} from './utils'; }, ], }) -export class TabPanel implements OnInit, OnDestroy { +export class TabPanel { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); @@ -66,16 +56,11 @@ export class TabPanel implements OnInit, OnDestroy { /** The DeferredContentAware host directive. */ private readonly _deferredContentAware = inject(DeferredContentAware); - /** The parent Tabs. */ - private readonly _tabs = inject(TABS); - /** A global unique identifier for the tab. */ readonly id = input(inject(_IdGenerator).getId('ng-tabpanel-', true)); /** The Tab UIPattern associated with the tabpanel */ - private readonly _tabPattern = computed(() => - this._tabs._tabPatterns()?.find(tab => tab.inputs.tabpanel() === this._pattern), - ); + _tabPattern?: TabPattern; /** Whether the tab panel is visible. */ readonly visible = computed(() => !this._pattern.hidden()); @@ -83,18 +68,10 @@ export class TabPanel implements OnInit, OnDestroy { /** The TabPanel UIPattern. */ readonly _pattern: TabPanelPattern = new TabPanelPattern({ ...this, - tab: this._tabPattern, + tab: () => this._tabPattern, }); constructor() { afterRenderEffect(() => this._deferredContentAware.contentVisible.set(this.visible())); } - - ngOnInit() { - this._tabs._register(this); - } - - ngOnDestroy() { - this._tabs._unregister(this); - } } diff --git a/src/aria/tabs/tab.ts b/src/aria/tabs/tab.ts index 35ec4878fc0a..3b29ba8f46c6 100644 --- a/src/aria/tabs/tab.ts +++ b/src/aria/tabs/tab.ts @@ -16,11 +16,9 @@ import { input, signal, OnInit, - OnDestroy, } from '@angular/core'; import {TabPattern} from '../private'; import {TabList} from './tab-list'; -import {HasElement, TABS} from './utils'; import {TabPanel} from './tab-panel'; /** @@ -52,29 +50,21 @@ import {TabPanel} from './tab-panel'; '[attr.aria-controls]': '_pattern.controls()', }, }) -export class Tab implements HasElement, OnInit, OnDestroy { +export class Tab implements OnInit { /** A reference to the host element. */ private readonly _elementRef = inject(ElementRef); /** A reference to the host element. */ readonly element = this._elementRef.nativeElement as HTMLElement; - /** The parent Tabs. */ - private readonly _tabs = inject(TABS); - /** The parent TabList. */ private readonly _tabList = inject(TabList); /** A unique identifier for the widget. */ readonly id = input(inject(_IdGenerator).getId('ng-tab-', true)); - /** The parent TabList UIPattern. */ - private readonly _tablistPattern = computed(() => this._tabList._pattern); - - /** The TabPanel UIPattern associated with the tab */ - private readonly _tabpanelPattern = computed(() => this.panel()?._pattern); - - readonly panel = input(); + /** The panel associated with this tab. */ + readonly panel = input.required(); /** Whether a tab is disabled. */ readonly disabled = input(false, {transform: booleanAttribute}); @@ -88,22 +78,18 @@ export class Tab implements HasElement, OnInit, OnDestroy { /** The Tab UIPattern. */ readonly _pattern: TabPattern = new TabPattern({ ...this, - tablist: this._tablistPattern, - tabpanel: this._tabpanelPattern, + tablist: () => this._tabList._pattern, + tabpanel: () => this.panel()._pattern, expanded: signal(false), element: () => this.element, }); - /** Opens this tab panel. */ - open() { - this._pattern.open(); - } - ngOnInit() { - this._tabList._register(this); + this.panel()._tabPattern = this._pattern; } - ngOnDestroy() { - this._tabList._unregister(this); + /** Opens this tab panel. */ + open() { + this._pattern.open(); } } diff --git a/src/aria/tabs/tabs.ts b/src/aria/tabs/tabs.ts index 0c8d616f411e..731bfa9789e3 100644 --- a/src/aria/tabs/tabs.ts +++ b/src/aria/tabs/tabs.ts @@ -6,11 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import {computed, Directive, ElementRef, inject, signal} from '@angular/core'; -import {TabList} from './tab-list'; -import {TabPanel} from './tab-panel'; -import {TABS} from './utils'; -import {TabPanelPattern, TabPattern} from '../private'; +import {Directive, ElementRef, inject} from '@angular/core'; /** * A Tabs container. @@ -46,7 +42,6 @@ import {TabPanelPattern, TabPattern} from '../private'; @Directive({ selector: '[ngTabs]', exportAs: 'ngTabs', - providers: [{provide: TABS, useExisting: Tabs}], }) export class Tabs { /** A reference to the host element. */ @@ -54,40 +49,4 @@ export class Tabs { /** A reference to the host element. */ readonly element = this._elementRef.nativeElement as HTMLElement; - - /** The TabList nested inside of the container. */ - private readonly _tablist = signal(undefined); - - /** The TabPanels nested inside of the container. */ - private readonly _unorderedPanels = signal(new Set()); - - /** The Tab UIPattern of the child Tabs. */ - readonly _tabPatterns = computed(() => this._tablist()?._tabPatterns()); - - /** The TabPanel UIPattern of the child TabPanels. */ - readonly _unorderedTabpanelPatterns = computed(() => - [...this._unorderedPanels()].map(tabpanel => tabpanel._pattern), - ); - - _register(child: TabList | TabPanel) { - if (child instanceof TabList) { - this._tablist.set(child); - } - - if (child instanceof TabPanel) { - this._unorderedPanels().add(child); - this._unorderedPanels.set(new Set(this._unorderedPanels())); - } - } - - _unregister(child: TabList | TabPanel) { - if (child instanceof TabList) { - this._tablist.set(undefined); - } - - if (child instanceof TabPanel) { - this._unorderedPanels().delete(child); - this._unorderedPanels.set(new Set(this._unorderedPanels())); - } - } } diff --git a/src/aria/tabs/utils.ts b/src/aria/tabs/utils.ts deleted file mode 100644 index 8a00da60c027..000000000000 --- a/src/aria/tabs/utils.ts +++ /dev/null @@ -1,26 +0,0 @@ -/** - * @license - * Copyright Google LLC All Rights Reserved. - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.dev/license - */ - -import {InjectionToken} from '@angular/core'; -import type {Tabs} from './tabs'; - -/** Token used to expose the `Tabs` directive to child directives. */ -export const TABS = new InjectionToken('TABS'); - -export interface HasElement { - element: HTMLElement; -} - -/** - * Sort directives by their document order. - */ -export function sortDirectives(a: HasElement, b: HasElement) { - return (a.element.compareDocumentPosition(b.element) & Node.DOCUMENT_POSITION_PRECEDING) > 0 - ? 1 - : -1; -} diff --git a/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.ts b/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.ts index c127a2851b36..f25d978afe8e 100644 --- a/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.ts +++ b/src/components-examples/aria/tabs/tabs-configurable/tabs-configurable-example.ts @@ -4,7 +4,6 @@ import {Tabs, TabList, Tab, TabPanel, TabContent} from '@angular/aria/tabs'; import {MatFormFieldModule} from '@angular/material/form-field'; import {MatSelectModule} from '@angular/material/select'; import {FormControl, ReactiveFormsModule} from '@angular/forms'; -import {computed} from '@angular/aria/private'; /** @title Configurable Tabs. */ @Component({ From 9f311f803e10976526db5c12d0acfc36f514f9da Mon Sep 17 00:00:00 2001 From: Andrey Dolgachev Date: Wed, 25 Feb 2026 21:54:54 -0500 Subject: [PATCH 3/3] fixing tests --- src/aria/private/tabs/tabs.spec.ts | 10 +---- src/aria/tabs/tabs.spec.ts | 60 +++++++++++++++--------------- 2 files changed, 31 insertions(+), 39 deletions(-) diff --git a/src/aria/private/tabs/tabs.spec.ts b/src/aria/private/tabs/tabs.spec.ts index 71129712e423..9e4fcb83b5bf 100644 --- a/src/aria/private/tabs/tabs.spec.ts +++ b/src/aria/private/tabs/tabs.spec.ts @@ -76,7 +76,6 @@ describe('Tabs Pattern', () => { id: signal('tab-1-id'), element: signal(createTabElement()), disabled: signal(false), - value: signal('tab-1'), expanded: signal(false), }, { @@ -85,7 +84,6 @@ describe('Tabs Pattern', () => { id: signal('tab-2-id'), element: signal(createTabElement()), disabled: signal(false), - value: signal('tab-2'), expanded: signal(false), }, { @@ -94,7 +92,6 @@ describe('Tabs Pattern', () => { id: signal('tab-3-id'), element: signal(createTabElement()), disabled: signal(false), - value: signal('tab-3'), expanded: signal(false), }, ]; @@ -109,17 +106,14 @@ describe('Tabs Pattern', () => { { id: signal('tabpanel-1-id'), tab: signal(undefined), - value: signal('tab-1'), }, { id: signal('tabpanel-2-id'), tab: signal(undefined), - value: signal('tab-2'), }, { id: signal('tabpanel-3-id'), tab: signal(undefined), - value: signal('tab-3'), }, ]; tabPanelPatterns = [ @@ -143,8 +137,8 @@ describe('Tabs Pattern', () => { describe('#open', () => { it('should open a tab with value', () => { expect(tabListPattern.selectedTab()).toBeUndefined(); - tabListPattern.open('tab-1'); - expect(tabListPattern.selectedTab()!.value()).toBe('tab-1'); + tabListPattern.open(tabPatterns[0]); + expect(tabListPattern.selectedTab()!).toBe(tabPatterns[0]); }); it('should open a tab with tab pattern instance', () => { diff --git a/src/aria/tabs/tabs.spec.ts b/src/aria/tabs/tabs.spec.ts index 8ac9ed40301d..d20ffddaf679 100644 --- a/src/aria/tabs/tabs.spec.ts +++ b/src/aria/tabs/tabs.spec.ts @@ -1,4 +1,4 @@ -import {Component, DebugElement, signal} from '@angular/core'; +import {Component, DebugElement, signal, viewChildren} from '@angular/core'; import {ComponentFixture, TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {Direction} from '@angular/cdk/bidi'; @@ -17,7 +17,6 @@ interface ModifierKeys { } interface TestTabDefinition { - value: string; label: string; content: string; disabled?: boolean; @@ -132,9 +131,9 @@ describe('Tabs', () => { setupTestTabs(); updateTabs({ initialTabs: [ - {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, - {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, - {value: 'tab3', label: 'Tab 3', content: 'Content 3'}, + {label: 'Tab 1', content: 'Content 1'}, + {label: 'Tab 2', content: 'Content 2', disabled: true}, + {label: 'Tab 3', content: 'Content 3'}, ], }); }); @@ -265,9 +264,9 @@ describe('Tabs', () => { setupTestTabs({textDirection: 'ltr'}); updateTabs({ initialTabs: [ - {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, - {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, - {value: 'tab3', label: 'Tab 3', content: 'Content 3'}, + {label: 'Tab 1', content: 'Content 1'}, + {label: 'Tab 2', content: 'Content 2', disabled: true}, + {label: 'Tab 3', content: 'Content 3'}, ], focusMode, selectedTab: 'tab1', @@ -356,9 +355,9 @@ describe('Tabs', () => { setupTestTabs({textDirection: 'rtl'}); updateTabs({ initialTabs: [ - {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, - {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, - {value: 'tab3', label: 'Tab 3', content: 'Content 3'}, + {label: 'Tab 1', content: 'Content 1'}, + {label: 'Tab 2', content: 'Content 2', disabled: true}, + {label: 'Tab 3', content: 'Content 3'}, ], focusMode, selectedTab: 'tab1', @@ -401,9 +400,9 @@ describe('Tabs', () => { updateTabs({ orientation: 'vertical', initialTabs: [ - {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, - {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, - {value: 'tab3', label: 'Tab 3', content: 'Content 3'}, + {label: 'Tab 1', content: 'Content 1'}, + {label: 'Tab 2', content: 'Content 2', disabled: true}, + {label: 'Tab 3', content: 'Content 3'}, ], focusMode, selectedTab: 'tab1', @@ -487,9 +486,9 @@ describe('Tabs', () => { setupTestTabs(); updateTabs({ initialTabs: [ - {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, - {value: 'tab2', label: 'Tab 2', content: 'Content 2'}, - {value: 'tab3', label: 'Tab 3', content: 'Content 3'}, + {label: 'Tab 1', content: 'Content 1'}, + {label: 'Tab 2', content: 'Content 2'}, + {label: 'Tab 3', content: 'Content 3'}, ], selectedTab: 'tab1', }); @@ -638,9 +637,9 @@ describe('Tabs', () => { it('should not select a disabled tab via click', () => { updateTabs({ initialTabs: [ - {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, - {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, - {value: 'tab3', label: 'Tab 3', content: 'Content 3'}, + {label: 'Tab 1', content: 'Content 1'}, + {label: 'Tab 2', content: 'Content 2', disabled: true}, + {label: 'Tab 3', content: 'Content 3'}, ], selectedTab: 'tab1', }); @@ -653,9 +652,9 @@ describe('Tabs', () => { it('should not select a disabled tab via keyboard', () => { updateTabs({ initialTabs: [ - {value: 'tab1', label: 'Tab 1', content: 'Content 1'}, - {value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: true}, - {value: 'tab3', label: 'Tab 3', content: 'Content 3'}, + {label: 'Tab 1', content: 'Content 1'}, + {label: 'Tab 2', content: 'Content 2', disabled: true}, + {label: 'Tab 3', content: 'Content 3'}, ], selectedTab: 'tab1', selectionMode: 'explicit', @@ -720,13 +719,13 @@ describe('Tabs', () => { [softDisabled]="softDisabled()" [focusMode]="focusMode()" [selectionMode]="selectionMode()"> - @for (tabDef of tabsData(); track tabDef.value) { -
  • {{ tabDef.label }}
  • + @for (tabDef of tabsData(); track tabDef.label) { +
  • {{ tabDef.label }}
  • } - @for (tabDef of tabsData(); track tabDef.value) { -
    + @for (tabDef of tabsData(); track tabDef.label) { +
    {{ tabDef.content }}
    } @@ -737,30 +736,29 @@ describe('Tabs', () => { class TestTabsComponent { tabsData = signal([ { - value: 'tab1', label: 'Tab 1', content: 'Content 1', disabled: false, }, { - value: 'tab2', label: 'Tab 2', content: 'Content 2', disabled: false, }, { - value: 'tab3', label: 'Tab 3', content: 'Content 3', disabled: true, }, ]); - selectedTab = signal(undefined); + selectedTab = signal(undefined); orientation = signal<'horizontal' | 'vertical'>('horizontal'); disabled = signal(false); wrap = signal(true); softDisabled = signal(true); focusMode = signal<'roving' | 'activedescendant'>('roving'); selectionMode = signal<'follow' | 'explicit'>('follow'); + + tabPanels = viewChildren(TabPanel); }