Skip to content

Commit 5cb59cd

Browse files
committed
refactor(aria/combobox): split directives and resolve circular dependencies
Splits up the directives across files and resolve circular dependencies in them.
1 parent 80ef657 commit 5cb59cd

File tree

9 files changed

+279
-226
lines changed

9 files changed

+279
-226
lines changed

goldens/aria/combobox/index.api.md

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ import { ComboboxListboxControls } from '@angular/aria/private';
1111
import { ComboboxPattern } from '@angular/aria/private';
1212
import { ComboboxTreeControls } from '@angular/aria/private';
1313
import * as i1 from '@angular/aria/private';
14-
import { WritableSignal } from '@angular/core';
1514

1615
// @public
1716
export class Combobox<V> {
@@ -43,7 +42,7 @@ export class ComboboxDialog {
4342
readonly combobox: Combobox<any>;
4443
readonly element: HTMLElement;
4544
// (undocumented)
46-
_pattern: ComboboxDialogPattern;
45+
_pattern: ComboboxDialogPattern_2;
4746
// (undocumented)
4847
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<ComboboxDialog, "dialog[ngComboboxDialog]", ["ngComboboxDialog"], {}, {}, never, never, true, [{ directive: typeof ComboboxPopup; inputs: {}; outputs: {}; }]>;
4948
// (undocumented)
@@ -65,7 +64,7 @@ export class ComboboxInput {
6564
// @public
6665
export class ComboboxPopup<V> {
6766
readonly combobox: Combobox<V> | null;
68-
readonly _controls: WritableSignal<ComboboxDialogPattern | ComboboxListboxControls<any, V> | ComboboxTreeControls<any, V> | undefined>;
67+
readonly _controls: _angular_core.WritableSignal<ComboboxListboxControls<any, V> | ComboboxTreeControls<any, V> | ComboboxDialogPattern | undefined>;
6968
// (undocumented)
7069
static ɵdir: _angular_core.ɵɵDirectiveDeclaration<ComboboxPopup<any>, "[ngComboboxPopup]", ["ngComboboxPopup"], {}, {}, never, never, true, never>;
7170
// (undocumented)

goldens/aria/listbox/index.api.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ import { ComboboxListboxControls } from '@angular/aria/private';
1212
import { ComboboxPattern } from '@angular/aria/private';
1313
import { ComboboxTreeControls } from '@angular/aria/private';
1414
import * as i1 from '@angular/aria/private';
15-
import { WritableSignal } from '@angular/core';
1615

1716
// @public
1817
export class Listbox<V> {
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 {afterRenderEffect, Directive, ElementRef, inject} from '@angular/core';
10+
import {ComboboxDialogPattern} from '../private';
11+
import {Combobox} from './combobox';
12+
import {ComboboxPopup} from './combobox-popup';
13+
14+
/**
15+
* Integrates a native `<dialog>` element with the combobox, allowing for
16+
* a modal or non-modal popup experience. It handles the opening and closing of the dialog
17+
* based on the combobox's expanded state.
18+
*
19+
* ```html
20+
* <ng-template ngComboboxPopupContainer>
21+
* <dialog ngComboboxDialog class="example-dialog">
22+
* <!-- ... dialog content ... -->
23+
* </dialog>
24+
* </ng-template>
25+
* ```
26+
*
27+
* @developerPreview 21.0
28+
*/
29+
@Directive({
30+
selector: 'dialog[ngComboboxDialog]',
31+
exportAs: 'ngComboboxDialog',
32+
host: {
33+
'[attr.data-open]': 'combobox._pattern.expanded()',
34+
'(keydown)': '_pattern.onKeydown($event)',
35+
'(click)': '_pattern.onClick($event)',
36+
},
37+
hostDirectives: [ComboboxPopup],
38+
})
39+
export class ComboboxDialog {
40+
/** The dialog element. */
41+
private readonly _elementRef = inject(ElementRef<HTMLDialogElement>);
42+
43+
/** A reference to the dialog element. */
44+
readonly element = this._elementRef.nativeElement as HTMLElement;
45+
46+
/** The combobox that the dialog belongs to. */
47+
readonly combobox = inject(Combobox);
48+
49+
/** A reference to the parent combobox popup, if one exists. */
50+
private readonly _popup = inject<ComboboxPopup<unknown>>(ComboboxPopup, {
51+
optional: true,
52+
});
53+
54+
_pattern: ComboboxDialogPattern;
55+
56+
constructor() {
57+
this._pattern = new ComboboxDialogPattern({
58+
id: () => '',
59+
element: () => this._elementRef.nativeElement,
60+
combobox: this.combobox._pattern,
61+
});
62+
63+
if (this._popup) {
64+
this._popup._controls.set(this._pattern);
65+
}
66+
67+
afterRenderEffect(() => {
68+
if (this._elementRef) {
69+
this.combobox._pattern.expanded()
70+
? this._elementRef.nativeElement.showModal()
71+
: this._elementRef.nativeElement.close();
72+
}
73+
});
74+
}
75+
76+
close() {
77+
this._popup?.combobox?._pattern.close();
78+
}
79+
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
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 {
10+
afterRenderEffect,
11+
Directive,
12+
ElementRef,
13+
inject,
14+
model,
15+
untracked,
16+
WritableSignal,
17+
} from '@angular/core';
18+
import {ComboboxDialogPattern} from '../private';
19+
import {Combobox} from './combobox';
20+
21+
/**
22+
* An input that is part of a combobox. It is responsible for displaying the
23+
* current value and handling user input for filtering and selection.
24+
*
25+
* This directive should be applied to an `<input>` element within an `ngCombobox`
26+
* container. It automatically handles keyboard interactions, such as opening the
27+
* popup and navigating through the options.
28+
*
29+
* ```html
30+
* <input
31+
* ngComboboxInput
32+
* placeholder="Search..."
33+
* [(value)]="searchString"
34+
* />
35+
* ```
36+
*
37+
* @developerPreview 21.0
38+
*/
39+
@Directive({
40+
selector: 'input[ngComboboxInput]',
41+
exportAs: 'ngComboboxInput',
42+
host: {
43+
'role': 'combobox',
44+
'[value]': 'value()',
45+
'[attr.aria-disabled]': 'combobox._pattern.disabled()',
46+
'[attr.aria-expanded]': 'combobox._pattern.expanded()',
47+
'[attr.aria-activedescendant]': 'combobox._pattern.activeDescendant()',
48+
'[attr.aria-controls]': 'combobox._pattern.popupId()',
49+
'[attr.aria-haspopup]': 'combobox._pattern.hasPopup()',
50+
'[attr.aria-autocomplete]': 'combobox._pattern.autocomplete()',
51+
'[attr.readonly]': 'combobox._pattern.readonly()',
52+
},
53+
})
54+
export class ComboboxInput {
55+
/** The element that the combobox is attached to. */
56+
private readonly _elementRef = inject<ElementRef<HTMLInputElement>>(ElementRef);
57+
58+
/** A reference to the input element. */
59+
readonly element = this._elementRef.nativeElement as HTMLElement;
60+
61+
/** The combobox that the input belongs to. */
62+
readonly combobox = inject(Combobox);
63+
64+
/** The value of the input. */
65+
value = model<string>('');
66+
67+
constructor() {
68+
(this.combobox._pattern.inputs.inputEl as WritableSignal<HTMLInputElement>).set(
69+
this._elementRef.nativeElement,
70+
);
71+
this.combobox._pattern.inputs.inputValue = this.value;
72+
73+
const controls = this.combobox.popup()?._controls();
74+
if (controls instanceof ComboboxDialogPattern) {
75+
return;
76+
}
77+
78+
/** Focuses & selects the first item in the combobox if the user changes the input value. */
79+
afterRenderEffect(() => {
80+
this.value();
81+
controls?.items();
82+
untracked(() => this.combobox._pattern.onFilter());
83+
});
84+
}
85+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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} from '@angular/core';
10+
import {DeferredContent} from '@angular/aria/private';
11+
12+
/**
13+
* A structural directive that marks the `ng-template` to be used as the popup
14+
* for a combobox. This content is conditionally rendered.
15+
*
16+
* The content of the popup can be a `ngListbox`, `ngTree`, or `role="dialog"`, allowing for
17+
* flexible and complex combobox implementations. The consumer is responsible for
18+
* implementing the filtering logic based on the `ngComboboxInput`'s value.
19+
*
20+
* ```html
21+
* <ng-template ngComboboxPopupContainer>
22+
* <div ngListbox [(value)]="selectedValue">
23+
* <!-- ... options ... -->
24+
* </div>
25+
* </ng-template>
26+
* ```
27+
*
28+
* When using CdkOverlay, this directive can be replaced by `cdkConnectedOverlay`.
29+
*
30+
* ```html
31+
* <ng-template
32+
* [cdkConnectedOverlay]="{origin: inputElement, usePopover: 'inline' matchWidth: true}"
33+
* [cdkConnectedOverlayOpen]="combobox.expanded()">
34+
* <div ngListbox [(value)]="selectedValue">
35+
* <!-- ... options ... -->
36+
* </div>
37+
* </ng-template>
38+
* ```
39+
*
40+
* @developerPreview 21.0
41+
*/
42+
@Directive({
43+
selector: 'ng-template[ngComboboxPopupContainer]',
44+
exportAs: 'ngComboboxPopupContainer',
45+
hostDirectives: [DeferredContent],
46+
})
47+
export class ComboboxPopupContainer {}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
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, inject, signal} from '@angular/core';
10+
import {
11+
ComboboxListboxControls,
12+
ComboboxTreeControls,
13+
ComboboxDialogPattern,
14+
} from '@angular/aria/private';
15+
import type {Combobox} from './combobox';
16+
import {COMBOBOX} from './combobox-tokens';
17+
18+
/**
19+
* Identifies an element as a popup for an `ngCombobox`.
20+
*
21+
* This directive acts as a bridge, allowing the `ngCombobox` to discover and interact
22+
* with the underlying control (e.g., `ngListbox`, `ngTree`, or `ngComboboxDialog`) that
23+
* manages the options. It's primarily used as a host directive and is responsible for
24+
* exposing the popup's control pattern to the parent combobox.
25+
*
26+
* @developerPreview 21.0
27+
*/
28+
@Directive({
29+
selector: '[ngComboboxPopup]',
30+
exportAs: 'ngComboboxPopup',
31+
})
32+
export class ComboboxPopup<V> {
33+
/** The combobox that the popup belongs to. */
34+
readonly combobox = inject<Combobox<V>>(COMBOBOX, {optional: true});
35+
36+
/** The popup controls exposed to the combobox. */
37+
readonly _controls = signal<
38+
| ComboboxListboxControls<any, V>
39+
| ComboboxTreeControls<any, V>
40+
| ComboboxDialogPattern
41+
| undefined
42+
>(undefined);
43+
}
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 {Combobox} from './combobox';
11+
12+
/** Token used to provide the combobox to child components. */
13+
export const COMBOBOX = new InjectionToken<Combobox<unknown>>('COMBOBOX');

0 commit comments

Comments
 (0)