Skip to content

Commit 232f390

Browse files
committed
refactor(cdk-experimental/ui-patterns): iterate on expansion behavior
1 parent 9288222 commit 232f390

File tree

5 files changed

+369
-122
lines changed

5 files changed

+369
-122
lines changed

src/cdk-experimental/ui-patterns/behaviors/expansion/BUILD.bazel

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ ts_project(
99
],
1010
deps = [
1111
"//:node_modules/@angular/core",
12+
"//src/cdk-experimental/ui-patterns/behaviors/list-focus",
1213
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
1314
],
1415
)
@@ -22,6 +23,7 @@ ts_project(
2223
deps = [
2324
":expansion",
2425
"//:node_modules/@angular/core",
26+
"//src/cdk-experimental/ui-patterns/behaviors/list-focus:unit_test_sources",
2527
],
2628
)
2729

src/cdk-experimental/ui-patterns/behaviors/expansion/expansion.spec.ts

Lines changed: 253 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -6,41 +6,269 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {signal, WritableSignal} from '@angular/core';
10-
import {ExpansionControl, ExpansionPanel} from './expansion';
9+
import {Signal, WritableSignal, signal} from '@angular/core';
10+
import {Expansion, ExpansionInputs, ExpansionItem} from './expansion';
11+
import {ListFocus, ListFocusInputs, ListFocusItem} from '../list-focus/list-focus';
12+
import {getListFocus as getListFocusManager} from '../list-focus/list-focus.spec';
13+
14+
type TestItem = ListFocusItem &
15+
ExpansionItem & {
16+
id: WritableSignal<string>;
17+
disabled: WritableSignal<boolean>;
18+
element: WritableSignal<HTMLElement>;
19+
expandable: WritableSignal<boolean>;
20+
expansionId: WritableSignal<string>;
21+
};
22+
23+
type TestInputs = Partial<Omit<ExpansionInputs<TestItem>, 'items' | 'focusManager'>> &
24+
Partial<
25+
Pick<ListFocusInputs<TestItem>, 'focusMode' | 'disabled' | 'activeIndex' | 'skipDisabled'>
26+
> & {
27+
numItems?: number;
28+
initialExpandedIds?: string[];
29+
};
30+
31+
function createItems(length: number): WritableSignal<TestItem[]> {
32+
return signal(
33+
Array.from({length}).map((_, i) => {
34+
const itemId = `item-${i}`;
35+
return {
36+
id: signal(itemId),
37+
element: signal(document.createElement('div') as HTMLElement),
38+
disabled: signal(false),
39+
expandable: signal(true),
40+
expansionId: signal(itemId),
41+
};
42+
}),
43+
);
44+
}
45+
46+
function getExpansion(inputs: TestInputs = {}): {
47+
expansion: Expansion<TestItem>;
48+
items: TestItem[];
49+
focusManager: ListFocus<TestItem>;
50+
} {
51+
const numItems = inputs.numItems ?? 3;
52+
const items = createItems(numItems);
53+
54+
const listFocusManagerInputs: Partial<ListFocusInputs<TestItem>> & {items: Signal<TestItem[]>} = {
55+
items: items,
56+
activeIndex: inputs.activeIndex ?? signal(0),
57+
disabled: inputs.disabled ?? signal(false),
58+
skipDisabled: inputs.skipDisabled ?? signal(true),
59+
focusMode: inputs.focusMode ?? signal('roving'),
60+
};
61+
62+
const focusManager = getListFocusManager(listFocusManagerInputs as any) as ListFocus<TestItem>;
63+
64+
const expansion = new Expansion<TestItem>({
65+
items: items,
66+
activeIndex: focusManager.inputs.activeIndex,
67+
disabled: focusManager.inputs.disabled,
68+
skipDisabled: focusManager.inputs.skipDisabled,
69+
focusMode: focusManager.inputs.focusMode,
70+
multiExpandable: inputs.multiExpandable ?? signal(false),
71+
focusManager,
72+
});
73+
74+
if (inputs.initialExpandedIds) {
75+
expansion.expandedIds.set(inputs.initialExpandedIds);
76+
}
77+
78+
return {expansion, items: items(), focusManager};
79+
}
1180

