Skip to content

Commit

Permalink
feat: A11Y Tab Component for PDP (#18948)
Browse files Browse the repository at this point in the history
  • Loading branch information
Zeyber authored Sep 26, 2024
1 parent 0e9ee9d commit ba271cf
Show file tree
Hide file tree
Showing 27 changed files with 1,080 additions and 46 deletions.
3 changes: 2 additions & 1 deletion projects/assets/src/translations/en/product.json
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@
"deliveryTab": "Shipping",
"SparePartsTabComponent": " Spare Parts"
},
"tabPanelContainerRegion": "Tab group with more product details"
"tabPanelContainerRegion": "Tab group with more product details",
"tabPanelContainerRegionGroup": "Group with more product details"
},
"addToWishList": {
"add": "Add to Wish List",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,13 @@ export interface FeatureTogglesInterface {
*/
a11yUseButtonsForBtnLinks?: boolean;

/**
* Enables the use of TabComponent in the PLP and PDP page to replace some functionality
* of the FacetListComponent and TabParagraphComponent to make then keyboard accessible
* and responsive in tab and accordion stles.
*/
a11yTabComponent?: boolean;

/**
* `ProductImageZoomProductImagesComponent`, `ProductImageZoomThumbnailsComponent` - enable
* arrow keys navigation for the carousel
Expand Down Expand Up @@ -621,6 +628,7 @@ export const defaultFeatureToggles: Required<FeatureTogglesInterface> = {
a11yEmptyWishlistHeading: false,
a11yScreenReaderBloatFix: false,
a11yUseButtonsForBtnLinks: false,
a11yTabComponent: false,
a11yCarouselArrowKeysNavigation: false,
a11yNotificationsOnConsentChange: false,
a11yDisabledCouponAndQuickOrderActionButtonsInsteadOfRequiredFields: false,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ context('Product details', { testIsolation: false }, () => {
before(productDetails.configureDefaultProduct);

productDetails.productDetailsTest();
productDetails.verifyTabKeyboardNavigation();
});

describe('Apparel', () => {
before(productDetails.configureApparelProduct);

productDetails.apparelProductDetailsTest();
productDetails.verifyTabKeyboardNavigation();
});
});

Expand All @@ -40,12 +42,14 @@ context(
before(productDetails.configureDefaultProduct);

productDetails.productDetailsTest();
productDetails.verifyTabKeyboardNavigation(true);
});

describe('Apparel', () => {
before(productDetails.configureApparelProduct);

productDetails.apparelProductDetailsTest();
productDetails.verifyTabKeyboardNavigation(true);
});
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { addProductToCart as addToCart } from './applied-promotions';

export const summaryContainer = `cx-product-summary`;
export const infoContainer = `cx-product-intro`;
export const tabsContainer = 'cx-tab-paragraph-container';
export const tabsContainer = 'cx-tab';
export const tabsHeaderList = `${tabsContainer} > div > button`;
export const activeTabContainer = `${tabsContainer} .active .container`;
export const shippingTabActive = `${tabsContainer} .active cx-paragraph`;
Expand Down Expand Up @@ -59,8 +59,7 @@ export function verifyShowReviewsLink() {
.click();
cy.get(`${tabsHeaderList}`)
.contains(/reviews/i)
.should('be.focused')
.and('have.attr', 'aria-expanded', 'true');
.should('be.focused');
}

export function verifyTextInTabs() {
Expand All @@ -86,7 +85,8 @@ export function verifyTextInTabs() {
}

export function verifyContentInReviewTab() {
cy.get(tabsHeaderList).eq(2).click();
// Double click to close and open on accordian view.
cy.get(tabsHeaderList).eq(2).click().click();
cy.get(reviewList).should('have.length', 5);
cy.get(writeAReviewButton).should('be.visible');
}
Expand Down Expand Up @@ -132,6 +132,43 @@ export function verifyQuantityInCart() {
cy.get(headerCartButton).should('contain', '5');
}

/**
* Verify arrow keys, HOME/END keys and SPACE key.
*/
export function verifyTabKeyboardNavigation(accordian = false) {
it('should navigate tab component with keyboard', () => {
cy.reload();
cy.get('cx-tab button').eq(0).click();
cy.focused().contains('Product Details').type('{downArrow}');
verifySpaceBarKeyForAccordian();
cy.focused().contains('Specs').type('{rightArrow}');
verifySpaceBarKeyForAccordian();
cy.focused().contains('Reviews').type('{leftArrow}');
verifySpaceBarKeyForAccordian();
cy.focused().contains('Specs').type('{upArrow}');
verifySpaceBarKeyForAccordian();
cy.focused().contains('Product Details').type('{upArrow}');
verifySpaceBarKeyForAccordian();
cy.focused().contains('Shipping').type('{downArrow}');
verifySpaceBarKeyForAccordian();
cy.focused().contains('Product Details').type('{end}');
verifySpaceBarKeyForAccordian();
cy.focused().contains('Shipping').type('{home}');
verifySpaceBarKeyForAccordian();
cy.focused().contains('Product Details');

function verifySpaceBarKeyForAccordian() {
if (accordian) {
cy.get('cx-tab-panel').should('not.exist');
cy.focused().type(' ');
cy.get('cx-tab-panel').should('exist');
cy.focused().type(' ');
cy.get('cx-tab-panel').should('not.exist');
}
}
});
}

export function selectProductStyleVariant() {
cy.get(`${variantStyleList} li img[alt="glacier"]`).first().click();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@ if (environment.cpq) {
a11yEmptyWishlistHeading: true,
a11yScreenReaderBloatFix: true,
a11yUseButtonsForBtnLinks: true,
a11yTabComponent: true,
a11yCarouselArrowKeysNavigation: true,
a11yNotificationsOnConsentChange: true,
a11yDisabledCouponAndQuickOrderActionButtonsInsteadOfRequiredFields:
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<ng-container *ngIf="components$ | async as components">
<div
*cxFeature="'!a11yTabComponent'"
role="region"
tabindex="-1"
[attr.aria-label]="ariaLabel | cxTranslate"
Expand Down Expand Up @@ -43,4 +44,16 @@
</ng-container>
</ng-container>
</div>

<ng-container *cxFeature="'a11yTabComponent'">
<cx-tab [tabs]="tabs$ | async" [config]="tabConfig$ | async">
<ng-template
*ngFor="let component of components; let i = index"
[cxOutlet]="component.flexType"
#tabRef
>
<ng-container [cxComponentWrapper]="component"></ng-container>
</ng-template>
</cx-tab>
</ng-container>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
I18nTestingModule,
WindowRef,
} from '@spartacus/core';
import { MockFeatureDirective } from '../../../shared/test/mock-feature-directive';
import { EMPTY, of } from 'rxjs';
import { CmsComponentData } from '../../../cms-structure/index';
import { OutletDirective } from '../../../cms-structure/outlet/index';
Expand Down Expand Up @@ -83,6 +84,7 @@ describe('TabParagraphContainerComponent', () => {
TabParagraphContainerComponent,
ComponentWrapperDirective,
OutletDirective,
MockFeatureDirective,
],
providers: [
WindowRef,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,32 +10,59 @@ import {
Component,
OnInit,
QueryList,
TemplateRef,
ViewChildren,
} from '@angular/core';
import {
CmsService,
CMSTabParagraphContainer,
WindowRef,
} from '@spartacus/core';
import { combineLatest, Observable } from 'rxjs';
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators';
import { ComponentWrapperDirective } from '../../../cms-structure/page/component/component-wrapper.directive';
import { CmsComponentData } from '../../../cms-structure/page/model/index';
import { BREAKPOINT } from '../../../layout/config/layout-config';
import { Tab, TabConfig } from '../tab/tab.model';

const defaultTabConfig = {
openTabs: [0],
breakpoint: BREAKPOINT.md,
};

@Component({
selector: 'cx-tab-paragraph-container',
templateUrl: './tab-paragraph-container.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class TabParagraphContainerComponent implements AfterViewInit, OnInit {
/**
* @deprecated This method will be removed.
*/
activeTabNum = 0;
/**
* @deprecated This method will be removed.
*/
ariaLabel: string;

/**
* @deprecated This method will be removed.
*/
@ViewChildren(ComponentWrapperDirective)
children!: QueryList<ComponentWrapperDirective>;

@ViewChildren('tabRef')
tabRefs: QueryList<TemplateRef<any>>;

/**
* @deprecated This method will be removed.
*/
tabTitleParams: (Observable<any> | null)[] = [];

tabConfig$: BehaviorSubject<TabConfig> = new BehaviorSubject<TabConfig>(
defaultTabConfig
);

constructor(
public componentData: CmsComponentData<CMSTabParagraphContainer>,
protected cmsService: CmsService,
Expand Down Expand Up @@ -71,10 +98,23 @@ export class TabParagraphContainerComponent implements AfterViewInit, OnInit {
})
)
)
).pipe(
// Update tablist label with name from CMS
tap(() => {
this.tabConfig$.next({
label: `${data?.uid}.tabPanelContainerRegionGroup`,
...defaultTabConfig,
});
})
)
)
);

tabs$: Observable<Tab[]>;

/**
* @deprecated This method will be removed.
*/
select(tabNum: number, event?: MouseEvent): void {
this.activeTabNum = this.activeTabNum === tabNum ? -1 : tabNum;
if (event && event?.target) {
Expand All @@ -88,6 +128,9 @@ export class TabParagraphContainerComponent implements AfterViewInit, OnInit {
}
}

/**
* @deprecated This method will be removed.
*/
ngOnInit(): void {
this.activeTabNum =
this.winRef?.nativeWindow?.history?.state?.activeTab ?? this.activeTabNum;
Expand All @@ -99,12 +142,29 @@ export class TabParagraphContainerComponent implements AfterViewInit, OnInit {
if (this.children.length > 0) {
this.getTitleParams(this.children);
}

// Render the tabs after the templates have completed loading in the view.
this.tabs$ = combineLatest([this.components$, this.tabRefs.changes]).pipe(
map(([components, refs]) =>
components.map((component, index) => ({
headerKey: component.title,
content: refs.get(index),
id: index,
}))
)
);
}

/**
* @deprecated This method will be removed.
*/
tabCompLoaded(componentRef: any): void {
this.tabTitleParams.push(componentRef.instance.tabTitleParam$);
}

/**
* @deprecated This method will be removed.
*/
protected getTitleParams(children: QueryList<ComponentWrapperDirective>) {
children.forEach((comp) => {
this.tabTitleParams.push(comp['cmpRef']?.instance.tabTitleParam$ ?? null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,26 @@

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { CmsConfig, I18nModule, provideDefaultConfig } from '@spartacus/core';
import {
CmsConfig,
FeaturesConfigModule,
I18nModule,
provideDefaultConfig,
} from '@spartacus/core';
import { OutletModule } from '../../../cms-structure/outlet/outlet.module';
import { PageComponentModule } from '../../../cms-structure/page/component/page-component.module';
import { TabModule } from '../tab/tab.module';
import { TabParagraphContainerComponent } from './tab-paragraph-container.component';

@NgModule({
imports: [CommonModule, PageComponentModule, OutletModule, I18nModule],
imports: [
CommonModule,
PageComponentModule,
OutletModule,
I18nModule,
TabModule,
FeaturesConfigModule,
],
providers: [
provideDefaultConfig(<CmsConfig>{
cmsComponents: {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<div
[attr.role]="mode === TAB_MODE.ACCORDIAN ? 'region' : 'tabpanel'"
[tabindex]="0"
[attr.aria-labelledby]="tab.id ?? null"
[attr.id]="tab.id ? 'section-' + tab.id : null"
class="active"
[cxFocus]="{ focusOnEscape: true }"
>
<ng-container [ngTemplateOutlet]="tab.content"></ng-container>
</div>
Loading

0 comments on commit ba271cf

Please sign in to comment.