Skip to content

Commit 57de1ff

Browse files
committed
refactor(aria/combobox): split files to avoid circular dependencies
1 parent c8314e3 commit 57de1ff

8 files changed

Lines changed: 196 additions & 142 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ export class SimpleComboboxPattern {
9292
);
9393

9494
/** The keydown event manager for the combobox. */
95+
// TODO(tjshiu): Allow combo keys in combobox (#33101).
9596
keydown = computed(() => {
9697
const manager = new KeyboardEventManager();
9798

src/aria/simple-combobox/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,4 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
export {Combobox, ComboboxPopup, ComboboxWidget} from './simple-combobox';
9+
export * from './public-api';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
export {Combobox} from './simple-combobox';
10+
export {ComboboxPopup} from './simple-combobox-popup';
11+
export {ComboboxWidget} from './simple-combobox-widget';
12+
13+
// This needs to be re-exported, because it's used by the combobox components.
14+
// See: https://github.com/angular/components/issues/30663.
15+
export {
16+
DeferredContent as ɵɵDeferredContent,
17+
DeferredContentAware as ɵɵDeferredContentAware,
18+
} from '../private';
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {computed, Directive, inject, input, OnDestroy, OnInit, signal} from '@angular/core';
10+
import {DeferredContent, SimpleComboboxPopupPattern} from '@angular/aria/private';
11+
import type {Combobox} from './simple-combobox';
12+
import type {ComboboxWidget} from './simple-combobox-widget';
13+
import {SIMPLE_COMBOBOX_POPUP} from './simple-combobox-tokens';
14+
15+
/**
16+
* A structural directive that marks the `ng-template` to be used as the popup
17+
* for a combobox. This content is conditionally rendered.
18+
*
19+
* The content of the popup can be any element with the `ngComboboxWidget` directive.
20+
*
21+
* ```html
22+
* <ng-template ngComboboxPopup>
23+
* <div ngComboboxWidget>
24+
* <!-- ... options ... -->
25+
* </div>
26+
* </ng-template>
27+
* ```
28+
*/
29+
@Directive({
30+
selector: 'ng-template[ngComboboxPopup]',
31+
exportAs: 'ngComboboxPopup',
32+
hostDirectives: [DeferredContent],
33+
providers: [{provide: SIMPLE_COMBOBOX_POPUP, useExisting: ComboboxPopup}],
34+
})
35+
export class ComboboxPopup implements OnInit, OnDestroy {
36+
private readonly _deferredContent = inject(DeferredContent);
37+
38+
/** The combobox that the popup belongs to. */
39+
readonly combobox = input.required<Combobox>();
40+
41+
/** The widget contained within the popup. */
42+
readonly _widget = signal<ComboboxWidget | undefined>(undefined);
43+
44+
/** The element that serves as the control target for the popup. */
45+
readonly controlTarget = computed(() => this._widget()?.element);
46+
47+
/** The ID of the popup. */
48+
readonly popupId = computed(() => this._widget()?.popupId());
49+
50+
/** The ID of the active descendant in the popup. */
51+
readonly activeDescendant = computed(() => this._widget()?.activeDescendant());
52+
53+
/** The type of the popup (e.g., listbox, tree, grid, dialog). */
54+
readonly popupType = input<'listbox' | 'tree' | 'grid' | 'dialog'>('listbox');
55+
56+
/** The popup pattern. */
57+
readonly _pattern = new SimpleComboboxPopupPattern({
58+
...this,
59+
});
60+
61+
ngOnInit() {
62+
this.combobox()._registerPopup(this);
63+
this._deferredContent.deferredContentAware.set(this.combobox());
64+
}
65+
66+
ngOnDestroy() {
67+
this.combobox()._unregisterPopup();
68+
}
69+
70+
/** Registers a widget with the popup. */
71+
_registerWidget(widget: ComboboxWidget) {
72+
this._widget.set(widget);
73+
}
74+
75+
/** Unregisters the widget from the popup. */
76+
_unregisterWidget() {
77+
this._widget.set(undefined);
78+
}
79+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {InjectionToken} from '@angular/core';
10+
import type {ComboboxPopup} from './simple-combobox-popup';
11+
12+
/** Token used to expose the combobox popup. */
13+
export const SIMPLE_COMBOBOX_POPUP = new InjectionToken<ComboboxPopup>('SIMPLE_COMBOBOX_POPUP');
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import {Directive, ElementRef, inject, input, OnDestroy, OnInit, signal} from '@angular/core';
10+
import {SIMPLE_COMBOBOX_POPUP} from './simple-combobox-tokens';
11+
12+
/**
13+
* Identifies an element as a widget within a combobox popup.
14+
*
15+
* This directive should be applied to the element that contains the options or content
16+
* of the popup. It handles the communication of ID and active descendant information
17+
* to the combobox.
18+
*/
19+
@Directive({
20+
selector: '[ngComboboxWidget]',
21+
exportAs: 'ngComboboxWidget',
22+
host: {
23+
'(focusin)': 'onFocusin()',
24+
'(focusout)': 'onFocusout($event)',
25+
},
26+
})
27+
export class ComboboxWidget implements OnInit, OnDestroy {
28+
/** The element that the popup widget is attached to. */
29+
private readonly _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
30+
private readonly _popup = inject(SIMPLE_COMBOBOX_POPUP);
31+
32+
/** A reference to the popup widget element. */
33+
readonly element = this._elementRef.nativeElement;
34+
35+
/** The ID of the popup widget. */
36+
readonly popupId = signal<string | undefined>(undefined);
37+
38+
/** The ID of the active descendant in the widget. */
39+
readonly activeDescendant = input<string | undefined>(undefined);
40+
41+
private _observer: MutationObserver | undefined;
42+
43+
constructor() {
44+
const el = this.element;
45+
this._observer = new MutationObserver(mutations => {
46+
for (const mutation of mutations) {
47+
if (mutation.attributeName === 'id') {
48+
this.popupId.set(el.id);
49+
}
50+
}
51+
});
52+
53+
this._observer.observe(el, {
54+
attributes: true,
55+
attributeFilter: ['id'],
56+
});
57+
}
58+
59+
ngOnInit() {
60+
this.popupId.set(this.element.id);
61+
this._popup._registerWidget(this);
62+
}
63+
64+
ngOnDestroy(): void {
65+
this._observer?.disconnect();
66+
this._popup._unregisterWidget();
67+
}
68+
69+
/** Handles focus in events for the widget. */
70+
onFocusin() {
71+
this._popup._pattern.onFocusin();
72+
}
73+
74+
/** Handles focus out events for the widget. */
75+
onFocusout(event: FocusEvent) {
76+
this._popup._pattern.onFocusout(event);
77+
}
78+
}

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,10 @@ import {
99
} from '@angular/core';
1010
import {ComponentFixture, TestBed} from '@angular/core/testing';
1111
import {By} from '@angular/platform-browser';
12-
import {Combobox, ComboboxPopup, ComboboxWidget} from './simple-combobox';
12+
import {Combobox} from './simple-combobox';
13+
import {ComboboxPopup} from './simple-combobox-popup';
14+
import {ComboboxWidget} from './simple-combobox-widget';
15+
1316
import {Listbox, Option} from '../listbox';
1417
import {runAccessibilityChecks} from '@angular/cdk/testing/private';
1518
import {Tree, TreeItem, TreeItemGroup} from '../tree';

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

Lines changed: 2 additions & 140 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,12 @@ import {
1515
inject,
1616
input,
1717
model,
18-
OnDestroy,
1918
OnInit,
2019
signal,
2120
Renderer2,
2221
} from '@angular/core';
23-
import {
24-
DeferredContent,
25-
DeferredContentAware,
26-
SimpleComboboxPattern,
27-
SimpleComboboxPopupPattern,
28-
} from '@angular/aria/private';
22+
import {DeferredContentAware, SimpleComboboxPattern} from '@angular/aria/private';
23+
import type {ComboboxPopup} from './simple-combobox-popup';
2924

3025
/**
3126
* The container element that wraps a combobox input and popup, and orchestrates its behavior.
@@ -133,136 +128,3 @@ export class Combobox extends DeferredContentAware implements OnInit {
133128
this._popup.set(undefined);
134129
}
135130
}
136-
137-
/**
138-
* A structural directive that marks the `ng-template` to be used as the popup
139-
* for a combobox. This content is conditionally rendered.
140-
*
141-
* The content of the popup can be any element with the `ngComboboxWidget` directive.
142-
*
143-
* ```html
144-
* <ng-template ngComboboxPopup>
145-
* <div ngComboboxWidget>
146-
* <!-- ... options ... -->
147-
* </div>
148-
* </ng-template>
149-
* ```
150-
*/
151-
@Directive({
152-
selector: 'ng-template[ngComboboxPopup]',
153-
exportAs: 'ngComboboxPopup',
154-
hostDirectives: [DeferredContent],
155-
})
156-
export class ComboboxPopup implements OnInit, OnDestroy {
157-
private readonly _deferredContent = inject(DeferredContent);
158-
159-
/** The combobox that the popup belongs to. */
160-
readonly combobox = input.required<Combobox>();
161-
162-
/** The widget contained within the popup. */
163-
readonly _widget = signal<ComboboxWidget | undefined>(undefined);
164-
165-
/** The element that serves as the control target for the popup. */
166-
readonly controlTarget = computed(() => this._widget()?.element);
167-
168-
/** The ID of the popup. */
169-
readonly popupId = computed(() => this._widget()?.popupId());
170-
171-
/** The ID of the active descendant in the popup. */
172-
readonly activeDescendant = computed(() => this._widget()?.activeDescendant());
173-
174-
/** The type of the popup (e.g., listbox, tree, grid, dialog). */
175-
readonly popupType = input<'listbox' | 'tree' | 'grid' | 'dialog'>('listbox');
176-
177-
/** The popup pattern. */
178-
readonly _pattern = new SimpleComboboxPopupPattern({
179-
...this,
180-
});
181-
182-
ngOnInit() {
183-
this.combobox()._registerPopup(this);
184-
this._deferredContent.deferredContentAware.set(this.combobox());
185-
}
186-
187-
ngOnDestroy() {
188-
this.combobox()._unregisterPopup();
189-
}
190-
191-
/** Registers a widget with the popup. */
192-
_registerWidget(widget: ComboboxWidget) {
193-
this._widget.set(widget);
194-
}
195-
196-
/** Unregisters the widget from the popup. */
197-
_unregisterWidget() {
198-
this._widget.set(undefined);
199-
}
200-
}
201-
202-
/**
203-
* Identifies an element as a widget within a combobox popup.
204-
*
205-
* This directive should be applied to the element that contains the options or content
206-
* of the popup. It handles the communication of ID and active descendant information
207-
* to the combobox.
208-
*/
209-
@Directive({
210-
selector: '[ngComboboxWidget]',
211-
exportAs: 'ngComboboxWidget',
212-
host: {
213-
'(focusin)': 'onFocusin()',
214-
'(focusout)': 'onFocusout($event)',
215-
},
216-
})
217-
export class ComboboxWidget implements OnInit, OnDestroy {
218-
/** The element that the popup widget is attached to. */
219-
private readonly _elementRef = inject<ElementRef<HTMLElement>>(ElementRef);
220-
private readonly _popup = inject(ComboboxPopup);
221-
222-
/** A reference to the popup widget element. */
223-
readonly element = this._elementRef.nativeElement;
224-
225-
/** The ID of the popup widget. */
226-
readonly popupId = signal<string | undefined>(undefined);
227-
228-
/** The ID of the active descendant in the widget. */
229-
readonly activeDescendant = input<string | undefined>(undefined);
230-
231-
private _observer: MutationObserver | undefined;
232-
233-
constructor() {
234-
const el = this.element;
235-
this._observer = new MutationObserver(mutations => {
236-
for (const mutation of mutations) {
237-
if (mutation.attributeName === 'id') {
238-
this.popupId.set(el.id);
239-
}
240-
}
241-
});
242-
243-
this._observer.observe(el, {
244-
attributes: true,
245-
attributeFilter: ['id'],
246-
});
247-
}
248-
249-
ngOnInit() {
250-
this.popupId.set(this.element.id);
251-
this._popup._registerWidget(this);
252-
}
253-
254-
ngOnDestroy(): void {
255-
this._observer?.disconnect();
256-
this._popup._unregisterWidget();
257-
}
258-
259-
/** Handles focus in events for the widget. */
260-
onFocusin() {
261-
this._popup._pattern.onFocusin();
262-
}
263-
264-
/** Handles focus out events for the widget. */
265-
onFocusout(event: FocusEvent) {
266-
this._popup._pattern.onFocusout(event);
267-
}
268-
}

0 commit comments

Comments
 (0)