From acc053ca7a1be238c80e6e964b7e9c46ec9503bb Mon Sep 17 00:00:00 2001 From: Phil Merrell Date: Wed, 30 Sep 2026 09:22:19 -0600 Subject: [PATCH] fix(model-dropdown): rotate the chevron when the menu opens The (opened)/(closed) bindings sat on the cdkMenu panel. CdkMenu has no opened output, so Angular bound a plain DOM listener that never fired: the open flag never became true and the trigger's chevron never rotated. Move both handlers to the trigger as (cdkMenuOpened)/(cdkMenuClosed), the CdkMenuTrigger outputs, and hold the open state in a signal. Specs cover the chevron rotating on open and resetting on close, both from a second trigger click and from choosing a model. Co-Authored-By: Claude Opus 5.5 --- .../model-dropdown.component.spec.ts | 41 +++++++++++++++++++ .../model-dropdown.component.ts | 20 +++++---- 2 files changed, 53 insertions(+), 8 deletions(-) diff --git a/frontend/ai.client/src/app/components/model-dropdown/model-dropdown.component.spec.ts b/frontend/ai.client/src/app/components/model-dropdown/model-dropdown.component.spec.ts index 734356474..9605e2855 100644 --- a/frontend/ai.client/src/app/components/model-dropdown/model-dropdown.component.spec.ts +++ b/frontend/ai.client/src/app/components/model-dropdown/model-dropdown.component.spec.ts @@ -270,6 +270,47 @@ describe('ModelDropdownComponent', () => { expect(setEffort).toHaveBeenCalledWith('high'); }); + it('rotates the trigger chevron while the menu is open', () => { + // The open state comes from the trigger's cdkMenuOpened/cdkMenuClosed + // outputs. CdkMenu has no `opened` output, so an `(opened)` binding on the + // panel compiles to a DOM listener that never fires and the chevron sticks. + const { fixture } = setup(); + const trigger: HTMLButtonElement = fixture.nativeElement.querySelector( + 'button[aria-label="Select model"]', + ); + const chevron = () => trigger.querySelector('svg')!; + expect(chevron().classList).not.toContain('rotate-180'); + + trigger.click(); + fixture.detectChanges(); + expect(menuItems().length).toBeGreaterThan(0); + expect(chevron().classList).toContain('rotate-180'); + + // A second click on the trigger toggles the menu closed. + trigger.click(); + fixture.detectChanges(); + expect(menuItems().length).toBe(0); + expect(chevron().classList).not.toContain('rotate-180'); + }); + + it('un-rotates the chevron when choosing a model closes the menu', () => { + const { fixture, setSelectedModel } = setup({ + featured: [ + makeModel({ id: 'a', modelId: 'a', modelName: 'Alpha' }), + makeModel({ id: 'b', modelId: 'b', modelName: 'Beta' }), + ], + }); + const chevron = () => + fixture.nativeElement.querySelector('button[aria-label="Select model"] svg') as SVGElement; + openMenu(fixture); + expect(chevron().classList).toContain('rotate-180'); + + itemLabelled('Beta')!.click(); + fixture.detectChanges(); + expect(setSelectedModel).toHaveBeenCalled(); + expect(chevron().classList).not.toContain('rotate-180'); + }); + it('locks the picker to a plain label when an agent pins the model', () => { const { fixture, modelService } = setup(); modelService.agentModelLocked.set(true); diff --git a/frontend/ai.client/src/app/components/model-dropdown/model-dropdown.component.ts b/frontend/ai.client/src/app/components/model-dropdown/model-dropdown.component.ts index 4b0114856..4a8cd149b 100644 --- a/frontend/ai.client/src/app/components/model-dropdown/model-dropdown.component.ts +++ b/frontend/ai.client/src/app/components/model-dropdown/model-dropdown.component.ts @@ -1,4 +1,4 @@ -import { Component, ChangeDetectionStrategy, computed, inject, input } from '@angular/core'; +import { Component, ChangeDetectionStrategy, computed, inject, input, signal } from '@angular/core'; import { Router } from '@angular/router'; import { CdkMenuTrigger, CdkMenu, CdkMenuItem } from '@angular/cdk/menu'; import { ConnectedPosition } from '@angular/cdk/overlay'; @@ -35,6 +35,8 @@ import { ModelOptionComponent } from './components/model-option.component'; type="button" [cdkMenuTriggerFor]="modelMenu" [cdkMenuPosition]="menuPositions()" + (cdkMenuOpened)="onMenuOpened()" + (cdkMenuClosed)="onMenuClosed()" [class]="triggerSizeClass()" class="flex items-center rounded-lg text-gray-600 transition-colors hover:bg-gray-100 hover:text-gray-700 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[var(--color-primary)] dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-300" aria-label="Select model" @@ -65,8 +67,6 @@ import { ModelOptionComponent } from './components/model-option.component';