Skip to content

Commit 17023ff

Browse files
authored
Merge branch 'angular:main' into aria-validation
2 parents 77c3cca + f4e9a87 commit 17023ff

47 files changed

Lines changed: 1228 additions & 171 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

goldens/aria/grid/index.api.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
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';
1110
import { OnDestroy } from '@angular/core';
1211
import { OnInit } from '@angular/core';
1312
import { Signal } from '@angular/core';
@@ -41,7 +40,6 @@ export class Grid implements OnDestroy {
4140
// @public
4241
export class GridCell implements OnInit, OnDestroy {
4342
constructor();
44-
readonly activated: EventEmitter<KeyboardEvent>;
4543
readonly active: Signal<boolean>;
4644
readonly colIndex: _angular_core.InputSignal<number | undefined>;
4745
readonly colSpan: _angular_core.InputSignal<number>;
@@ -62,7 +60,7 @@ export class GridCell implements OnInit, OnDestroy {
6260
readonly tabindex: _angular_core.InputSignal<number | undefined>;
6361
readonly textDirection: _angular_core.WritableSignal<_angular_cdk_bidi.Direction>;
6462
// (undocumented)
65-
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>;
63+
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>;
6664
// (undocumented)
6765
static ɵfac: _angular_core.ɵɵFactoryDeclaration<GridCell, never>;
6866
}

goldens/aria/private/index.api.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,6 @@ export interface GridCellInputs extends GridCell {
301301
colIndex: SignalLike<number | undefined>;
302302
getWidget: (e: Element | null) => GridCellWidgetPattern | undefined;
303303
grid: SignalLike<GridPattern>;
304-
onActivate?: (event: KeyboardEvent) => void;
305304
row: SignalLike<GridRowPattern>;
306305
rowIndex: SignalLike<number | undefined>;
307306
widget: SignalLike<GridCellWidgetPattern | undefined>;
@@ -341,15 +340,19 @@ export interface GridCellWidgetInputs {
341340
disabled: SignalLike<boolean>;
342341
element: SignalLike<HTMLElement>;
343342
focusTarget: SignalLike<ElementResolver<HTMLElement>>;
343+
onActivate?: (event: KeyboardEvent | FocusEvent | undefined) => void;
344+
onDeactivate?: (event: KeyboardEvent | FocusEvent | undefined) => void;
344345
widgetType: SignalLike<'simple' | 'complex' | 'editable'>;
345346
}
346347

347348
// @public
348349
export class GridCellWidgetPattern {
349350
constructor(inputs: GridCellWidgetInputs);
350351
activate(event?: KeyboardEvent | FocusEvent): void;
352+
activationEffect(): void;
351353
readonly active: SignalLike<boolean>;
352354
deactivate(event?: KeyboardEvent | FocusEvent): void;
355+
deactivationEffect(): void;
353356
readonly disabled: SignalLike<boolean>;
354357
readonly element: SignalLike<HTMLElement>;
355358
focus(): void;
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## API Report File for "@angular/aria_simple-combobox_testing"
2+
3+
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
4+
5+
```ts
6+
7+
import { BaseHarnessFilters } from '@angular/cdk/testing';
8+
import { ComponentHarness } from '@angular/cdk/testing';
9+
import { ComponentHarnessConstructor } from '@angular/cdk/testing';
10+
import { ContentContainerComponentHarness } from '@angular/cdk/testing';
11+
import { HarnessLoader } from '@angular/cdk/testing';
12+
import { HarnessPredicate } from '@angular/cdk/testing';
13+
14+
// @public
15+
export class ComboboxHarness extends ContentContainerComponentHarness {
16+
blur(): Promise<void>;
17+
close(): Promise<void>;
18+
focus(): Promise<void>;
19+
getPlaceholder(): Promise<string | null>;
20+
getPopupLoader(): Promise<HarnessLoader>;
21+
getPopupWidget<T extends ComponentHarness>(type: ComponentHarnessConstructor<T> & {
22+
with: (options?: {
23+
selector?: string;
24+
}) => HarnessPredicate<T>;
25+
}): Promise<T>;
26+
protected getRootHarnessLoader(): Promise<HarnessLoader>;
27+
getValue(): Promise<string>;
28+
// (undocumented)
29+
static hostSelector: string;
30+
isDisabled(): Promise<boolean>;
31+
isFocused(): Promise<boolean>;
32+
isOpen(): Promise<boolean>;
33+
open(): Promise<void>;
34+
setValue(value: string): Promise<void>;
35+
static with(options?: ComboboxHarnessFilters): HarnessPredicate<ComboboxHarness>;
36+
}
37+
38+
// @public
39+
export interface ComboboxHarnessFilters extends BaseHarnessFilters {
40+
disabled?: boolean;
41+
placeholder?: string | RegExp;
42+
value?: string | RegExp;
43+
}
44+
45+
// (No @packageDocumentation comment for this package)
46+
47+
```

src/aria/config.bzl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ ARIA_ENTRYPOINTS = [
1010
"menu",
1111
"menu/testing",
1212
"simple-combobox",
13+
"simple-combobox/testing",
1314
"tabs",
1415
"tabs/testing",
1516
"toolbar",

src/aria/grid/grid-cell-widget.ts

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -88,14 +88,16 @@ export class GridCellWidget {
8888
* If a focus target exists then return -1. Unless an override.
8989
*/
9090
protected readonly _tabIndex: Signal<number> = computed(
91-
() => this.tabindex() ?? (this.focusTarget() ? -1 : this._pattern.tabIndex()),
91+
() => this.tabindex() ?? this._pattern.tabIndex(),
9292
);
9393

9494
/** The UI pattern for the grid cell widget. */
9595
readonly _pattern = new GridCellWidgetPattern({
9696
...this,
9797
element: () => this.element,
9898
cell: () => this._cell._pattern,
99+
onActivate: e => this.activated.emit(e),
100+
onDeactivate: e => this.deactivated.emit(e),
99101
});
100102

101103
/** Whether the widget is activated. */
@@ -105,22 +107,11 @@ export class GridCellWidget {
105107

106108
constructor() {
107109
afterRenderEffect({
108-
read: () => {
109-
if (this._pattern.isActivated()) {
110-
const activateEvent = this._pattern.lastActivateEvent();
111-
this.activated.emit(activateEvent);
112-
this._pattern.focus();
113-
}
114-
},
110+
write: () => this._pattern.activationEffect(),
115111
});
116112

117113
afterRenderEffect({
118-
read: () => {
119-
const deactivateEvent = this._pattern.lastDeactivateEvent();
120-
if (deactivateEvent) {
121-
this.deactivated.emit(deactivateEvent);
122-
}
123-
},
114+
write: () => this._pattern.deactivationEffect(),
124115
});
125116
}
126117

src/aria/grid/grid-cell.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,11 @@ import {
1414
contentChild,
1515
Directive,
1616
ElementRef,
17-
EventEmitter,
1817
inject,
1918
input,
2019
model,
2120
OnDestroy,
2221
OnInit,
23-
Output,
2422
Signal,
2523
Renderer2,
2624
} from '@angular/core';
@@ -56,9 +54,6 @@ export class GridCell implements OnInit, OnDestroy {
5654
/** A reference to the host element. */
5755
readonly element = this._elementRef.nativeElement as HTMLElement;
5856

59-
/** Emits when the cell is activated via Enter/Space (simple widgets only). */
60-
@Output() readonly activated = new EventEmitter<KeyboardEvent>();
61-
6257
/** Whether the cell is currently active (focused). */
6358
readonly active = computed(() => this._pattern.active());
6459

@@ -122,7 +117,6 @@ export class GridCell implements OnInit, OnDestroy {
122117
widget: this._widgetPattern,
123118
getWidget: e => this._getWidget(e),
124119
element: () => this.element,
125-
onActivate: e => this.activated.emit(e),
126120
});
127121

128122
constructor() {

src/aria/grid/grid.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -938,6 +938,56 @@ describe('Grid directives', () => {
938938
expect(widgetElement.getAttribute('tabindex')).toBe('-1');
939939
});
940940

941+
it('should emit the activated output on Enter for simple widget', () => {
942+
const gridData = createGridData();
943+
gridData[0].cells[0].widgets = [{id: 'w1', type: 'simple'}];
944+
setupGrid({gridData});
945+
gridInstance._pattern.setDefaultStateEffect();
946+
fixture.detectChanges();
947+
948+
tabIntoGrid();
949+
950+
expect(fixture.componentInstance.onActivated).not.toHaveBeenCalled();
951+
952+
keydown('Enter');
953+
expect(fixture.componentInstance.onActivated).toHaveBeenCalled();
954+
});
955+
956+
it('should emit the activated output on Space for simple widget', () => {
957+
const gridData = createGridData();
958+
gridData[0].cells[0].widgets = [{id: 'w1', type: 'simple'}];
959+
setupGrid({gridData});
960+
gridInstance._pattern.setDefaultStateEffect();
961+
fixture.detectChanges();
962+
963+
tabIntoGrid();
964+
965+
expect(fixture.componentInstance.onActivated).not.toHaveBeenCalled();
966+
967+
keydown(' ');
968+
expect(fixture.componentInstance.onActivated).toHaveBeenCalled();
969+
});
970+
971+
it('should emit the activated output in activedescendant mode when event is dispatched directly to grid', () => {
972+
const gridData = createGridData();
973+
gridData[0].cells[0].widgets = [{id: 'w1', type: 'simple'}];
974+
setupGrid({gridData, focusMode: 'activedescendant'});
975+
gridInstance._pattern.setDefaultStateEffect();
976+
fixture.detectChanges();
977+
978+
expect(fixture.componentInstance.onActivated).not.toHaveBeenCalled();
979+
980+
// Verify standard activedescendant behavior by targeting the CONTAINER directly
981+
const event = new KeyboardEvent('keydown', {
982+
key: 'Enter',
983+
bubbles: true,
984+
});
985+
gridElement.dispatchEvent(event);
986+
fixture.detectChanges();
987+
988+
expect(fixture.componentInstance.onActivated).toHaveBeenCalled();
989+
});
990+
941991
it('should emit the activated output when the widget becomes active', () => {
942992
const gridData = createGridData();
943993
gridData[0].cells[0].widgets = [{id: 'w1', type: 'complex'}];

src/aria/listbox/listbox.spec.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -602,6 +602,17 @@ describe('Listbox', () => {
602602
expect(listboxInstance.value().sort()).toEqual([0, 1, 2]);
603603
});
604604

605+
it('should move selection anchor along with focus during normal non-shift navigation', () => {
606+
setupListbox({multi: true, selectionMode: 'explicit'});
607+
down({shiftKey: true});
608+
expect(listboxInstance.value().sort()).toEqual([0, 1]);
609+
down();
610+
down();
611+
down();
612+
up({shiftKey: true});
613+
expect(listboxInstance.value().sort()).toEqual([0, 1, 3, 4]);
614+
});
615+
605616
it('should toggle selection of all options on Ctrl+A', () => {
606617
setupListbox({multi: true, selectionMode: 'explicit', value: [0]});
607618
keydown('A', {ctrlKey: true});

src/aria/menu/menu-item.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ import type {MenuBar} from './menu-bar';
3131
*
3232
* ```html
3333
* <div ngMenu (itemSelected)="doAction()">
34-
* <div ngMenuItem >Action Item</div>
34+
* <div ngMenuItem>Action Item</div>
3535
* <div ngMenuItem [submenu]="anotherMenu">Submenu Trigger</div>
3636
* </div>
3737
* ```
@@ -49,7 +49,6 @@ import type {MenuBar} from './menu-bar';
4949
'(focusin)': '_pattern.onFocusIn()',
5050
'[attr.tabindex]': '_pattern.tabIndex()',
5151
'[attr.data-active]': 'active()',
52-
'[attr.aria-label]': 'value()',
5352
'[attr.aria-haspopup]': 'hasPopup()',
5453
'[attr.aria-expanded]': 'expanded()',
5554
'[attr.aria-disabled]': '_pattern.disabled()',
@@ -66,7 +65,7 @@ export class MenuItem<V> implements OnInit, OnDestroy {
6665
/** The unique ID of the menu item. */
6766
readonly id = input(inject(_IdGenerator).getId('ng-menu-item-', true));
6867

69-
/** The value of the menu item, used as the default aria-label */
68+
/** The value of the menu item. */
7069
readonly value = input.required<V>();
7170

7271
/** Whether the menu item is disabled. */

0 commit comments

Comments
 (0)