Skip to content

Commit 4b1e8f3

Browse files
committed
feat(cdk-experimental/ui-patterns): add show-hide behavior and refactor tabs
1 parent 0e39170 commit 4b1e8f3

File tree

6 files changed

+206
-46
lines changed

6 files changed

+206
-46
lines changed

src/cdk-experimental/tabs/tabs.ts

+12-7
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,20 @@ import {TabListPattern, TabPanelPattern, TabPattern} from '../ui-patterns';
3434
* ```html
3535
* <div cdkTabs>
3636
* <ul cdkTabList>
37-
* <li cdkTab>Tab 1</li>
38-
* <li cdkTab>Tab 2</li>
39-
* <li cdkTab>Tab 3</li>
37+
* <li cdkTab value="tab1">Tab 1</li>
38+
* <li cdkTab value="tab2">Tab 2</li>
39+
* <li cdkTab value="tab3">Tab 3</li>
4040
* </ul>
4141
*
42-
* <div cdkTabPanel>Tab content 1</div>
43-
* <div cdkTabPanel>Tab content 2</div>
44-
* <div cdkTabPanel>Tab content 3</div>
45-
* </div>
42+
* <div cdkTabPanel value="tab1">
43+
* <ng-template cdkTabContent>Tab content 1</ng-template>
44+
* </div>
45+
* <div cdkTabPanel value="tab2">
46+
* <ng-template cdkTabContent>Tab content 2</ng-template>
47+
* </div>
48+
* <div cdkTabPanel value="tab3">
49+
* <ng-template cdkTabContent>Tab content 3</ng-template>
50+
* </div>
4651
* ```
4752
*/
4853
@Directive({
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
load("//tools:defaults.bzl", "ng_web_test_suite", "ts_project")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_project(
6+
name = "show-hide",
7+
srcs = [
8+
"show-hide.ts",
9+
],
10+
deps = [
11+
"//:node_modules/@angular/core",
12+
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
13+
],
14+
)
15+
16+
ts_project(
17+
name = "unit_test_sources",
18+
testonly = True,
19+
srcs = [
20+
"show-hide.spec.ts",
21+
],
22+
deps = [
23+
":list-navigation",
24+
"//:node_modules/@angular/core",
25+
"//src/cdk-experimental/ui-patterns/behaviors/list-focus:unit_test_sources",
26+
],
27+
)
28+
29+
ng_web_test_suite(
30+
name = "unit_tests",
31+
deps = [":unit_test_sources"],
32+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {signal, WritableSignal} from '@angular/core';
10+
import {ShowHideControl, ShowHidePanel} from './show-hide';
11+
12+
describe('Show Hide', () => {
13+
let testShowHideControl: ShowHideControl;
14+
let panelVisibility: WritableSignal<boolean>;
15+
let testShowHidePanel: ShowHidePanel;
16+
17+
beforeEach(() => {
18+
let showHideControlRef = signal<ShowHideControl | undefined>(undefined);
19+
let showHidePanelRef = signal<ShowHidePanel | undefined>(undefined);
20+
panelVisibility = signal(false);
21+
testShowHideControl = new ShowHideControl({
22+
visible: panelVisibility,
23+
showHidePanel: showHidePanelRef,
24+
});
25+
testShowHidePanel = new ShowHidePanel({
26+
id: () => 'test-panel',
27+
showHideControl: showHideControlRef,
28+
});
29+
showHideControlRef.set(testShowHideControl);
30+
showHidePanelRef.set(testShowHidePanel);
31+
});
32+
33+
it('sets a panel hidden to true by setting a control to invisible.', () => {
34+
panelVisibility.set(false);
35+
expect(testShowHidePanel.hidden()).toBeTrue();
36+
});
37+
38+
it('sets a panel hidden to false by setting a control to visible.', () => {
39+
panelVisibility.set(true);
40+
expect(testShowHidePanel.hidden()).toBeFalse();
41+
});
42+
43+
it('gets a controlled panel id from ShowHideControl.', () => {
44+
expect(testShowHideControl.controls()).toBe('test-panel');
45+
});
46+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
import {computed} from '@angular/core';
9+
import {SignalLike} from '../signal-like/signal-like';
10+
11+
/** Inputs for a ShowHide control. */
12+
export interface ShowHideControlInputs {
13+
/** Whether a ShowHide is visible. */
14+
visible: SignalLike<boolean>;
15+
16+
/** The controlled ShowHide panel. */
17+
showHidePanel: SignalLike<ShowHidePanel | undefined>;
18+
}
19+
20+
/** Inputs for a ShowHide panel. */
21+
export interface ShowHidePanelInputs {
22+
/** A unique identifier for the panel. */
23+
id: SignalLike<string>;
24+
25+
/** The ShowHide control. */
26+
showHideControl: SignalLike<ShowHideControl | undefined>;
27+
}
28+
29+
/**
30+
* A ShowHide control.
31+
*
32+
* Use ShowHide behavior if a pttern has a show-hide or collapsible view that has two elements rely
33+
* on the states of each other. For example
34+
*
35+
* ```html
36+
* <button aria-controls="remote-content" aria-expanded="false">Toggle Content</button>
37+
*
38+
* ...
39+
*
40+
* <div id="remote-content" aria-hidden="true">
41+
* Show-Hide content
42+
* </div>
43+
* ```
44+
*/
45+
export class ShowHideControl {
46+
/** Whether a ShowHide is visible. */
47+
visible: SignalLike<boolean>;
48+
49+
/** The ShowHide panel Id controlled by this control. */
50+
controls = computed(() => this.inputs.showHidePanel()?.id());
51+
52+
constructor(readonly inputs: ShowHideControlInputs) {
53+
this.visible = inputs.visible;
54+
}
55+
}
56+
57+
/** A ShowHide panel. */
58+
export class ShowHidePanel {
59+
/** A unique identifier for the panel. */
60+
id: SignalLike<string>;
61+
62+
/** Whether the panel is hidden. */
63+
hidden = computed(() => !this.inputs.showHideControl()?.visible());
64+
65+
constructor(readonly inputs: ShowHidePanelInputs) {
66+
this.id = inputs.id;
67+
}
68+
}

src/cdk-experimental/ui-patterns/tabs/BUILD.bazel

+1
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ ts_project(
1313
"//src/cdk-experimental/ui-patterns/behaviors/list-focus",
1414
"//src/cdk-experimental/ui-patterns/behaviors/list-navigation",
1515
"//src/cdk-experimental/ui-patterns/behaviors/list-selection",
16+
"//src/cdk-experimental/ui-patterns/behaviors/show-hide",
1617
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
1718
],
1819
)

src/cdk-experimental/ui-patterns/tabs/tabs.ts

+47-39
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,15 @@ import {
2121
ListSelectionInputs,
2222
ListSelectionItem,
2323
} from '../behaviors/list-selection/list-selection';
24+
import {ShowHideControl, ShowHidePanel} from '../behaviors/show-hide/show-hide';
2425
import {SignalLike} from '../behaviors/signal-like/signal-like';
2526

2627
/** The required inputs to tabs. */
2728
export interface TabInputs extends ListNavigationItem, ListSelectionItem<string>, ListFocusItem {
29+
/** The parent tablist that controls the tab. */
2830
tablist: SignalLike<TabListPattern>;
31+
32+
/** The remote tabpanel controlled by the tab. */
2933
tabpanel: SignalLike<TabPanelPattern | undefined>;
3034
}
3135

@@ -37,55 +41,41 @@ export class TabPattern {
3741
/** A local unique identifier for the tab. */
3842
value: SignalLike<string>;
3943

40-
/** Whether the tab is active. */
41-
active = computed(() => this.tablist()?.focusManager.activeItem() === this);
44+
/** Whether the tab is disabled. */
45+
disabled: SignalLike<boolean>;
4246

43-
/** Whether the tab is selected. */
44-
selected = computed(() => this.tablist().selection.inputs.value().includes(this.value()));
47+
/** The html element that should receive focus. */
48+
element: SignalLike<HTMLElement>;
4549

46-
/** A Tabpanel Id controlled by the tab. */
47-
controls = computed(() => this.tabpanel()?.id());
50+
/** Controls the show-hide state for the tab. */
51+
showHideControl: ShowHideControl;
4852

49-
/** Whether the tab is disabled. */
50-
disabled: SignalLike<boolean>;
53+
/** Whether the tab is active. */
54+
active = computed(() => this.inputs.tablist().focusManager.activeItem() === this);
5155

52-
/** A reference to the parent tablist. */
53-
tablist: SignalLike<TabListPattern>;
56+
/** Whether the tab is selected. */
57+
selected = computed(
58+
() => !!this.inputs.tablist().selection.inputs.value().includes(this.value()),
59+
);
5460

55-
/** A reference to the corresponding tabpanel. */
56-
tabpanel: SignalLike<TabPanelPattern | undefined>;
61+
/** A tabpanel Id controlled by the tab. */
62+
controls = computed(() => this.showHideControl.controls());
5763

5864
/** The tabindex of the tab. */
59-
tabindex = computed(() => this.tablist().focusManager.getItemTabindex(this));
65+
tabindex = computed(() => this.inputs.tablist().focusManager.getItemTabindex(this));
6066

61-
/** The html element that should receive focus. */
62-
element: SignalLike<HTMLElement>;
63-
64-
constructor(inputs: TabInputs) {
67+
constructor(readonly inputs: TabInputs) {
6568
this.id = inputs.id;
6669
this.value = inputs.value;
67-
this.tablist = inputs.tablist;
68-
this.tabpanel = inputs.tabpanel;
69-
this.element = inputs.element;
7070
this.disabled = inputs.disabled;
71+
this.element = inputs.element;
72+
this.showHideControl = new ShowHideControl({
73+
visible: this.selected,
74+
showHidePanel: computed(() => inputs.tabpanel()?.showHidePanel),
75+
});
7176
}
7277
}
7378

74-
/** The selection operations that the tablist can perform. */
75-
interface SelectOptions {
76-
select?: boolean;
77-
toggle?: boolean;
78-
toggleOne?: boolean;
79-
selectOne?: boolean;
80-
}
81-
82-
/** The required inputs for the tablist. */
83-
export type TabListInputs = ListNavigationInputs<TabPattern> &
84-
Omit<ListSelectionInputs<TabPattern, string>, 'multi'> &
85-
ListFocusInputs<TabPattern> & {
86-
disabled: SignalLike<boolean>;
87-
};
88-
8979
/** The required inputs for the tabpanel. */
9080
export interface TabPanelInputs {
9181
id: SignalLike<string>;
@@ -101,19 +91,37 @@ export class TabPanelPattern {
10191
/** A local unique identifier for the tabpanel. */
10292
value: SignalLike<string>;
10393

104-
/** A reference to the corresponding tab. */
105-
tab: SignalLike<TabPattern | undefined>;
94+
/** Represents the show-hide state for the tabpanel. */
95+
showHidePanel: ShowHidePanel;
10696

10797
/** Whether the tabpanel is hidden. */
108-
hidden = computed(() => !this.tab()?.selected());
98+
hidden = computed(() => this.showHidePanel.hidden());
10999

110100
constructor(inputs: TabPanelInputs) {
111101
this.id = inputs.id;
112102
this.value = inputs.value;
113-
this.tab = inputs.tab;
103+
this.showHidePanel = new ShowHidePanel({
104+
id: inputs.id,
105+
showHideControl: computed(() => inputs.tab()?.showHideControl),
106+
});
114107
}
115108
}
116109

110+
/** The selection operations that the tablist can perform. */
111+
interface SelectOptions {
112+
select?: boolean;
113+
toggle?: boolean;
114+
toggleOne?: boolean;
115+
selectOne?: boolean;
116+
}
117+
118+
/** The required inputs for the tablist. */
119+
export type TabListInputs = ListNavigationInputs<TabPattern> &
120+
Omit<ListSelectionInputs<TabPattern, string>, 'multi'> &
121+
ListFocusInputs<TabPattern> & {
122+
disabled: SignalLike<boolean>;
123+
};
124+
117125
/** Controls the state of a tablist. */
118126
export class TabListPattern {
119127
/** Controls navigation for the tablist. */

0 commit comments

Comments
 (0)