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 1
- * - Tab 2
- * - Tab 3
+ * - Tab 1
+ * - Tab 2
+ * - Tab 3
*
*
- *
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
+ *
+ *
+ * ...
+ *
+ *
+ * Show-Hide content
+ *
+ * ```
+ */
+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. */