diff --git a/src/cdk-experimental/ui-patterns/behaviors/popup/BUILD.bazel b/src/cdk-experimental/ui-patterns/behaviors/popup/BUILD.bazel new file mode 100644 index 000000000000..b2a11c43043e --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/popup/BUILD.bazel @@ -0,0 +1,30 @@ +load("//tools:defaults.bzl", "ng_web_test_suite", "ts_project") + +package(default_visibility = ["//visibility:public"]) + +ts_project( + name = "popup", + srcs = glob( + ["**/*.ts"], + exclude = ["**/*.spec.ts"], + ), + deps = [ + "//:node_modules/@angular/core", + "//src/cdk-experimental/ui-patterns/behaviors/signal-like", + ], +) + +ts_project( + name = "unit_test_sources", + testonly = True, + srcs = glob(["**/*.spec.ts"]), + deps = [ + ":popup", + "//:node_modules/@angular/core", + ], +) + +ng_web_test_suite( + name = "unit_tests", + deps = [":unit_test_sources"], +) diff --git a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts new file mode 100644 index 000000000000..cf7795d5eac4 --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts @@ -0,0 +1,61 @@ +/** + * @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} from '@angular/core'; +import {PopupTypes, PopupControl, PopupControlInputs} from './popup'; + +type TestInputs = Partial>; + +function getPopupControl(inputs: TestInputs = {}): PopupControl { + const expanded = inputs.expanded || signal(false); + const controls = signal('popup-element-id'); + const hasPopup = signal(PopupTypes.LISTBOX); + + return new PopupControl({ + controls, + expanded, + hasPopup, + }); +} + +describe('Popup Control', () => { + describe('#open', () => { + it('should set expanded to true and popup inert to false', () => { + const control = getPopupControl(); + + expect(control.inputs.expanded()).toBeFalse(); + control.open(); + expect(control.inputs.expanded()).toBeTrue(); + }); + }); + + describe('#close', () => { + it('should set expanded to false and popup inert to true', () => { + const expanded = signal(true); + const control = getPopupControl({expanded}); + + expect(control.inputs.expanded()).toBeTrue(); + control.close(); + expect(control.inputs.expanded()).toBeFalse(); + }); + }); + + describe('#toggle', () => { + it('should toggle expanded and popup inert states', () => { + const control = getPopupControl(); + + expect(control.inputs.expanded()).toBeFalse(); + control.toggle(); + + expect(control.inputs.expanded()).toBeTrue(); + control.toggle(); + + expect(control.inputs.expanded()).toBeFalse(); + }); + }); +}); diff --git a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts new file mode 100644 index 000000000000..742c0abcc481 --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts @@ -0,0 +1,51 @@ +/** + * @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 {SignalLike, WritableSignalLike} from '../signal-like/signal-like'; + +/** Valid popup types for aria-haspopup. */ +export enum PopupTypes { + MENU = 'menu', + TREE = 'tree', + GRID = 'grid', + DIALOG = 'dialog', + LISTBOX = 'listbox', +} + +/** Represents the inputs for the PopupControl behavior. */ +export interface PopupControlInputs { + /* Refers to the element that serves as the popup. */ + controls: SignalLike; + + /* Whether the popup is open or closed. */ + expanded: WritableSignalLike; + + /* Corresponds to the popup type. */ + hasPopup: SignalLike; +} + +/** A behavior that manages the open/close state of a component. */ +export class PopupControl { + /** The inputs for the popup behavior, containing the `expanded` state signal. */ + constructor(readonly inputs: PopupControlInputs) {} + + /** Opens the popup by setting the expanded state to true. */ + open(): void { + this.inputs.expanded.set(true); + } + + /** Closes the popup by setting the expanded state to false. */ + close(): void { + this.inputs.expanded.set(false); + } + + /** Toggles the popup's expanded state. */ + toggle(): void { + this.inputs.expanded.set(!this.inputs.expanded()); + } +}