Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 18 additions & 9 deletions src/app/search-navbar/search-navbar.component.html
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
<div [title]="'nav.search' | translate" (dsClickOutside)="collapse()">
<div class="d-inline-block position-relative">
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on" class="d-flex">
<input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query"
formControlName="query" type="text" placeholder="{{searchExpanded ? ('nav.search' | translate) : ''}}"
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-end p1"
[class.display]="searchExpanded ? 'inline-block' : 'none'"
[tabIndex]="searchExpanded ? 0 : -1"
[attr.data-test]="'header-search-box' | dsBrowserOnly">
<button class="submit-icon btn btn-link btn-link-inline" [attr.aria-label]="'nav.search.button' | translate" type="button" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly" tabindex="0" role="button">
@if (!searchExpanded) {
<button type="button" class="search-toggle btn btn-link btn-link-inline"
[attr.aria-label]="'nav.search.button' | translate"
(click)="expand()"
[attr.data-test]="'header-search-icon' | dsBrowserOnly">
<em class="fas fa-search fa-lg fa-fw"></em>
</button>
</form>
} @else {
<form [formGroup]="searchForm" (ngSubmit)="onSubmit(searchForm.value)" autocomplete="on" class="d-flex">
<input #searchInput [@toggleAnimation]="isExpanded" [attr.aria-label]="('nav.search' | translate)" name="query"
formControlName="query" type="text" placeholder="{{'nav.search' | translate}}"
class="d-inline-block bg-transparent position-absolute form-control dropdown-menu-end p1"
[attr.data-test]="'header-search-box' | dsBrowserOnly">
<button type="submit" class="submit-icon btn btn-link btn-link-inline"
[attr.aria-label]="'nav.search.button' | translate"
[attr.data-test]="'header-search-icon' | dsBrowserOnly">
<em class="fas fa-search fa-lg fa-fw"></em>
</button>
</form>
}
</div>
</div>
40 changes: 28 additions & 12 deletions src/app/search-navbar/search-navbar.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,13 +70,24 @@ describe('SearchNavbarComponent', () => {
expect(component).toBeTruthy();
});

