Skip to content

Commit a6625a1

Browse files
committed
feat(cdk-experimental/ui-patterns): add popup behavior
* Adds a new popup behavior to manage the open/close state of a component. * Includes the PopupControl class with open, close, and toggle methods. * Provides comprehensive unit tests for the new behavior.
1 parent 3cfbd9d commit a6625a1

File tree

3 files changed

+176
-0
lines changed

3 files changed

+176
-0
lines changed
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
load("//tools:defaults.bzl", "ng_web_test_suite", "ts_project")
2+
3+
package(default_visibility = ["//visibility:public"])
4+
5+
ts_project(
6+
name = "popup",
7+
srcs = glob(
8+
["**/*.ts"],
9+
exclude = ["**/*.spec.ts"],
10+
),
11+
deps = [
12+
"//:node_modules/@angular/core",
13+
"//src/cdk-experimental/ui-patterns/behaviors/signal-like",
14+
],
15+
)
16+
17+
ts_project(
18+
name = "unit_test_sources",
19+
testonly = True,
20+
srcs = glob(["**/*.spec.ts"]),
21+
deps = [
22+
":popup",
23+
"//:node_modules/@angular/core",
24+
],
25+
)
26+
27+
ng_web_test_suite(
28+
name = "unit_tests",
29+
deps = [":unit_test_sources"],
30+
)
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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} from '@angular/core';
10+
import {ComboboxPopupTypes, PopupControl, PopupControlInputs} from './popup';
11+
12+
type TestInputs = Partial<Pick<PopupControlInputs, 'popup' | 'expanded'>>;
13+
14+
function getPopupControl(inputs: TestInputs = {}): PopupControl {
15+
const expanded = inputs.expanded || signal(false);
16+
const popup = signal({inert: signal(!expanded())});
17+
const controls = signal('popup-element-id');
18+
const hasPopup = signal(ComboboxPopupTypes.LISTBOX);
19+
20+
return new PopupControl({
21+
popup,
22+
controls,
23+
expanded,
24+
hasPopup,
25+
});
26+
}
27+
28+
describe('Popup Control', () => {
29+
describe('#open', () => {
30+
it('should set expanded to true and popup inert to false', () => {
31+
const control = getPopupControl();
32+
33+
expect(control.inputs.expanded()).toBeFalse();
34+
expect(control.inputs.popup().inert()).toBeTrue();
35+
36+
control.open();
37+
38+
expect(control.inputs.expanded()).toBeTrue();
39+
expect(control.inputs.popup().inert()).toBeFalse();
40+
});
41+
});
42+
43+
describe('#close', () => {
44+
it('should set expanded to false and popup inert to true', () => {
45+
const expanded = signal(true);
46+
const control = getPopupControl({expanded});
47+
48+
expect(control.inputs.expanded()).toBeTrue();
49+
expect(control.inputs.popup().inert()).toBeFalse();
50+
51+
control.close();
52+
53+
expect(control.inputs.expanded()).toBeFalse();
54+
expect(control.inputs.popup().inert()).toBeTrue();
55+
});
56+
});
57+
58+
describe('#toggle', () => {
59+
it('should toggle expanded and popup inert states', () => {
60+
const control = getPopupControl();
61+
62+
expect(control.inputs.expanded()).toBeFalse();
63+
expect(control.inputs.popup().inert()).toBeTrue();
64+
65+
control.toggle();
66+
67+
expect(control.inputs.expanded()).toBeTrue();
68+
expect(control.inputs.popup().inert()).toBeFalse();
69+
70+
control.toggle();
71+
72+
expect(control.inputs.expanded()).toBeFalse();
73+
expect(control.inputs.popup().inert()).toBeTrue();
74+
});
75+
});
76+
});
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
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 {SignalLike, WritableSignalLike} from '../signal-like/signal-like';
10+
11+
/**
12+
* Represents the inputs required for a popup behavior.
13+
* It includes a signal for the expanded state and a reference to the popup element.
14+
*/
15+
export enum ComboboxPopupTypes {
16+
TREE = 'tree',
17+
GRID = 'grid',
18+
DIALOG = 'dialog',
19+
LISTBOX = 'listbox',
20+
}
21+
22+
/** The element that serves as the popup. */
23+
export interface Popup {
24+
/** Whether the popup is interactive or not. */
25+
inert: WritableSignalLike<boolean>;
26+
}
27+
28+
/** Represents the inputs for the PopupControl behavior. */
29+
export interface PopupControlInputs {
30+
/** The element that serves as the popup. */
31+
popup: SignalLike<Popup>;
32+
33+
/* Refers to the element that serves as the popup. */
34+
controls: SignalLike<string>;
35+
36+
/* Whether the popup is open or closed. */
37+
expanded: WritableSignalLike<boolean>;
38+
39+
/* Corresponds to the popup type. */
40+
hasPopup: SignalLike<ComboboxPopupTypes>;
41+
}
42+
43+
/**
44+
* A behavior that manages the open/close state of a component.
45+
* It provides methods to open, close, and toggle the state,
46+
* which is controlled via a writable signal.
47+
*/
48+
export class PopupControl {
49+
/** The inputs for the popup behavior, containing the `expanded` state signal. */
50+
constructor(readonly inputs: PopupControlInputs) {}
51+
52+
/** Opens the popup by setting the expanded state to true. */
53+
open(): void {
54+
this.inputs.expanded.set(true);
55+
this.inputs.popup().inert.set(false);
56+
}
57+
58+
/** Closes the popup by setting the expanded state to false. */
59+
close(): void {
60+
this.inputs.expanded.set(false);
61+
this.inputs.popup().inert.set(true);
62+
}
63+
64+
/** Toggles the popup's expanded state. */
65+
toggle(): void {
66+
const expanded = !this.inputs.expanded();
67+
this.inputs.expanded.set(expanded);
68+
this.inputs.popup().inert.set(!expanded);
69+
}
70+
}

0 commit comments

Comments
 (0)