From a6625a1c509ffb52d95d2ab4599c412f3459b12b Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Wed, 16 Jul 2025 11:57:48 -0400 Subject: [PATCH 1/4] 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. --- .../ui-patterns/behaviors/popup/BUILD.bazel | 30 ++++++++ .../ui-patterns/behaviors/popup/popup.spec.ts | 76 +++++++++++++++++++ .../ui-patterns/behaviors/popup/popup.ts | 70 +++++++++++++++++ 3 files changed, 176 insertions(+) create mode 100644 src/cdk-experimental/ui-patterns/behaviors/popup/BUILD.bazel create mode 100644 src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts create mode 100644 src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts 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..3ee279d76b1d --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts @@ -0,0 +1,76 @@ +/** + * @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 {ComboboxPopupTypes, PopupControl, PopupControlInputs} from './popup'; + +type TestInputs = Partial>; + +function getPopupControl(inputs: TestInputs = {}): PopupControl { + const expanded = inputs.expanded || signal(false); + const popup = signal({inert: signal(!expanded())}); + const controls = signal('popup-element-id'); + const hasPopup = signal(ComboboxPopupTypes.LISTBOX); + + return new PopupControl({ + popup, + 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(); + expect(control.inputs.popup().inert()).toBeTrue(); + + control.open(); + + expect(control.inputs.expanded()).toBeTrue(); + expect(control.inputs.popup().inert()).toBeFalse(); + }); + }); + + 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(); + expect(control.inputs.popup().inert()).toBeFalse(); + + control.close(); + + expect(control.inputs.expanded()).toBeFalse(); + expect(control.inputs.popup().inert()).toBeTrue(); + }); + }); + + describe('#toggle', () => { + it('should toggle expanded and popup inert states', () => { + const control = getPopupControl(); + + expect(control.inputs.expanded()).toBeFalse(); + expect(control.inputs.popup().inert()).toBeTrue(); + + control.toggle(); + + expect(control.inputs.expanded()).toBeTrue(); + expect(control.inputs.popup().inert()).toBeFalse(); + + control.toggle(); + + expect(control.inputs.expanded()).toBeFalse(); + expect(control.inputs.popup().inert()).toBeTrue(); + }); + }); +}); 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..e64debd8807b --- /dev/null +++ b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts @@ -0,0 +1,70 @@ +/** + * @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'; + +/** + * Represents the inputs required for a popup behavior. + * It includes a signal for the expanded state and a reference to the popup element. + */ +export enum ComboboxPopupTypes { + TREE = 'tree', + GRID = 'grid', + DIALOG = 'dialog', + LISTBOX = 'listbox', +} + +/** The element that serves as the popup. */ +export interface Popup { + /** Whether the popup is interactive or not. */ + inert: WritableSignalLike; +} + +/** Represents the inputs for the PopupControl behavior. */ +export interface PopupControlInputs { + /** The element that serves as the popup. */ + popup: SignalLike; + + /* 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. + * It provides methods to open, close, and toggle the state, + * which is controlled via a writable signal. + */ +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); + this.inputs.popup().inert.set(false); + } + + /** Closes the popup by setting the expanded state to false. */ + close(): void { + this.inputs.expanded.set(false); + this.inputs.popup().inert.set(true); + } + + /** Toggles the popup's expanded state. */ + toggle(): void { + const expanded = !this.inputs.expanded(); + this.inputs.expanded.set(expanded); + this.inputs.popup().inert.set(!expanded); + } +} From c38624110b6025cc9a305246a0b2301e9b023158 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Wed, 16 Jul 2025 16:58:16 -0400 Subject: [PATCH 2/4] fixup! feat(cdk-experimental/ui-patterns): add popup behavior --- .../ui-patterns/behaviors/popup/popup.spec.ts | 20 ++----------- .../ui-patterns/behaviors/popup/popup.ts | 28 ++++--------------- 2 files changed, 8 insertions(+), 40 deletions(-) diff --git a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts index 3ee279d76b1d..45d82abc980b 100644 --- a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts +++ b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts @@ -7,18 +7,17 @@ */ import {signal} from '@angular/core'; -import {ComboboxPopupTypes, PopupControl, PopupControlInputs} from './popup'; +import {PopupTypes, PopupControl, PopupControlInputs} from './popup'; -type TestInputs = Partial>; +type TestInputs = Partial>; function getPopupControl(inputs: TestInputs = {}): PopupControl { const expanded = inputs.expanded || signal(false); const popup = signal({inert: signal(!expanded())}); const controls = signal('popup-element-id'); - const hasPopup = signal(ComboboxPopupTypes.LISTBOX); + const hasPopup = signal(PopupTypes.LISTBOX); return new PopupControl({ - popup, controls, expanded, hasPopup, @@ -31,12 +30,8 @@ describe('Popup Control', () => { const control = getPopupControl(); expect(control.inputs.expanded()).toBeFalse(); - expect(control.inputs.popup().inert()).toBeTrue(); - control.open(); - expect(control.inputs.expanded()).toBeTrue(); - expect(control.inputs.popup().inert()).toBeFalse(); }); }); @@ -46,12 +41,8 @@ describe('Popup Control', () => { const control = getPopupControl({expanded}); expect(control.inputs.expanded()).toBeTrue(); - expect(control.inputs.popup().inert()).toBeFalse(); - control.close(); - expect(control.inputs.expanded()).toBeFalse(); - expect(control.inputs.popup().inert()).toBeTrue(); }); }); @@ -60,17 +51,12 @@ describe('Popup Control', () => { const control = getPopupControl(); expect(control.inputs.expanded()).toBeFalse(); - expect(control.inputs.popup().inert()).toBeTrue(); - control.toggle(); expect(control.inputs.expanded()).toBeTrue(); - expect(control.inputs.popup().inert()).toBeFalse(); - control.toggle(); expect(control.inputs.expanded()).toBeFalse(); - expect(control.inputs.popup().inert()).toBeTrue(); }); }); }); diff --git a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts index e64debd8807b..e77b9e6ab7db 100644 --- a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts +++ b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts @@ -8,28 +8,17 @@ import {SignalLike, WritableSignalLike} from '../signal-like/signal-like'; -/** - * Represents the inputs required for a popup behavior. - * It includes a signal for the expanded state and a reference to the popup element. - */ -export enum ComboboxPopupTypes { +/** Valid popup types for aria-haspopup. */ +export enum PopupTypes { + MENU = 'menu', TREE = 'tree', GRID = 'grid', DIALOG = 'dialog', LISTBOX = 'listbox', } -/** The element that serves as the popup. */ -export interface Popup { - /** Whether the popup is interactive or not. */ - inert: WritableSignalLike; -} - /** Represents the inputs for the PopupControl behavior. */ export interface PopupControlInputs { - /** The element that serves as the popup. */ - popup: SignalLike; - /* Refers to the element that serves as the popup. */ controls: SignalLike; @@ -37,14 +26,10 @@ export interface PopupControlInputs { expanded: WritableSignalLike; /* Corresponds to the popup type. */ - hasPopup: SignalLike; + hasPopup: SignalLike; } -/** - * A behavior that manages the open/close state of a component. - * It provides methods to open, close, and toggle the state, - * which is controlled via a writable signal. - */ +/** 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) {} @@ -52,19 +37,16 @@ export class PopupControl { /** Opens the popup by setting the expanded state to true. */ open(): void { this.inputs.expanded.set(true); - this.inputs.popup().inert.set(false); } /** Closes the popup by setting the expanded state to false. */ close(): void { this.inputs.expanded.set(false); - this.inputs.popup().inert.set(true); } /** Toggles the popup's expanded state. */ toggle(): void { const expanded = !this.inputs.expanded(); this.inputs.expanded.set(expanded); - this.inputs.popup().inert.set(!expanded); } } From ef7fa64ff65375746b8636109b8727b5e94d9ef2 Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Wed, 16 Jul 2025 16:59:41 -0400 Subject: [PATCH 3/4] fixup! feat(cdk-experimental/ui-patterns): add popup behavior --- src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts index e77b9e6ab7db..742c0abcc481 100644 --- a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts +++ b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.ts @@ -46,7 +46,6 @@ export class PopupControl { /** Toggles the popup's expanded state. */ toggle(): void { - const expanded = !this.inputs.expanded(); - this.inputs.expanded.set(expanded); + this.inputs.expanded.set(!this.inputs.expanded()); } } From f929a9e2e072c551a5fba1688da89e9c35ab059d Mon Sep 17 00:00:00 2001 From: Wagner Maciel Date: Thu, 17 Jul 2025 19:03:29 -0400 Subject: [PATCH 4/4] fixup! feat(cdk-experimental/ui-patterns): add popup behavior --- src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts index 45d82abc980b..cf7795d5eac4 100644 --- a/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts +++ b/src/cdk-experimental/ui-patterns/behaviors/popup/popup.spec.ts @@ -13,7 +13,6 @@ type TestInputs = Partial>; function getPopupControl(inputs: TestInputs = {}): PopupControl { const expanded = inputs.expanded || signal(false); - const popup = signal({inert: signal(!expanded())}); const controls = signal('popup-element-id'); const hasPopup = signal(PopupTypes.LISTBOX);