Skip to content

Commit d8d026c

Browse files
bloveclaude
andauthored
feat(chat): chat-launcher-button emits (clicked) output (#354)
The inner <button> now directly emits a `clicked` output, replacing the previous pattern where consumers bound `(click)` to the host element and relied on event bubbling. The bubbling approach worked visually but was fragile under hit-testing — Playwright (and potentially screen readers / pointer-event detection in higher stacking contexts) could mis-target the wrapping host element when a higher-z-index sibling overlapped the launcher's bounding box. Migrated internal consumers (chat-sidebar, chat-popup) to (clicked). Back-compat: native (click) bindings on the host still work — the click event still bubbles unchanged. External consumers don't need to migrate, but should prefer (clicked) for new code. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 8267899 commit d8d026c

5 files changed

Lines changed: 45 additions & 6 deletions

File tree

apps/website/content/docs/chat/api/api-docs.json

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2801,7 +2801,14 @@
28012801
"description": "",
28022802
"params": [],
28032803
"examples": [],
2804-
"properties": [],
2804+
"properties": [
2805+
{
2806+
"name": "clicked",
2807+
"type": "OutputEmitterRef<void>",
2808+
"description": "Fires when the inner <button> receives a click. Prefer this over\n binding `(click)` on the host element — explicit output gives\n consumers (and Playwright) an unambiguous click target that won't\n be intercepted by sibling overlays in higher stacking contexts.\n Native `(click)` on the host still works for back-compat: the\n click event bubbles through unchanged.",
2809+
"optional": false
2810+
}
2811+
],
28052812
"methods": []
28062813
},
28072814
{

libs/chat/src/lib/compositions/chat-popup/chat-popup.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens';
5959
`],
6060
template: `
6161
<div class="chat-popup__launcher">
62-
<chat-launcher-button (click)="toggle()" />
62+
<chat-launcher-button (clicked)="toggle()" />
6363
</div>
6464
<div class="chat-popup__window" [attr.data-open]="open() ? 'true' : 'false'" role="dialog" aria-modal="false">
6565
<button type="button" class="chat-popup__close" (click)="closeWindow()" aria-label="Close chat">

libs/chat/src/lib/compositions/chat-sidebar/chat-sidebar.component.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,7 @@ import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens';
6767
template: `
6868
<div class="chat-sidebar__content"><ng-content /></div>
6969
<div class="chat-sidebar__launcher">
70-
<chat-launcher-button (click)="toggle()" />
70+
<chat-launcher-button (clicked)="toggle()" />
7171
</div>
7272
<aside class="chat-sidebar__panel" [attr.data-open]="open() ? 'true' : 'false'" role="complementary" [attr.aria-hidden]="open() ? 'false' : 'true'">
7373
<button type="button" class="chat-sidebar__close" (click)="closeWindow()" aria-label="Close chat">

libs/chat/src/lib/primitives/chat-launcher-button/chat-launcher-button.component.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,28 @@ describe('ChatLauncherButtonComponent', () => {
2121
const svg = (fx.nativeElement as HTMLElement).querySelector('.chat-launcher-button svg');
2222
expect(svg).toBeTruthy();
2323
});
24+
25+
it('emits clicked output when the inner button is clicked', () => {
26+
TestBed.configureTestingModule({});
27+
const fx = TestBed.createComponent(ChatLauncherButtonComponent);
28+
fx.detectChanges();
29+
let fired = 0;
30+
fx.componentInstance.clicked.subscribe(() => fired++);
31+
const btn = (fx.nativeElement as HTMLElement).querySelector('button.chat-launcher-button') as HTMLButtonElement;
32+
btn.click();
33+
expect(fired).toBe(1);
34+
});
35+
36+
it('keeps native (click) on the host working — event bubbles through', () => {
37+
// Back-compat: existing consumers that bind `(click)` on
38+
// <chat-launcher-button> must continue to fire on inner-button clicks.
39+
TestBed.configureTestingModule({});
40+
const fx = TestBed.createComponent(ChatLauncherButtonComponent);
41+
fx.detectChanges();
42+
let hostClicks = 0;
43+
fx.nativeElement.addEventListener('click', () => hostClicks++);
44+
const btn = (fx.nativeElement as HTMLElement).querySelector('button.chat-launcher-button') as HTMLButtonElement;
45+
btn.click();
46+
expect(hostClicks).toBe(1);
47+
});
2448
});
Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// SPDX-License-Identifier: MIT
2-
import { Component, ChangeDetectionStrategy } from '@angular/core';
2+
import { Component, ChangeDetectionStrategy, output } from '@angular/core';
33
import { CHAT_HOST_TOKENS } from '../../styles/chat-tokens';
44
import { CHAT_LAUNCHER_BUTTON_STYLES } from '../../styles/chat-launcher-button.styles';
55

@@ -9,11 +9,19 @@ import { CHAT_LAUNCHER_BUTTON_STYLES } from '../../styles/chat-launcher-button.s
99
changeDetection: ChangeDetectionStrategy.OnPush,
1010
styles: [CHAT_HOST_TOKENS, CHAT_LAUNCHER_BUTTON_STYLES],
1111
template: `
12-
<button type="button" class="chat-launcher-button" aria-label="Open chat">
12+
<button type="button" class="chat-launcher-button" aria-label="Open chat" (click)="clicked.emit()">
1313
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
1414
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
1515
</svg>
1616
</button>
1717
`,
1818
})
19-
export class ChatLauncherButtonComponent {}
19+
export class ChatLauncherButtonComponent {
20+
/** Fires when the inner <button> receives a click. Prefer this over
21+
* binding `(click)` on the host element — explicit output gives
22+
* consumers (and Playwright) an unambiguous click target that won't
23+
* be intercepted by sibling overlays in higher stacking contexts.
24+
* Native `(click)` on the host still works for back-compat: the
25+
* click event bubbles through unchanged. */
26+
readonly clicked = output<void>();
27+
}

0 commit comments

Comments
 (0)