Skip to content

Commit

Permalink
feat(a11y): Redesign searchbox to include label (#19222)
Browse files Browse the repository at this point in the history
  • Loading branch information
sdrozdsap authored Nov 4, 2024
1 parent 49572f5 commit 22ece42
Show file tree
Hide file tree
Showing 6 changed files with 320 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,11 @@ export interface FeatureTogglesInterface {
*/
a11yUnitsListKeyboardControls?: boolean;

/**
* Adds label to the `SearchBoxComponent` search input
*/
a11ySearchboxLabel?: boolean;

/**
* When set to `true`, product titles in `CartItemComponent`, `QuickOrderItemComponent`, `WishListItemComponent`
* adopt a more link-like style, appearing blue with an underline. This enhances visual cues for clickable elements,
Expand Down Expand Up @@ -744,6 +749,7 @@ export const defaultFeatureToggles: Required<FeatureTogglesInterface> = {
a11yFacetKeyboardNavigation: false,
a11yUnitsListKeyboardControls: true,
a11yCartItemsLinksStyles: true,
a11ySearchboxLabel: false,
a11yHideSelectBtnForSelectedAddrOrPayment: false,
a11yFocusableCarouselControls: true,
a11yUseTrapTabInsteadOfTrapInDialogs: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,7 @@ if (environment.cpq) {
a11ySearchBoxMobileFocus: true,
a11yFacetKeyboardNavigation: true,
a11yUnitsListKeyboardControls: true,
a11ySearchboxLabel: true,
a11yCartItemsLinksStyles: true,
a11yHideSelectBtnForSelectedAddrOrPayment: true,
a11yFocusableCarouselControls: true,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
<div [attr.aria-label]="'searchBox.productSearch' | cxTranslate" role="search">
<div
*cxFeature="'!a11ySearchboxLabel'"
[attr.aria-label]="'searchBox.productSearch' | cxTranslate"
role="search"
>
<label class="searchbox" [class.dirty]="!!searchInput.value">
<!-- TODO: (CXSPA-6929) - Remove feature flag next major release -->
<input
Expand Down Expand Up @@ -57,6 +61,74 @@
</button>
</label>
</div>
<div
*cxFeature="'a11ySearchboxLabel'"
[attr.aria-label]="'searchBox.productSearch' | cxTranslate"
role="search"
class="cx-searchbox-container"
>
<label
class="searchbox cxFeat_a11ySearchboxLabel"
[class.dirty]="!!searchInput?.value"
>
<span class="cx-input-label">{{ 'common.search' | cxTranslate }}</span>
<div class="cx-label-inner-container">
<input
#searchInput
[placeholder]="'searchBox.placeholder' | cxTranslate"
autocomplete="off"
aria-describedby="initialDescription"
aria-controls="results"
[attr.tabindex]="
a11ySearchBoxMobileFocusEnabled ? getTabIndex(isMobile | async) : null
"
[attr.aria-label]="'searchBox.placeholder' | cxTranslate"
(focus)="a11ySearchBoxMobileFocusEnabled ? null : open()"
(click)="open()"
(input)="search(searchInput.value)"
(blur)="close($any($event))"
(keydown.tab)="
a11ySearchBoxMobileFocusEnabled ? close($any($event)) : null
"
(keydown.escape)="close($any($event))"
(keydown.enter)="
close($any($event), true);
launchSearchResult($any($event), searchInput.value);
updateChosenWord(searchInput.value)
"
(keydown.arrowup)="focusPreviousChild($any($event))"
(keydown.arrowdown)="focusNextChild($any($event))"
value="{{ chosenWord }}"
/>

<button
[attr.aria-label]="'common.reset' | cxTranslate"
[title]="'common.reset' | cxTranslate"
(click)="clear(searchInput)"
class="reset"
>
<cx-icon [type]="iconTypes.RESET"></cx-icon>
</button>

<div
role="presentation"
class="search-icon"
[title]="'common.search' | cxTranslate"
>
<cx-icon [type]="iconTypes.SEARCH"></cx-icon>
</div>
</div>
<button
#searchButton
[attr.aria-label]="'common.search' | cxTranslate"
[title]="'common.search' | cxTranslate"
class="search"
(click)="open()"
>
<cx-icon [type]="iconTypes.SEARCH"></cx-icon>
</button>
</label>
</div>

<div
*ngIf="results$ | async as result"
Expand All @@ -79,9 +151,11 @@
{{ 'searchBox.closeSearchPanel' | cxTranslate }}
</button>
<h3 *ngIf="result.message" [innerHTML]="result.message"></h3>

<!--RESULT SUGGESTIONS-->
<div class="suggestions" *ngIf="(searchInput?.value ?? '').length > 0">
<div
class="suggestions"
*ngIf="(searchInputEl?.nativeElement?.value ?? '').length > 0"
>
<ng-container
*ngIf="
isEnabledFeature(searchBoxFeatures.RECENT_SEARCHES_FEATURE) ||
Expand All @@ -106,7 +180,9 @@ <h3>
<li *ngFor="let suggestion of result.suggestions">
<a
role="option"
[innerHTML]="suggestion | cxHighlight: searchInput.value"
[innerHTML]="
suggestion | cxHighlight: searchInputEl?.nativeElement?.value
"
[routerLink]="
{
cxRoute: 'search',
Expand All @@ -123,7 +199,7 @@ <h3>
(mousedown)="preventDefault($event)"
(click)="
dispatchSuggestionEvent({
freeText: searchInput.value,
freeText: searchInputEl?.nativeElement?.value,
selectedSuggestion: suggestion,
searchSuggestions: result.suggestions ?? [],
});
Expand All @@ -138,7 +214,7 @@ <h3>
<!-- TRENDING SEARCHES-->
<div
class="trending-searches-container"
[class.d-block]="searchInput?.value?.length === 0"
[class.d-block]="searchInputEl?.nativeElement?.value?.length === 0"
>
<ng-container *ngIf="searchBoxFeatures.TRENDING_SEARCHES_FEATURE">
<ng-container *cxFeature="searchBoxFeatures.TRENDING_SEARCHES_FEATURE">
Expand All @@ -158,7 +234,7 @@ <h3>
*ngIf="config.recentSearches"
[cxOutlet]="searchBoxOutlets.RECENT_SEARCHES"
[cxOutletContext]="{
search: searchInput.value,
search: searchInputEl?.nativeElement?.value,
searchBoxActive: searchBoxActive,
maxRecentSearches: config.maxRecentSearches,
}"
Expand Down Expand Up @@ -204,7 +280,7 @@ <h3 *ngIf="result.products?.length">
(mousedown)="preventDefault($event)"
(click)="
dispatchProductEvent({
freeText: searchInput.value,
freeText: searchInputEl?.nativeElement?.value,
productCode: product.code,
})
"
Expand Down Expand Up @@ -248,7 +324,7 @@ <h3 *ngIf="result.products?.length">
(mousedown)="preventDefault($event)"
(click)="
dispatchProductEvent({
freeText: searchInput.value,
freeText: searchInputEl?.nativeElement?.value,
productCode: product.code,
})
"
Expand Down Expand Up @@ -288,7 +364,7 @@ <h3 class="suggestions-header">
</h3>
<h3
class="trendingSearches-header"
*ngIf="searchInput?.value?.length === 0"
*ngIf="searchInputEl?.nativeElement?.value?.length === 0"
>
{{ 'cdsTrendingSearches.trendingSearches' | cxTranslate }}
</h3>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import { Component, Input, Pipe, PipeTransform } from '@angular/core';
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import {
Component,
Directive,
Input,
Pipe,
PipeTransform,
} from '@angular/core';
import { By } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
Expand Down Expand Up @@ -34,6 +40,8 @@ import {
SearchBoxSuggestionSelectedEvent,
} from './search-box.events';
import { SearchResults } from './search-box.model';
import { MockFeatureDirective } from 'projects/storefrontlib/shared/test/mock-feature-directive';
import { OutletDirective } from '@spartacus/storefront';

const mockSearchBoxComponentData: CmsSearchBoxComponent = {
uid: '001',
Expand Down Expand Up @@ -90,6 +98,24 @@ class MockMediaComponent {
@Input() alt;
}

@Directive({
selector: '[cxOutlet]',
})
class MockOutletDirective implements Partial<OutletDirective> {
@Input() cxOutlet: string;
@Input() cxOutletContext: string;
}

@Component({
selector: 'cx-carousel',
template: ``,
})
class MockCarouselComponent {
@Input() items: any;
@Input() itemWidth: any;
@Input() template: any;
@Input() hideIndicators: any;
}
const mockRouterState: RouterState = {
nextState: undefined,
state: {
Expand Down Expand Up @@ -168,10 +194,13 @@ describe('SearchBoxComponent', () => {
],
declarations: [
SearchBoxComponent,
MockFeatureDirective,
MockUrlPipe,
MockHighlightPipe,
MockCxIconComponent,
MockMediaComponent,
MockOutletDirective,
MockCarouselComponent,
],
providers: [
{
Expand Down Expand Up @@ -256,8 +285,8 @@ describe('SearchBoxComponent', () => {
});

it('should launch the search page, given it is not an empty search', () => {
const input = fixture.debugElement.query(By.css('.searchbox > input'));

fixture.detectChanges();
const input = fixture.debugElement.query(By.css('.searchbox input'));
input.nativeElement.value = PRODUCT_SEARCH_STRING;
input.triggerEventHandler('keydown.enter', {});

Expand All @@ -267,7 +296,8 @@ describe('SearchBoxComponent', () => {
});

it('should not launch search page on empty search', () => {
const input = fixture.debugElement.query(By.css('.searchbox > input'));
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('.searchbox input'));
input.triggerEventHandler('keydown.enter', {});

fixture.detectChanges();
Expand Down Expand Up @@ -307,7 +337,7 @@ describe('SearchBoxComponent', () => {
selectedSuggestion: 'laptop',
searchSuggestions: [{ value: 'laptop' }, { value: 'camileo' }],
};
searchBoxComponent.searchInput = { nativeElement: inputElement };
searchBoxComponent.searchInputEl = { nativeElement: inputElement };
// Simulate typing a query
searchBoxComponent.search('laptop');

Expand Down Expand Up @@ -363,6 +393,7 @@ describe('SearchBoxComponent', () => {

describe('UI tests', () => {
it('should contain an input text field', () => {
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('input'))).not.toBeNull();
});

Expand Down Expand Up @@ -392,7 +423,7 @@ describe('SearchBoxComponent', () => {
searchBoxComponent.queryText = 'something';
fixture.detectChanges();
const box = fixture.debugElement.query(
By.css('.searchbox > input')
By.css('.searchbox input')
).nativeElement;
box.select();
fixture.debugElement.query(By.css('.reset')).nativeElement.click();
Expand All @@ -413,7 +444,7 @@ describe('SearchBoxComponent', () => {
fixture.detectChanges();
searchBoxComponent.searchBoxActive = true;
const mockSearchInput = fixture.debugElement.query(
By.css('.searchbox > input')
By.css('.searchbox input')
).nativeElement;
spyOn(mockSearchInput, 'focus');

Expand Down Expand Up @@ -471,7 +502,8 @@ describe('SearchBoxComponent', () => {
});

it('should contain chosen word from the dropdown', () => {
const input = fixture.debugElement.query(By.css('.searchbox > input'));
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('.searchbox input'));
mockRouterState.state.context = {
id: 'search',
type: PageType.CONTENT_PAGE,
Expand All @@ -485,11 +517,13 @@ describe('SearchBoxComponent', () => {
});

it('should not contain searched word when navigating to another page', () => {
const input = fixture.debugElement.query(By.css('.searchbox > input'));
fixture.detectChanges();
const input = fixture.debugElement.query(By.css('.searchbox input'));
mockRouterState.state.context = null;
input.nativeElement.value = PRODUCT_SEARCH_STRING;
input.triggerEventHandler('keydown.enter', {});
routerState$.next(mockRouterState);

fixture.detectChanges();
expect(searchBoxComponent.chosenWord).toEqual('');
expect(input.nativeElement.value).toEqual('');
Expand All @@ -502,7 +536,7 @@ describe('SearchBoxComponent', () => {

// Focus should begin on searchbox input
const inputSearchBox: HTMLElement = fixture.debugElement.query(
By.css('.searchbox > input')
By.css('.searchbox input')
).nativeElement;
inputSearchBox.focus();
expect(inputSearchBox).toBe(getFocusedElement());
Expand Down
Loading

0 comments on commit 22ece42

Please sign in to comment.