diff --git a/src/cdk-experimental/tabs/tabs.ts b/src/cdk-experimental/tabs/tabs.ts index bf17a9882732..68768319462b 100644 --- a/src/cdk-experimental/tabs/tabs.ts +++ b/src/cdk-experimental/tabs/tabs.ts @@ -34,15 +34,20 @@ import {TabListPattern, TabPanelPattern, TabPattern} from '../ui-patterns'; * ```html *
* * - *
Tab content 1
- *
Tab content 2
- *
Tab content 3
- *
+ *
+ * Tab content 1 + *
+ *
+ * Tab content 2 + *
+ *
+ * Tab content 3 + *
* ``` */ @Directive({ diff --git a/src/cdk-experimental/ui-patterns/behaviors/expansion/BUILD.bazel b/src/cdk-experimental/ui-patterns/behaviors/expansion/BUILD.bazel new file mode 100644 index 000000000000..1e72caacfb44 --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/expansion/BUILD.bazel @@ -0,0 +1,31 @@ +load("//tools:defaults.bzl", "ng_web_test_suite", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "expansion", + srcs = [ + "expansion.ts", + ], + deps = [ + "//:node_modules/@angular/core", + "//src/cdk-experimental/ui-patterns/behaviors/signal-like", + ], +) + +ts_project( + name = "unit_test_sources", + testonly = True, + srcs = [ + "expansion.spec.ts", + ], + deps = [ + ":expansion", + "//:node_modules/@angular/core", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_test_sources"], +) diff --git a/src/cdk-experimental/ui-patterns/behaviors/expansion/expansion.spec.ts b/src/cdk-experimental/ui-patterns/behaviors/expansion/expansion.spec.ts new file mode 100644 index 000000000000..3248fb42c169 --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/expansion/expansion.spec.ts @@ -0,0 +1,46 @@ +/** + * @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 {signal, WritableSignal} from '@angular/core'; +import {ExpansionControl, ExpansionPanel} from './expansion'; + +describe('Expansion', () => { + let testExpansionControl: ExpansionControl; + let panelVisibility: WritableSignal; + let testExpansionPanel: ExpansionPanel; + + beforeEach(() => { + let expansionControlRef = signal(undefined); + let expansionPanelRef = signal(undefined); + panelVisibility = signal(false); + testExpansionControl = new ExpansionControl({ + visible: panelVisibility, + expansionPanel: expansionPanelRef, + }); + testExpansionPanel = new ExpansionPanel({ + id: () => 'test-panel', + expansionControl: expansionControlRef, + }); + expansionControlRef.set(testExpansionControl); + expansionPanelRef.set(testExpansionPanel); + }); + + it('sets a panel hidden to true by setting a control to invisible.', () => { + panelVisibility.set(false); + expect(testExpansionPanel.hidden()).toBeTrue(); + }); + + it('sets a panel hidden to false by setting a control to visible.', () => { + panelVisibility.set(true); + expect(testExpansionPanel.hidden()).toBeFalse(); + }); + + it('gets a controlled panel id from ExpansionControl.', () => { + expect(testExpansionControl.controls()).toBe('test-panel'); + }); +}); diff --git a/src/cdk-experimental/ui-patterns/behaviors/expansion/expansion.ts b/src/cdk-experimental/ui-patterns/behaviors/expansion/expansion.ts new file mode 100644 index 000000000000..aadc354acd72 --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/expansion/expansion.ts @@ -0,0 +1,68 @@ +/** + * @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 {computed} from '@angular/core'; +import {SignalLike} from '../signal-like/signal-like'; + +/** Inputs for an Expansion control. */ +export interface ExpansionControlInputs { + /** Whether an Expansion is visible. */ + visible: SignalLike; + + /** The controlled Expansion panel. */ + expansionPanel: SignalLike; +} + +/** Inputs for an Expansion panel. */ +export interface ExpansionPanelInputs { + /** A unique identifier for the panel. */ + id: SignalLike; + + /** The Expansion control. */ + expansionControl: SignalLike; +} + +/** + * An Expansion control. + * + * Use Expansion behavior if a pattern has a collapsible view that has two elements rely on the + * states from each other. For example + * + * ```html + * + * + * ... + * + * + * ``` + */ +export class ExpansionControl { + /** Whether an Expansion is visible. */ + visible: SignalLike; + + /** The controllerd Expansion panel Id. */ + controls = computed(() => this.inputs.expansionPanel()?.id()); + + constructor(readonly inputs: ExpansionControlInputs) { + this.visible = inputs.visible; + } +} + +/** A Expansion panel. */ +export class ExpansionPanel { + /** A unique identifier for the panel. */ + id: SignalLike; + + /** Whether the panel is hidden. */ + hidden = computed(() => !this.inputs.expansionControl()?.visible()); + + constructor(readonly inputs: ExpansionPanelInputs) { + this.id = inputs.id; + } +} diff --git a/src/cdk-experimental/ui-patterns/tabs/BUILD.bazel b/src/cdk-experimental/ui-patterns/tabs/BUILD.bazel index 299e39ee2545..01853ef19a33 100644 --- a/src/cdk-experimental/ui-patterns/tabs/BUILD.bazel +++ b/src/cdk-experimental/ui-patterns/tabs/BUILD.bazel @@ -10,6 +10,7 @@ ts_project( deps = [ "//:node_modules/@angular/core", "//src/cdk-experimental/ui-patterns/behaviors/event-manager", + "//src/cdk-experimental/ui-patterns/behaviors/expansion", "//src/cdk-experimental/ui-patterns/behaviors/list-focus", "//src/cdk-experimental/ui-patterns/behaviors/list-navigation", "//src/cdk-experimental/ui-patterns/behaviors/list-selection", diff --git a/src/cdk-experimental/ui-patterns/tabs/tabs.ts b/src/cdk-experimental/ui-patterns/tabs/tabs.ts index 820c145ab468..2ee7375553a8 100644 --- a/src/cdk-experimental/ui-patterns/tabs/tabs.ts +++ b/src/cdk-experimental/ui-patterns/tabs/tabs.ts @@ -21,11 +21,15 @@ import { ListSelectionInputs, ListSelectionItem, } from '../behaviors/list-selection/list-selection'; +import {ExpansionControl, ExpansionPanel} from '../behaviors/expansion/expansion'; import {SignalLike} from '../behaviors/signal-like/signal-like'; /** The required inputs to tabs. */ export interface TabInputs extends ListNavigationItem, ListSelectionItem, ListFocusItem { + /** The parent tablist that controls the tab. */ tablist: SignalLike; + + /** The remote tabpanel controlled by the tab. */ tabpanel: SignalLike; } @@ -37,55 +41,41 @@ export class TabPattern { /** A local unique identifier for the tab. */ value: SignalLike; - /** Whether the tab is active. */ - active = computed(() => this.tablist()?.focusManager.activeItem() === this); + /** Whether the tab is disabled. */ + disabled: SignalLike; - /** Whether the tab is selected. */ - selected = computed(() => this.tablist().selection.inputs.value().includes(this.value())); + /** The html element that should receive focus. */ + element: SignalLike; - /** A Tabpanel Id controlled by the tab. */ - controls = computed(() => this.tabpanel()?.id()); + /** Controls the expansion state for the tab. */ + expansionControl: ExpansionControl; - /** Whether the tab is disabled. */ - disabled: SignalLike; + /** Whether the tab is active. */ + active = computed(() => this.inputs.tablist().focusManager.activeItem() === this); - /** A reference to the parent tablist. */ - tablist: SignalLike; + /** Whether the tab is selected. */ + selected = computed( + () => !!this.inputs.tablist().selection.inputs.value().includes(this.value()), + ); - /** A reference to the corresponding tabpanel. */ - tabpanel: SignalLike; + /** A tabpanel Id controlled by the tab. */ + controls = computed(() => this.expansionControl.controls()); /** The tabindex of the tab. */ - tabindex = computed(() => this.tablist().focusManager.getItemTabindex(this)); + tabindex = computed(() => this.inputs.tablist().focusManager.getItemTabindex(this)); - /** The html element that should receive focus. */ - element: SignalLike; - - constructor(inputs: TabInputs) { + constructor(readonly inputs: TabInputs) { this.id = inputs.id; this.value = inputs.value; - this.tablist = inputs.tablist; - this.tabpanel = inputs.tabpanel; - this.element = inputs.element; this.disabled = inputs.disabled; + this.element = inputs.element; + this.expansionControl = new ExpansionControl({ + visible: this.selected, + expansionPanel: computed(() => inputs.tabpanel()?.expansionPanel), + }); } } -/** The selection operations that the tablist can perform. */ -interface SelectOptions { - select?: boolean; - toggle?: boolean; - toggleOne?: boolean; - selectOne?: boolean; -} - -/** The required inputs for the tablist. */ -export type TabListInputs = ListNavigationInputs & - Omit, 'multi'> & - ListFocusInputs & { - disabled: SignalLike; - }; - /** The required inputs for the tabpanel. */ export interface TabPanelInputs { id: SignalLike; @@ -101,19 +91,37 @@ export class TabPanelPattern { /** A local unique identifier for the tabpanel. */ value: SignalLike; - /** A reference to the corresponding tab. */ - tab: SignalLike; + /** Represents the expansion state for the tabpanel. */ + expansionPanel: ExpansionPanel; /** Whether the tabpanel is hidden. */ - hidden = computed(() => !this.tab()?.selected()); + hidden = computed(() => this.expansionPanel.hidden()); constructor(inputs: TabPanelInputs) { this.id = inputs.id; this.value = inputs.value; - this.tab = inputs.tab; + this.expansionPanel = new ExpansionPanel({ + id: inputs.id, + expansionControl: computed(() => inputs.tab()?.expansionControl), + }); } } +/** The selection operations that the tablist can perform. */ +interface SelectOptions { + select?: boolean; + toggle?: boolean; + toggleOne?: boolean; + selectOne?: boolean; +} + +/** The required inputs for the tablist. */ +export type TabListInputs = ListNavigationInputs & + Omit, 'multi'> & + ListFocusInputs & { + disabled: SignalLike; + }; + /** Controls the state of a tablist. */ export class TabListPattern { /** Controls navigation for the tablist. */