docs(docs-infra): preselect search text on re-open (#61129)

PR Close #61129
This commit is contained in:
Matthieu Riegler 2025-05-05 23:20:26 +02:00 committed by Alex Rickabaugh
parent 223ca9f983
commit d1501bce5c
6 changed files with 38 additions and 15 deletions

View file

@ -3,7 +3,7 @@
<docs-text-field
[autofocus]="true"
[hideIcon]="true"
[(ngModel)]="searchQuery"
[formControl]="searchControl"
[resetLabel]="'Clear the search'"
class="docs-search-input"
placeholder="Search docs"

View file

@ -25,7 +25,7 @@ import {ClickOutside} from '../../directives/index';
import {Search} from '../../services/index';
import {TextField} from '../text-field/text-field.component';
import {FormsModule} from '@angular/forms';
import {FormControl, ReactiveFormsModule} from '@angular/forms';
import {ActiveDescendantKeyManager} from '@angular/cdk/a11y';
import {SearchItem} from '../../directives/search-item/search-item.directive';
import {takeUntilDestroyed} from '@angular/core/rxjs-interop';
@ -40,7 +40,7 @@ import {RelativeLink} from '../../pipes/relative-link.pipe';
imports: [
ClickOutside,
TextField,
FormsModule,
ReactiveFormsModule,
SearchItem,
AlgoliaIcon,
RelativeLink,
@ -53,6 +53,7 @@ export class SearchDialog implements OnDestroy {
onClose = output();
dialog = viewChild.required<ElementRef<HTMLDialogElement>>('searchDialog');
items = viewChildren(SearchItem);
textField = viewChild(TextField);
private readonly search = inject(Search);
private readonly relativeLink = new RelativeLink();
@ -67,7 +68,18 @@ export class SearchDialog implements OnDestroy {
searchQuery = this.search.searchQuery;
searchResults = this.search.searchResults;
// We use a FormControl instead of relying on NgModel+signal to avoid
// the issue https://github.com/angular/angular/issues/13568
// TODO: Use signal forms when available
searchControl = new FormControl(this.searchQuery(), {nonNullable: true});
constructor() {
this.searchControl.valueChanges.pipe(takeUntilDestroyed()).subscribe((value) => {
this.searchQuery.set(value);
});
// Thinkig about refactoring this to a single afterRenderEffect ?
// Answer: It won't have the same behavior
effect(() => {
this.items();
afterNextRender(
@ -87,6 +99,9 @@ export class SearchDialog implements OnDestroy {
if (!this.dialog().nativeElement.open) {
this.dialog().nativeElement.showModal?.();
}
// We want to select the pre-existing text on opening
// In order to change the search input with minimal user interaction.
this.textField()?.input().nativeElement.select();
},
});

View file

@ -6,8 +6,8 @@
type="text"
[attr.placeholder]="placeholder()"
[attr.name]="name()"
[ngModel]="value()"
(ngModelChange)="setValue($event)"
[value]="value()"
(input)="setValue(inputRef.value)"
class="docs-text-field"
/>

View file

@ -28,4 +28,13 @@ describe('TextField', () => {
it('should create', () => {
expect(component).toBeTruthy();
});
it('should update DOM when setting the value via the CVA', () => {
component.setValue('test');
fixture.detectChanges();
expect(fixture.nativeElement.querySelector('input').value).toBe('test');
// If we were using ngModel instead of the value binding, we would get an empty string
// because of https://github.com/angular/angular/issues/13568
});
});

View file

@ -17,6 +17,8 @@ import {SearchResultItem} from '../../interfaces';
},
})
export class SearchItem implements Highlightable {
// Those inputs are required by the Highlightable interface
// We can't migrate them to signals yet
@Input() item?: SearchResultItem;
@Input() disabled = false;

View file

@ -22,15 +22,12 @@ export default class MainComponent {
search = model<string | undefined>('');
constructor() {
effect(
() => {
const search = this.search();
if (search !== undefined) {
this.displaySearchDialog.set(true);
this.searchService.searchQuery.set(search);
}
},
{allowSignalWrites: true},
);
effect(() => {
const search = this.search();
if (search !== undefined) {
this.displaySearchDialog.set(true);
this.searchService.searchQuery.set(search);
}
});
}
}