Skip to content

Commit b2c275f

Browse files
committed
refactor(multiple): use output for grid cell activation
Replaced manual clicks with an activated output on GridCell and updated examples. Also cleaned up an unused input that snuck in from a bad rebase.
1 parent cdc9582 commit b2c275f

11 files changed

Lines changed: 75 additions & 41 deletions

File tree

goldens/aria/grid/index.api.md

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import * as _angular_cdk_bidi from '@angular/cdk/bidi';
88
import * as _angular_core from '@angular/core';
99
import { ElementRef } from '@angular/core';
10+
import { EventEmitter } from '@angular/core';
1011
import { Signal } from '@angular/core';
1112

1213
// @public
@@ -16,7 +17,6 @@ export class Grid {
1617
readonly colWrap: _angular_core.InputSignal<"continuous" | "loop" | "nowrap">;
1718
readonly disabled: _angular_core.InputSignalWithTransform<boolean, unknown>;
1819
readonly element: HTMLElement;
19-
readonly enableRangeSelection: _angular_core.InputSignalWithTransform<boolean, unknown>;
2020
readonly enableSelection: _angular_core.InputSignalWithTransform<boolean, unknown>;
2121
readonly focusMode: _angular_core.InputSignal<"roving" | "activedescendant">;
2222
readonly multi: _angular_core.InputSignalWithTransform<boolean, unknown>;
@@ -28,14 +28,15 @@ export class Grid {
2828
readonly tabIndex: _angular_core.InputSignalWithTransform<number | undefined, string | number | undefined>;
2929
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
3030
// (undocumented)
31-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<Grid, "[ngGrid]", ["ngGrid"], { "enableSelection": { "alias": "enableSelection"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "focusMode": { "alias": "focusMode"; "required": false; "isSignal": true; }; "rowWrap": { "alias": "rowWrap"; "required": false; "isSignal": true; }; "colWrap": { "alias": "colWrap"; "required": false; "isSignal": true; }; "multi": { "alias": "multi"; "required": false; "isSignal": true; }; "selectionMode": { "alias": "selectionMode"; "required": false; "isSignal": true; }; "enableRangeSelection": { "alias": "enableRangeSelection"; "required": false; "isSignal": true; }; "tabIndex": { "alias": "tabIndex"; "required": false; "isSignal": true; }; }, {}, ["_rows"], never, true, never>;
31+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<Grid, "[ngGrid]", ["ngGrid"], { "enableSelection": { "alias": "enableSelection"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "softDisabled": { "alias": "softDisabled"; "required": false; "isSignal": true; }; "focusMode": { "alias": "focusMode"; "required": false; "isSignal": true; }; "rowWrap": { "alias": "rowWrap"; "required": false; "isSignal": true; }; "colWrap": { "alias": "colWrap"; "required": false; "isSignal": true; }; "multi": { "alias": "multi"; "required": false; "isSignal": true; }; "selectionMode": { "alias": "selectionMode"; "required": false; "isSignal": true; }; "tabIndex": { "alias": "tabIndex"; "required": false; "isSignal": true; }; }, {}, ["_rows"], never, true, never>;
3232
// (undocumented)
3333
static ɵfac: _angular_core.ɵɵFactoryDeclaration<Grid, never>;
3434
}
3535

3636
// @public
3737
export class GridCell {
3838
constructor();
39+
readonly activated: EventEmitter<KeyboardEvent>;
3940
readonly active: Signal<boolean>;
4041
readonly colIndex: _angular_core.InputSignal<number | undefined>;
4142
readonly colSpan: _angular_core.InputSignal<number>;
@@ -52,7 +53,7 @@ export class GridCell {
5253
readonly tabindex: _angular_core.InputSignal<number | undefined>;
5354
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
5455
// (undocumented)
55-
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<GridCell, "[ngGridCell]", ["ngGridCell"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "role": { "alias": "role"; "required": false; "isSignal": true; }; "rowSpan": { "alias": "rowSpan"; "required": false; "isSignal": true; }; "colSpan": { "alias": "colSpan"; "required": false; "isSignal": true; }; "rowIndex": { "alias": "rowIndex"; "required": false; "isSignal": true; }; "colIndex": { "alias": "colIndex"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "selected": { "alias": "selected"; "required": false; "isSignal": true; }; "selectable": { "alias": "selectable"; "required": false; "isSignal": true; }; "tabindex": { "alias": "tabindex"; "required": false; "isSignal": true; }; }, { "selected": "selectedChange"; }, ["_widget"], never, true, never>;
56+
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<GridCell, "[ngGridCell]", ["ngGridCell"], { "id": { "alias": "id"; "required": false; "isSignal": true; }; "role": { "alias": "role"; "required": false; "isSignal": true; }; "rowSpan": { "alias": "rowSpan"; "required": false; "isSignal": true; }; "colSpan": { "alias": "colSpan"; "required": false; "isSignal": true; }; "rowIndex": { "alias": "rowIndex"; "required": false; "isSignal": true; }; "colIndex": { "alias": "colIndex"; "required": false; "isSignal": true; }; "disabled": { "alias": "disabled"; "required": false; "isSignal": true; }; "selected": { "alias": "selected"; "required": false; "isSignal": true; }; "selectable": { "alias": "selectable"; "required": false; "isSignal": true; }; "tabindex": { "alias": "tabindex"; "required": false; "isSignal": true; }; }, { "activated": "activated"; "selected": "selectedChange"; }, ["_widget"], never, true, never>;
5657
// (undocumented)
5758
static ɵfac: _angular_core.ɵɵFactoryDeclaration<GridCell, never>;
5859
}

goldens/aria/private/index.api.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -300,6 +300,7 @@ export interface GridCellInputs extends GridCell {
300300
colIndex: SignalLike<number | undefined>;
301301
getWidget: (e: Element | null) => GridCellWidgetPattern | undefined;
302302
grid: SignalLike<GridPattern>;
303+
onActivate?: (event: KeyboardEvent) => void;
303304
row: SignalLike<GridRowPattern>;
304305
rowIndex: SignalLike<number | undefined>;
305306
widget: SignalLike<GridCellWidgetPattern | undefined>;

src/aria/grid/grid-cell.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,11 @@ import {
1414
contentChild,
1515
Directive,
1616
ElementRef,
17+
EventEmitter,
1718
inject,
1819
input,
1920
model,
21+
Output,
2022
Signal,
2123
Renderer2,
2224
} from '@angular/core';
@@ -52,6 +54,9 @@ export class GridCell {
5254
/** A reference to the host element. */
5355
readonly element = this._elementRef.nativeElement as HTMLElement;
5456

57+
/** Emits when the cell is activated via Enter/Space (simple widgets only). */
58+
@Output() readonly activated = new EventEmitter<KeyboardEvent>();
59+
5560
/** Whether the cell is currently active (focused). */
5661
readonly active = computed(() => this._pattern.active());
5762

@@ -115,6 +120,7 @@ export class GridCell {
115120
widget: this._widgetPattern,
116121
getWidget: e => this._getWidget(e),
117122
element: () => this.element,
123+
onActivate: e => this.activated.emit(e),
118124
});
119125

120126
constructor() {

src/aria/grid/grid.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,9 +121,6 @@ export class Grid {
121121
*/
122122
readonly selectionMode = input<'follow' | 'explicit'>('follow');
123123

124-
/** Whether enable range selections (with modifier keys or dragging). */
125-
readonly enableRangeSelection = input(false, {transform: booleanAttribute});
126-
127124
/** The tabindex of the grid. */
128125
readonly tabIndex = input(undefined, {
129126
transform: (v: string | number | undefined) =>

src/aria/private/grid/cell.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export interface GridCellInputs extends GridCell {
3636

3737
/** A function that returns the cell widget associated with a given element. */
3838
getWidget: (e: Element | null) => GridCellWidgetPattern | undefined;
39+
40+
/** Callback when the cell is activated via Enter/Space. */
41+
onActivate?: (event: KeyboardEvent) => void;
3942
}
4043

4144
/** The UI pattern for a grid cell. */
@@ -117,6 +120,12 @@ export class GridCellPattern implements GridCell {
117120
onKeydown(event: KeyboardEvent): void {
118121
if (this.disabled()) return;
119122
this.widget()?.onKeydown(event);
123+
124+
if (this.widget()?.inputs.widgetType() === 'simple') {
125+
if (event.key === 'Enter' || event.key === ' ') {
126+
this.inputs.onActivate?.(event);
127+
}
128+
}
120129
}
121130

122131
/** Handles focusin events for the cell. */

src/aria/private/grid/grid.spec.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -274,16 +274,6 @@ describe('Grid', () => {
274274
expect(widget.isActivated()).toBe(true);
275275
});
276276

277-
it('should trigger click on Enter for simple widget', () => {
278-
const {grid} = createGrid([{cells: [{widget: {widgetType: 'simple'}}]}], gridInputs);
279-
const widget = grid.cells()[0][0].inputs.widget()!;
280-
const element = widget.element();
281-
spyOn(element, 'click');
282-
283-
widget.onKeydown(enter());
284-
expect(element.click).toHaveBeenCalled();
285-
});
286-
287277
it('should not activate if disabled', () => {
288278
const {grid} = createGrid(
289279
[{cells: [{widget: {widgetType: 'complex', disabled: true}}]}],
@@ -375,6 +365,50 @@ describe('Grid', () => {
375365
cell.onKeydown(event);
376366
expect(widget.onKeydown).toHaveBeenCalledWith(event);
377367
});
368+
369+
it('should call onActivate on Enter for simple widget', () => {
370+
const onActivateSpy = jasmine.createSpy('onActivate');
371+
const {grid} = createGrid([{cells: [{widget: {widgetType: 'simple'}}]}], gridInputs);
372+
const cell = grid.cells()[0][0];
373+
(cell.inputs as any).onActivate = onActivateSpy;
374+
375+
const event = enter();
376+
cell.onKeydown(event);
377+
expect(onActivateSpy).toHaveBeenCalledWith(event);
378+
});
379+
380+
it('should call onActivate on Space for simple widget', () => {
381+
const onActivateSpy = jasmine.createSpy('onActivate');
382+
const {grid} = createGrid([{cells: [{widget: {widgetType: 'simple'}}]}], gridInputs);
383+
const cell = grid.cells()[0][0];
384+
(cell.inputs as any).onActivate = onActivateSpy;
385+
386+
const event = space();
387+
cell.onKeydown(event);
388+
expect(onActivateSpy).toHaveBeenCalledWith(event);
389+
});
390+
391+
it('should NOT call onActivate for complex widget', () => {
392+
const onActivateSpy = jasmine.createSpy('onActivate');
393+
const {grid} = createGrid([{cells: [{widget: {widgetType: 'complex'}}]}], gridInputs);
394+
const cell = grid.cells()[0][0];
395+
(cell.inputs as any).onActivate = onActivateSpy;
396+
397+
const event = enter();
398+
cell.onKeydown(event);
399+
expect(onActivateSpy).not.toHaveBeenCalled();
400+
});
401+
402+
it('should NOT call onActivate for other keys', () => {
403+
const onActivateSpy = jasmine.createSpy('onActivate');
404+
const {grid} = createGrid([{cells: [{widget: {widgetType: 'simple'}}]}], gridInputs);
405+
const cell = grid.cells()[0][0];
406+
(cell.inputs as any).onActivate = onActivateSpy;
407+
408+
const event = up();
409+
cell.onKeydown(event);
410+
expect(onActivateSpy).not.toHaveBeenCalled();
411+
});
378412
});
379413
});
380414

src/aria/private/grid/widget.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -71,14 +71,6 @@ export class GridCellWidgetPattern {
7171
readonly keydown = computed(() => {
7272
const manager = new KeyboardEventManager();
7373

74-
// Simple widget does not need to pause default grid behaviors.
75-
// However, it does need to capture Enter key and trigger a click on the host element
76-
// since the browser won't do it for us in activedescendant mode.
77-
if (this.inputs.widgetType() === 'simple') {
78-
manager.on('Enter', () => this.element().click());
79-
return manager;
80-
}
81-
8274
// If a widget is activated, only listen to events that exits activate state.
8375
if (this.isActivated()) {
8476
manager.on('Escape', e => {

src/aria/simple-combobox/simple-combobox.spec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1434,12 +1434,12 @@ const states = [
14341434
<div ngComboboxWidget #grid="ngGrid" ngGrid id="grid" focusMode="activedescendant" [tabIndex]="-1" colWrap="continuous" [activeDescendant]="grid.activeDescendant()">
14351435
@for (item of filteredItems(); track item; let i = $index) {
14361436
<div ngGridRow>
1437-
<div ngGridCell [id]="item + '-label'" [rowIndex]="i" [colIndex]="0">
1437+
<div ngGridCell [id]="item + '-label'" [rowIndex]="i" [colIndex]="0" (activated)="selectItem(item)">
14381438
<button ngGridCellWidget (click)="selectItem(item)">
14391439
{{item}}
14401440
</button>
14411441
</div>
1442-
<div ngGridCell [id]="item + '-delete'" [rowIndex]="i" [colIndex]="1">
1442+
<div ngGridCell [id]="item + '-delete'" [rowIndex]="i" [colIndex]="1" (activated)="removeItem(item)">
14431443
<button ngGridCellWidget (click)="removeItem(item)" (pointerdown)="$event.preventDefault()">
14441444
Delete
14451445
</button>

src/components-examples/aria/simple-combobox/simple-combobox-datepicker/simple-combobox-datepicker-example.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
<ng-template [cdkConnectedOverlay]="{origin, usePopover: 'inline', matchWidth: false}"
1010
[cdkConnectedOverlayOpen]="popupExpanded()">
11-
<ng-template ngComboboxPopup [combobox]="combobox" popupType="grid">
11+
<ng-template ngComboboxPopup [combobox]="combobox" popupType="dialog">
1212
<div class="example-popover">
1313
<div ngComboboxWidget class="example-datepicker-popup" cdkTrapFocus (keydown)="handleWidgetKeydown($event)">
1414
<div class="example-datepicker-header">
@@ -44,7 +44,7 @@
4444
}
4545

4646
@for (day of week; track $index) {
47-
<td class="example-datepicker-cell" ngGridCell [(selected)]="day.selected" (click)="selectDate(day)">
47+
<td class="example-datepicker-cell" ngGridCell [(selected)]="day.selected" (click)="selectDate(day)" (keydown.enter)="selectDate(day)" (keydown.space)="selectDate(day)">
4848
<button ngGridCellWidget class="example-datepicker-day-button" [attr.aria-label]="day.ariaLabel">
4949
{{ day.displayName }}
5050
</button>

src/components-examples/aria/simple-combobox/simple-combobox-datepicker/simple-combobox-datepicker-example.ts

Lines changed: 5 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
signal,
1414
Signal,
1515
computed,
16-
untracked,
1716
viewChild,
1817
ElementRef,
1918
} from '@angular/core';
@@ -102,9 +101,10 @@ export class SimpleComboboxDatepickerExample<D> {
102101
});
103102
return weekdays.slice(firstDayOfWeek).concat(weekdays.slice(0, firstDayOfWeek));
104103
});
105-
readonly weeks: Signal<CalendarCell<D>[][]> = computed(() =>
106-
this._createWeekCells(this.viewMonth()),
107-
);
104+
readonly weeks: Signal<CalendarCell<D>[][]> = computed(() => {
105+
this._activeDate(); // Create dependency on active date
106+
return this._createWeekCells(this.viewMonth());
107+
});
108108

109109
nextMonth(): void {
110110
this.viewMonth.set(this._dateAdapter.addCalendarMonths(this.viewMonth(), 1));
@@ -132,7 +132,6 @@ export class SimpleComboboxDatepickerExample<D> {
132132
this._activeDate.set(parsedDate);
133133
this.viewMonth.set(parsedDate);
134134
this.popupExpanded.set(false);
135-
event.stopPropagation();
136135
}
137136
} else if (event.key === 'ArrowDown' && this.popupExpanded()) {
138137
setTimeout(() => {
@@ -180,12 +179,7 @@ export class SimpleComboboxDatepickerExample<D> {
180179
displayName: dateNames[i],
181180
ariaLabel,
182181
date,
183-
selected: signal(
184-
this._dateAdapter.compareDate(
185-
date,
186-
untracked(() => this._activeDate()),
187-
) === 0,
188-
),
182+
selected: signal(this._dateAdapter.compareDate(date, this._activeDate()) === 0),
189183
});
190184
}
191185
return weeks;

0 commit comments

Comments
 (0)