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
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,16 @@ export interface FeatureTogglesInterface {
*/
siteIsolationForCustomLoginPage?: boolean;

/**
* When enabled, the navigation menu buttons (e.g. "My Account") and dropdown
* headers activate on spacebar key *release* (`keyup`) rather than key *press*
* (`keydown`), preventing the action from firing repeatedly while the key is
* held down.
* Fixes WCAG 2.5.2 (Pointer Cancellation) ACC-270.16 (Level A).
* Affects: `NavigationUIComponent`, 'NativeSelectSpaceDirective'
*/
a11yNavigationSpaceKeyOnKeyUp?: boolean;

/**
* When enabled, the storefront's active theme follows the `theme` field of
* the active base site (configured in SAP Commerce BackOffice). The theme
Expand Down Expand Up @@ -715,6 +725,7 @@ export const defaultFeatureToggles: Required<FeatureTogglesInterface> = {
asyncAuthConfigInitializer: false,
siteIsolationForCustomLoginPage: false,
applyBaseSiteThemeFromCms: false,
a11yNavigationSpaceKeyOnKeyUp: false,
b2bCheckoutShippingAddressFilter: false,
improvedTabStyling: false,
productConfiguratorConsolidatedButtonDisabling: false,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,66 @@
<label *ngIf="(items$ | async)?.length > 1 && (items$ | async) as items">
<span>{{ label$ | async | cxTranslate }}:</span>
<div *cxFeature="'a11ySiteContextCaretClick'" class="cx-select-wrapper">
<select (change)="active = $any($event).target.value">
<option
[attr.aria-label]="ariaLabel$(item.label, i, items.length) | async"
*ngFor="let item of items; index as i"
value="{{ item.isocode }}"
[attr.selected]="(activeItem$ | async) === item.isocode ? '' : null"
>
{{ item.label }}
</option>
</select>
<cx-icon
aria-hidden="true"
[type]="iconTypes.CARET_DOWN"
class="small"
></cx-icon>
</div>
<ng-container *cxFeature="'!a11ySiteContextCaretClick'">
<select (change)="active = $any($event).target.value">
<option
[attr.aria-label]="ariaLabel$(item.label, i, items.length) | async"
*ngFor="let item of items; index as i"
value="{{ item.isocode }}"
[attr.selected]="(activeItem$ | async) === item.isocode ? '' : null"
>
{{ item.label }}
</option>
</select>
<cx-icon
aria-hidden="true"
[type]="iconTypes.CARET_DOWN"
class="small"
></cx-icon>
<ng-container *ngIf="items$ | async as items">
<ng-container *ngIf="items.length > 1">
<ng-container *cxFeature="'a11yNavigationSpaceKeyOnKeyUp'">
<div class="cx-ng-select-label">
<span>{{ label$ | async | cxTranslate }}:</span>
<ng-select
[searchable]="false"
[clearable]="false"
[items]="items"
bindValue="isocode"
bindLabel="label"
[ngModel]="selectedItem"
(change)="active = $event?.isocode"
ariaLabelDropdown="{{
'common.ngSelectDropdownOptionsList' | cxTranslate
}}"
[cxNgSelectA11y]="{ ariaLabel: label$ | async | cxTranslate }"
></ng-select>
</div>
</ng-container>

<label *cxFeature="'!a11yNavigationSpaceKeyOnKeyUp'">
<span>{{ label$ | async | cxTranslate }}:</span>
<div *cxFeature="'a11ySiteContextCaretClick'" class="cx-select-wrapper">
<select
cxNativeSelectSpace
(change)="active = $any($event).target.value"
>
<option
[attr.aria-label]="ariaLabel$(item.label, i, items.length) | async"
*ngFor="let item of items; index as i"
value="{{ item.isocode }}"
[attr.selected]="(activeItem$ | async) === item.isocode ? '' : null"
>
{{ item.label }}
</option>
</select>
<cx-icon
aria-hidden="true"
[type]="iconTypes.CARET_DOWN"
class="small"
></cx-icon>
</div>
<ng-container *cxFeature="'!a11ySiteContextCaretClick'">
<select
cxNativeSelectSpace
(change)="active = $any($event).target.value"
>
<option
[attr.aria-label]="ariaLabel$(item.label, i, items.length) | async"
*ngFor="let item of items; index as i"
value="{{ item.isocode }}"
[attr.selected]="(activeItem$ | async) === item.isocode ? '' : null"
>
{{ item.label }}
</option>
</select>
<cx-icon
aria-hidden="true"
[type]="iconTypes.CARET_DOWN"
class="small"
></cx-icon>
</ng-container>
</label>
</ng-container>
</label>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,16 @@
import { AsyncPipe, NgFor, NgIf } from '@angular/common';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef,
inject,
Input,
OnInit,
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule } from '@angular/forms';
import { NgSelectComponent } from '@ng-select/ng-select';
import {
FeatureDirective,
I18nModule,
Expand All @@ -19,6 +25,8 @@ import {
useFeatureStyles,
} from '@spartacus/core';
import { map, Observable } from 'rxjs';
import { NativeSelectSpaceDirective } from '../../../layout/a11y/native-select-space/native-select-space.directive';
import { NgSelectA11yDirective } from '../../../shared/components/ng-select-a11y/ng-select-a11y.directive';
import { IconComponent } from '../icon/icon.component';
import { ICON_TYPE } from '../icon/icon.model';
import { SiteContextComponentService } from './site-context-component.service';
Expand All @@ -35,9 +43,13 @@ import { SiteContextType } from './site-context.model';
AsyncPipe,
I18nModule,
FeatureDirective,
NativeSelectSpaceDirective,
NgSelectComponent,
NgSelectA11yDirective,
FormsModule,
],
})
export class SiteContextSelectorComponent {
export class SiteContextSelectorComponent implements OnInit {
/**
* @deprecated since 2011.21 removed unused property
*/
Expand All @@ -50,26 +62,36 @@ export class SiteContextSelectorComponent {
@Input() context: SiteContextType;

protected translationService = inject(TranslationService);
private destroyRef = inject(DestroyRef);
private cdr = inject(ChangeDetectorRef);

selectedItem: string | undefined;

constructor(private componentService: SiteContextComponentService) {
useFeatureStyles('a11ySiteContextCaretClick');
useFeatureStyles('a11yNavigationSpaceKeyOnKeyUp');
}

get items$(): Observable<any> {
return this.componentService.getItems(this.context);
ngOnInit(): void {
this.items$ = this.componentService.getItems(this.context);
this.activeItem$ = this.componentService.getActiveItem(this.context);
this.label$ = this.componentService.getLabel(this.context);
this.activeItem$
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => {
this.selectedItem = value;
this.cdr.markForCheck();
});
}

get activeItem$(): Observable<string> {
return this.componentService.getActiveItem(this.context);
}
items$!: Observable<any>;
activeItem$!: Observable<string>;

set active(value: string) {
this.componentService.setActive(value, this.context);
}

get label$(): Observable<any> {
return this.componentService.getLabel(this.context);
}
label$!: Observable<any>;

ariaLabel$(label: string, index: number, length: number): Observable<string> {
return this.translationService.translate('common.of').pipe(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,37 +1,76 @@
<label *ngIf="items$ | async as themes">
<span> {{ 'siteThemeSwitcher.theme' | cxTranslate }}:</span>
<div *cxFeature="'a11ySiteContextCaretClick'" class="cx-select-wrapper">
<select (change)="activeItem = $any($event).target.value">
<option
[attr.aria-label]="ariaLabel$(theme, i, themes.length) | async"
*ngFor="let theme of themes; index as i"
value="{{ theme.className }}"
[selected]="(activeItem$ | async) === theme.className"
>
{{
theme.i18nNameKey
? (theme.i18nNameKey | cxTranslate)
: theme.className
}}
</option>
</select>
<cx-icon aria-hidden="true" [type]="iconTypes.CARET_DOWN" class="small" />
</div>
<ng-container *cxFeature="'!a11ySiteContextCaretClick'">
<select (change)="activeItem = $any($event).target.value">
<option
[attr.aria-label]="ariaLabel$(theme, i, themes.length) | async"
*ngFor="let theme of themes; index as i"
value="{{ theme.className }}"
[selected]="(activeItem$ | async) === theme.className"
>
{{
theme.i18nNameKey
? (theme.i18nNameKey | cxTranslate)
: theme.className
}}
</option>
</select>
<cx-icon aria-hidden="true" [type]="iconTypes.CARET_DOWN" class="small" />
<ng-container *ngIf="items$ | async as themes">
<ng-container *ngIf="themes.length">
<ng-container *cxFeature="'a11yNavigationSpaceKeyOnKeyUp'">
<div class="cx-ng-select-label">
<span>{{ 'siteThemeSwitcher.theme' | cxTranslate }}:</span>
<ng-select
[searchable]="false"
[clearable]="false"
[items]="(translatedItems$ | async) ?? []"
bindValue="className"
bindLabel="label"
[ngModel]="selectedItem"
(change)="activeItem = $event?.className"
ariaLabelDropdown="{{
'common.ngSelectDropdownOptionsList' | cxTranslate
}}"
[cxNgSelectA11y]="{
ariaLabel: 'siteThemeSwitcher.theme' | cxTranslate,
}"
></ng-select>
</div>
</ng-container>

<label *cxFeature="'!a11yNavigationSpaceKeyOnKeyUp'">
<span> {{ 'siteThemeSwitcher.theme' | cxTranslate }}:</span>
<div *cxFeature="'a11ySiteContextCaretClick'" class="cx-select-wrapper">
<select
cxNativeSelectSpace
(change)="activeItem = $any($event).target.value"
>
<option
[attr.aria-label]="ariaLabel$(theme, i, themes.length) | async"
*ngFor="let theme of themes; index as i"
value="{{ theme.className }}"
[selected]="(activeItem$ | async) === theme.className"
>
{{
theme.i18nNameKey
? (theme.i18nNameKey | cxTranslate)
: theme.className
}}
</option>
</select>
<cx-icon
aria-hidden="true"
[type]="iconTypes.CARET_DOWN"
class="small"
/>
</div>
<ng-container *cxFeature="'!a11ySiteContextCaretClick'">
<select
cxNativeSelectSpace
(change)="activeItem = $any($event).target.value"
>
<option
[attr.aria-label]="ariaLabel$(theme, i, themes.length) | async"
*ngFor="let theme of themes; index as i"
value="{{ theme.className }}"
[selected]="(activeItem$ | async) === theme.className"
>
{{
theme.i18nNameKey
? (theme.i18nNameKey | cxTranslate)
: theme.className
}}
</option>
</select>
<cx-icon
aria-hidden="true"
[type]="iconTypes.CARET_DOWN"
class="small"
/>
</ng-container>
</label>
</ng-container>
</label>
</ng-container>
Loading
Loading