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/show-hide/BUILD.bazel b/src/cdk-experimental/ui-patterns/behaviors/show-hide/BUILD.bazel new file mode 100644 index 000000000000..b819feb6b26f --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/show-hide/BUILD.bazel @@ -0,0 +1,31 @@ +load("//tools:defaults.bzl", "ng_web_test_suite", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "show-hide", + srcs = [ + "show-hide.ts", + ], + deps = [ + "//:node_modules/@angular/core", + "//src/cdk-experimental/ui-patterns/behaviors/signal-like", + ], +) + +ts_project( + name = "unit_test_sources", + testonly = True, + srcs = [ + "show-hide.spec.ts", + ], + deps = [ + ":show-hide", + "//:node_modules/@angular/core", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_test_sources"], +) diff --git a/src/cdk-experimental/ui-patterns/behaviors/show-hide/show-hide.spec.ts b/src/cdk-experimental/ui-patterns/behaviors/show-hide/show-hide.spec.ts new file mode 100644 index 000000000000..732b6f38534b --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/show-hide/show-hide.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 {ShowHideControl, ShowHidePanel} from './show-hide'; + +describe('Show Hide', () => { + let testShowHideControl: ShowHideControl; + let panelVisibility: WritableSignal; + let testShowHidePanel: ShowHidePanel; + + beforeEach(() => { + let showHideControlRef = signal(undefined); + let showHidePanelRef = signal(undefined); + panelVisibility = signal(false); + testShowHideControl = new ShowHideControl({ + visible: panelVisibility, + showHidePanel: showHidePanelRef, + }); + testShowHidePanel = new ShowHidePanel({ + id: () => 'test-panel', + showHideControl: showHideControlRef, + }); + showHideControlRef.set(testShowHideControl); + showHidePanelRef.set(testShowHidePanel); + }); + + it('sets a panel hidden to true by setting a control to invisible.', () => { + panelVisibility.set(false); + expect(testShowHidePanel.hidden()).toBeTrue(); + }); + + it('sets a panel hidden to false by setting a control to visible.', () => { + panelVisibility.set(true); + expect(testShowHidePanel.hidden()).toBeFalse(); + }); + + it('gets a controlled panel id from ShowHideControl.', () => { + expect(testShowHideControl.controls()).toBe('test-panel'); + }); +}); diff --git a/src/cdk-experimental/ui-patterns/behaviors/show-hide/show-hide.ts b/src/cdk-experimental/ui-patterns/behaviors/show-hide/show-hide.ts new file mode 100644 index 000000000000..623a149469b0 --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/show-hide/show-hide.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 a ShowHide control. */ +export interface ShowHideControlInputs { + /** Whether a ShowHide is visible. */ + visible: SignalLike; + + /** The controlled ShowHide panel. */ + showHidePanel: SignalLike; +} + +/** Inputs for a ShowHide panel. */ +export interface ShowHidePanelInputs { + /** A unique identifier for the panel. */ + id: SignalLike; + + /** The ShowHide control. */ + showHideControl: SignalLike; +} + +/** + * A ShowHide control. + * + * Use Show-Hide behavior if a pattern has a collapsible view that has two elements rely on the + * states from each other. For example + * + * ```html + * + * + * ... + * + * + * ``` + */ +export class ShowHideControl { + /** Whether a ShowHide is visible. */ + visible: SignalLike; + + /** The ShowHide panel Id controlled by this control. */ + controls = computed(() => this.inputs.showHidePanel()?.id()); + + constructor(readonly inputs: ShowHideControlInputs) { + this.visible = inputs.visible; + } +} + +/** A ShowHide panel. */ +export class ShowHidePanel { + /** A unique identifier for the panel. */ + id: SignalLike; + + /** Whether the panel is hidden. */ + hidden = computed(() => !this.inputs.showHideControl()?.visible()); + + constructor(readonly inputs: ShowHidePanelInputs) { + 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..e753bf56a9f4 100644 --- a/src/cdk-experimental/ui-patterns/tabs/BUILD.bazel +++ b/src/cdk-experimental/ui-patterns/tabs/BUILD.bazel @@ -13,6 +13,7 @@ ts_project( "//src/cdk-experimental/ui-patterns/behaviors/list-focus", "//src/cdk-experimental/ui-patterns/behaviors/list-navigation", "//src/cdk-experimental/ui-patterns/behaviors/list-selection", + "//src/cdk-experimental/ui-patterns/behaviors/show-hide", "//src/cdk-experimental/ui-patterns/behaviors/signal-like", ], ) diff --git a/src/cdk-experimental/ui-patterns/tabs/tabs.ts b/src/cdk-experimental/ui-patterns/tabs/tabs.ts index 820c145ab468..296f7a74167b 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 {ShowHideControl, ShowHidePanel} from '../behaviors/show-hide/show-hide'; 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 show-hide state for the tab. */ + showHideControl: ShowHideControl; - /** 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.showHideControl.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.showHideControl = new ShowHideControl({ + visible: this.selected, + showHidePanel: computed(() => inputs.tabpanel()?.showHidePanel), + }); } } -/** 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 show-hide state for the tabpanel. */ + showHidePanel: ShowHidePanel; /** Whether the tabpanel is hidden. */ - hidden = computed(() => !this.tab()?.selected()); + hidden = computed(() => this.showHidePanel.hidden()); constructor(inputs: TabPanelInputs) { this.id = inputs.id; this.value = inputs.value; - this.tab = inputs.tab; + this.showHidePanel = new ShowHidePanel({ + id: inputs.id, + showHideControl: computed(() => inputs.tab()?.showHideControl), + }); } } +/** 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. */