1281
describe('Expansion', () => {
13-
let testExpansionControl: ExpansionControl;
14-
let panelVisibility: WritableSignal<boolean>;
15-
let testExpansionPanel: ExpansionPanel;
82+
describe('#open', () => {
83+
it('should open only one item at a time when multiExpandable is false', () => {
84+
const {expansion, items} = getExpansion({
85+
multiExpandable: signal(false),
86+
});
87+
88+
expansion.open(items[0]);
89+
expect(expansion.expandedIds()).toEqual(['item-0']);
90+
91+
expansion.open(items[1]);
92+
expect(expansion.expandedIds()).toEqual(['item-1']);
93+
});
94+
95+
it('should open multiple items when multiExpandable is true', () => {
96+
const {expansion, items} = getExpansion({
97+
multiExpandable: signal(true),
98+
});
99+
100+
expansion.open(items[0]);
101+
expect(expansion.expandedIds()).toEqual(['item-0']);
102+
103+
expansion.open(items[1]);
104+
expect(expansion.expandedIds()).toEqual(['item-0', 'item-1']);
105+
});
106+
107+
it('should not open an item if it is not expandable (expandable is false)', () => {
108+
const {expansion, items} = getExpansion();
109+
items[1].expandable.set(false);
110+
expansion.open(items[1]);
111+
expect(expansion.expandedIds()).toEqual([]);
112+
});
113+
114+
it('should not open an item if it is not focusable (disabled and skipDisabled is true)', () => {
115+
const {expansion, items} = getExpansion({skipDisabled: signal(true)});
116+
items[1].disabled.set(true);
117+
expansion.open(items[1]);
118+
expect(expansion.expandedIds()).toEqual([]);
119+
});
120+
});
121+
122+
describe('#close', () => {
123+
it('should close the specified item', () => {
124+
const {expansion, items} = getExpansion({initialExpandedIds: ['item-0', 'item-1']});
125+
expansion.close(items[0]);
126+
expect(expansion.expandedIds()).toEqual(['item-1']);
127+
});
128+
129+
it('should not close an item if it is not expandable', () => {
130+
const {expansion, items} = getExpansion({initialExpandedIds: ['item-0']});
131+
items[0].expandable.set(false);
132+
expansion.close(items[0]);
133+
expect(expansion.expandedIds()).toEqual(['item-0']);
134+
});
135+
136+
it('should not close an item if it is not focusable (disabled and skipDisabled is true)', () => {
137+
const {expansion, items} = getExpansion({
138+
initialExpandedIds: ['item-0'],
139+
skipDisabled: signal(true),
140+
});
141+
items[0].disabled.set(true);
142+
expansion.close(items[0]);
143+
expect(expansion.expandedIds()).toEqual(['item-0']);
144+
});
145+
});
16146

17-
beforeEach(() => {
18-
let expansionControlRef = signal<ExpansionControl | undefined>(undefined);
19-
let expansionPanelRef = signal<ExpansionPanel | undefined>(undefined);
20-
panelVisibility = signal(false);
21-
testExpansionControl = new ExpansionControl({
22-
visible: panelVisibility,
23-
expansionPanel: expansionPanelRef,
147+
describe('#toggle', () => {
148+
it('should open a closed item', () => {
149+
const {expansion, items} = getExpansion();
150+
expansion.toggle(items[0]);
151+
expect(expansion.expandedIds()).toEqual(['item-0']);
24152
});
25-
testExpansionPanel = new ExpansionPanel({
26-
id: () => 'test-panel',
27-
expansionControl: expansionControlRef,
153+
154+
it('should close an opened item', () => {
155+
const {expansion, items} = getExpansion({
156+
initialExpandedIds: ['item-0'],
157+
});
158+
expansion.toggle(items[0]);
159+
expect(expansion.expandedIds()).toEqual([]);
28160
});
29-
expansionControlRef.set(testExpansionControl);
30-
expansionPanelRef.set(testExpansionPanel);
31161
});
32162

33-
it('sets a panel hidden to true by setting a control to invisible.', () => {
34-
panelVisibility.set(false);
35-
expect(testExpansionPanel.hidden()).toBeTrue();
163+
describe('#openAll', () => {
164+
it('should open all focusable and expandable items when multiExpandable is true', () => {
165+
const {expansion, items} = getExpansion({
166+
numItems: 3,
167+
multiExpandable: signal(true),
168+
});
169+
expansion.openAll();
170+
expect(expansion.expandedIds()).toEqual(['item-0', 'item-1', 'item-2']);
171+
});
172+
173+
it('should not expand items that are not expandable', () => {
174+
const {expansion, items} = getExpansion({
175+
numItems: 3,
176+
multiExpandable: signal(true),
177+
});
178+
items[1].expandable.set(false);
179+
expansion.openAll();
180+
expect(expansion.expandedIds()).toEqual(['item-0', 'item-2']);
181+
});
182+
183+
it('should not expand items that are not focusable (disabled and skipDisabled is true)', () => {
184+
const {expansion, items} = getExpansion({
185+
numItems: 3,
186+
multiExpandable: signal(true),
187+
});
188+
items[1].disabled.set(true);
189+
expansion.openAll();
190+
expect(expansion.expandedIds()).toEqual(['item-0', 'item-2']);
191+
});
192+
193+
it('should do nothing when multiExpandable is false', () => {
194+
const {expansion, items} = getExpansion({
195+
numItems: 3,
196+
multiExpandable: signal(false),
197+
});
198+
expansion.openAll();
199+
expect(expansion.expandedIds()).toEqual([]);
200+
});
36201
});
37202

38-
it('sets a panel hidden to false by setting a control to visible.', () => {
39-
panelVisibility.set(true);
40-
expect(testExpansionPanel.hidden()).toBeFalse();
203+
describe('#closeAll', () => {
204+
it('should close all expanded items', () => {
205+
const {expansion, items} = getExpansion({
206+
multiExpandable: signal(true),
207+
initialExpandedIds: ['item-0', 'item-2'],
208+
});
209+
items[1].expandable.set(false);
210+
expansion.closeAll();
211+
expect(expansion.expandedIds()).toEqual([]);
212+
});
213+
214+
it('should not close items that are not expandable', () => {
215+
const {expansion, items} = getExpansion({
216+
multiExpandable: signal(true),
217+
initialExpandedIds: ['item-0', 'item-1', 'item-2'],
218+
});
219+
items[1].expandable.set(false);
220+
expansion.closeAll();
221+
expect(expansion.expandedIds()).toEqual(['item-1']);
222+
});
223+
224+
it('should not close items that are not focusable (disabled and skipDisabled is true)', () => {
225+
const {expansion, items} = getExpansion({
226+
skipDisabled: signal(true),
227+
multiExpandable: signal(true),
228+
initialExpandedIds: ['item-0', 'item-1', 'item-2'],
229+
});
230+
items[1].disabled.set(true);
231+
expansion.closeAll();
232+
expect(expansion.expandedIds()).toEqual(['item-1']);
233+
});
41234
});
42235

43-
it('gets a controlled panel id from ExpansionControl.', () => {
44-
expect(testExpansionControl.controls()).toBe('test-panel');
236+
describe('#isExpandable', () => {
237+
it('should return true if an item is focusable and expandable is true', () => {
238+
const {expansion, items} = getExpansion();
239+
items[0].expandable.set(true);
240+
items[0].disabled.set(false);
241+
expect(expansion.isExpandable(items[0])).toBeTrue();
242+
});
243+
244+
it('should return true if an item is disabled and skipDisabled is false', () => {
245+
const {expansion, items} = getExpansion({skipDisabled: signal(false)});
246+
items[0].disabled.set(true);
247+
expect(expansion.isExpandable(items[0])).toBeTrue();
248+
});
249+
250+
it('should return false if an item is disabled and skipDisabled is true', () => {
251+
const {expansion, items} = getExpansion({skipDisabled: signal(true)});
252+
items[0].disabled.set(true);
253+
expect(expansion.isExpandable(items[0])).toBeFalse();
254+
});
255+
256+
it('should return false if expandable is false', () => {
257+
const {expansion, items} = getExpansion();
258+
items[0].expandable.set(false);
259+
expect(expansion.isExpandable(items[0])).toBeFalse();
260+
});
261+
});
262+
263+
describe('#isExpanded', () => {
264+
it('should return true if item ID is in expandedIds', () => {
265+
const {expansion, items} = getExpansion({initialExpandedIds: ['item-0']});
266+
expect(expansion.isExpanded(items[0])).toBeTrue();
267+
});
268+
269+
it('should return false if item ID is not in expandedIds', () => {
270+
const {expansion, items} = getExpansion();
271+
expect(expansion.isExpanded(items[0])).toBeFalse();
272+
});
45273
});
46274
});

0 commit comments

Comments
 (0)