describe('when you click on search icon', () => {
describe('when collapsed', () => {
it('should render only the toggle <button> with no surrounding <form>', () => {
const form = fixture.debugElement.query(By.css('form'));
const toggle = fixture.debugElement.query(By.css('button.search-toggle'));
expect(form).toBeNull();
expect(toggle).not.toBeNull();
expect(toggle.nativeElement.tagName).toBe('BUTTON');
expect(toggle.nativeElement.getAttribute('type')).toBe('button');
});
});

describe('when you click on the search toggle', () => {
beforeEach(fakeAsync(() => {
spyOn(component, 'expand').and.callThrough();
spyOn(component, 'onSubmit').and.callThrough();
spyOn(router, 'navigate');
const searchIcon = fixture.debugElement.query(By.css('form .submit-icon'));
searchIcon.triggerEventHandler('click', {
const toggle = fixture.debugElement.query(By.css('button.search-toggle'));
toggle.triggerEventHandler('click', {
preventDefault: () => {/**/
},
});
Expand All @@ -86,16 +97,22 @@ describe('SearchNavbarComponent', () => {

it('input expands', () => {
expect(component.expand).toHaveBeenCalled();
expect(component.searchExpanded).toBeTrue();
});

it('renders the form with a real submit button', () => {
const form = fixture.debugElement.query(By.css('form'));
const submit = fixture.debugElement.query(By.css('form button.submit-icon'));
expect(form).not.toBeNull();
expect(submit).not.toBeNull();
expect(submit.nativeElement.getAttribute('type')).toBe('submit');
});

describe('empty query', () => {
describe('press submit button', () => {
beforeEach(fakeAsync(() => {
const searchIcon = fixture.debugElement.query(By.css('form .submit-icon'));
searchIcon.triggerEventHandler('click', {
preventDefault: () => {/**/
},
});
const form = fixture.debugElement.query(By.css('form'));
form.triggerEventHandler('submit', null);
tick();
fixture.detectChanges();
}));
Expand All @@ -117,17 +134,16 @@ describe('SearchNavbarComponent', () => {
searchInput.nativeElement.dispatchEvent(new Event('input'));
fixture.detectChanges();
});
describe('press submit button', () => {
describe('press Enter on the input (native submit)', () => {
beforeEach(fakeAsync(() => {
const searchIcon = fixture.debugElement.query(By.css('form .submit-icon'));
searchIcon.triggerEventHandler('click', null);
const form = fixture.debugElement.query(By.css('form'));
form.triggerEventHandler('submit', null);
tick();
fixture.detectChanges();
}));
it('to search page with query', async () => {
const extras: NavigationExtras = { queryParams: { query: 'test' } };
expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' });

expect(router.navigate).toHaveBeenCalledWith(['search'], extras);
});
});
Expand Down
45 changes: 34 additions & 11 deletions src/app/search-navbar/search-navbar.component.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import {
AfterViewInit,
ChangeDetectorRef,
Component,
ElementRef,
ViewChild,
Expand All @@ -17,7 +19,13 @@
import { ClickOutsideDirective } from '../shared/utils/click-outside.directive';

/**
* The search box in the header that expands on focus and collapses on focus out
* The search box in the header that expands on focus and collapses on focus out.
*
* When collapsed, only a single icon `<button>` is rendered (no form). The form, input
* and submit button are only rendered while the search bar is expanded; this avoids the
* Pa11y/HTMLCS rule WCAG2AA H32.2 ("This form does not contain a submit button"), which
* is otherwise reported on every page because the collapsed-state form had a
* `type="button"` icon control rather than a real submit button.
*/
@Component({
selector: 'ds-base-search-navbar',
Expand All @@ -32,57 +40,72 @@
TranslateModule,
],
})
export class SearchNavbarComponent {
export class SearchNavbarComponent implements AfterViewInit {

// The search form
searchForm;
// Whether or not the search bar is expanded, boolean for html ngIf, string for AngularAnimation state change
searchExpanded = false;
isExpanded = 'collapsed';

// Search input field
// Search input field. Only present once the form is rendered (`searchExpanded === true`).
@ViewChild('searchInput') searchField: ElementRef;

constructor(private formBuilder: UntypedFormBuilder, private router: Router, private searchService: SearchService) {
constructor(
private formBuilder: UntypedFormBuilder,
private router: Router,
private searchService: SearchService,
private cdr: ChangeDetectorRef,
) {
this.searchForm = this.formBuilder.group(({
query: '',
}));
}

ngAfterViewInit(): void {

Check failure on line 65 in src/app/search-navbar/search-navbar.component.ts

View workflow job for this annotation

GitHub Actions / tests (22.x)

Lifecycle methods should not be empty

Check failure on line 65 in src/app/search-navbar/search-navbar.component.ts

View workflow job for this annotation

GitHub Actions / tests (20.x)

Lifecycle methods should not be empty
// No-op; here so the @ViewChild reference is wired up after each render pass when
// the form is conditionally added/removed from the DOM.
}

/**
* Expands search bar by angular animation, see expandSearchInput
* Expands the search bar and focuses the input on the next change-detection pass
* (the input only exists once `searchExpanded` is true).
*/
expand() {
this.searchExpanded = true;
this.isExpanded = 'expanded';
// Force a synchronous render so `searchField` is populated, then focus it.
this.cdr.detectChanges();
this.editSearch();
}

/**
* Collapses & blurs search bar by angular animation, see expandSearchInput
* Collapses & blurs the search bar. The form, input and submit button are removed
* from the DOM by the template's `@if (!searchExpanded)` branch.
*/
collapse() {
this.searchField.nativeElement.blur();
this.searchField?.nativeElement?.blur();
this.searchExpanded = false;
this.isExpanded = 'collapsed';
}

/**
* Focuses on input search bar so search can be edited
* Focuses the input search bar so it can be edited.
*/
editSearch(): void {
this.searchField.nativeElement.focus();
this.searchField?.nativeElement?.focus();
}

/**
* Submits the search (on enter or on search icon click)
* Submits the search. Triggered both by Enter on the input (native form submit) and
* by clicking the magnifying-glass icon (`<button type="submit">` inside the form).
* @param data Data for the searchForm, containing the search query
*/
onSubmit(data: any) {
this.collapse();
const queryParams = Object.assign({}, data);
const linkToNavigateTo = [this.searchService.getSearchLink().replace('/', '')];
this.searchForm.reset();
this.collapse();

this.router.navigate(linkToNavigateTo, {
queryParams: queryParams,
Expand Down
Loading