Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 2 additions & 8 deletions src/aria/private/tabs/tabs.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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),
},
{
Expand All @@ -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),
},
{
Expand All @@ -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),
},
];
Expand All @@ -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 = [
Expand All @@ -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', () => {
Expand Down
22 changes: 1 addition & 21 deletions src/aria/private/tabs/tabs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,6 @@ export interface TabInputs

/** The remote tabpanel controlled by the tab. */
tabpanel: SignalLike<TabPanelPattern | undefined>;

/** The remote tabpanel unique identifier. */
value: SignalLike<string>;
}

/** A tab in a tablist. */
Expand All @@ -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<string> = () => this.inputs.value();

/** Whether the tab is disabled. */
readonly disabled: SignalLike<boolean> = () => this.inputs.disabled();

Expand Down Expand Up @@ -87,19 +81,13 @@ export interface TabPanelInputs extends LabelControlOptionalInputs {

/** The tab that controls this tabpanel. */
tab: SignalLike<TabPattern | undefined>;

/** A local unique identifier for the tabpanel. */
value: SignalLike<string>;
}

/** A tabpanel associated with a tab. */
export class TabPanelPattern {
/** A global unique identifier for the tabpanel. */
readonly id: SignalLike<string> = () => this.inputs.id();

/** A local unique identifier for the tabpanel. */
readonly value: SignalLike<string> = () => this.inputs.value();

/** Controls label for this tabpanel. */
readonly labelManager: LabelControl;

Expand Down Expand Up @@ -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);
Expand Down
68 changes: 19 additions & 49 deletions src/aria/tabs/tab-list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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<Tab>());
/** 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<TabPattern[]>(() =>
[...this._unorderedTabs()].sort(sortDirectives).map(tab => tab._pattern),
);

/** Whether the tablist is vertically or horizontally oriented. */
readonly orientation = input<'vertical' | 'horizontal'>('horizontal');

Expand All @@ -103,12 +96,12 @@ export class TabList implements OnInit, OnDestroy {
*/
readonly selectionMode = input<'follow' | 'explicit'>('follow');

/** The current selected tab. */
readonly selectedTab = model<string | undefined>();

/** Whether the tablist is disabled. */
readonly disabled = input(false, {transform: booleanAttribute});

/** The current selected tab. */
readonly selectedTab = model<Tab | undefined>();

/** The TabList UIPattern. */
readonly _pattern: TabListPattern = new TabListPattern({
...this,
Expand All @@ -128,47 +121,24 @@ 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._tabs().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);
}
});
}

_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()));
}

/** Opens the tab panel with the specified value. */
open(value: string): boolean {
return this._pattern.open(value);
}
}
36 changes: 5 additions & 31 deletions src/aria/tabs/tab-panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);

Expand All @@ -66,38 +56,22 @@ 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.value() === this.value()),
);

/** A local unique identifier for the tabpanel. */
readonly value = input.required<string>();
_tabPattern?: TabPattern;

/** Whether the tab panel is visible. */
readonly visible = computed(() => !this._pattern.hidden());

/** 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);
}
}
36 changes: 10 additions & 26 deletions src/aria/tabs/tab.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,10 @@ 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';

/**
* A selectable tab in a TabList.
Expand Down Expand Up @@ -51,36 +50,25 @@ import {HasElement, TABS} from './utils';
'[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._tabs._unorderedTabpanelPatterns().find(tabpanel => tabpanel.value() === this.value()),
);
/** The panel associated with this tab. */
readonly panel = input.required<TabPanel>();

/** Whether a tab is disabled. */
readonly disabled = input(false, {transform: booleanAttribute});

/** The remote tabpanel unique identifier. */
readonly value = input.required<string>();

/** Whether the tab is active. */
readonly active = computed(() => this._pattern.active());

Expand All @@ -90,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();
}
}
Loading
Loading