diff --git a/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.html b/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.html index ce154665b90..a04b8963cc5 100644 --- a/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.html +++ b/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.html @@ -10,6 +10,6 @@ [displayPickupLocation]="(storeDetails$ | async)?.displayName" [selectedOption]="pickupOption$ | async" (pickupOptionChange)="onPickupOptionChange($event)" - (pickupLocationChange)="openDialog()" + (pickupLocationChange)="openDialog($event)" > diff --git a/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.spec.ts b/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.spec.ts index d2b4b220687..4a2d0d22a95 100644 --- a/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.spec.ts +++ b/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.spec.ts @@ -1,4 +1,5 @@ import { CommonModule } from '@angular/common'; +import { ElementRef } from '@angular/core'; import { ComponentFixture, TestBed, @@ -6,7 +7,12 @@ import { tick, } from '@angular/core/testing'; import { ActiveCartFacade, Cart, OrderEntry } from '@spartacus/cart/base/root'; -import { CmsService, I18nTestingModule, Page } from '@spartacus/core'; +import { + CmsService, + FeatureConfigService, + I18nTestingModule, + Page, +} from '@spartacus/core'; import { AugmentedPointOfService, IntendedPickupLocationFacade, @@ -16,8 +22,8 @@ import { PreferredStoreFacade, } from '@spartacus/pickup-in-store/root'; import { - LaunchDialogService, LAUNCH_CALLER, + LaunchDialogService, OutletContextData, } from '@spartacus/storefront'; import { cold } from 'jasmine-marbles'; @@ -109,6 +115,12 @@ const mockOutletContext: { item: OrderEntry; cartType: string } = { cartType: 'cart', }; +class MockFeatureConfigService { + isEnabled() { + return true; + } +} + const context$ = of(mockOutletContext); class MockIntendedPickupLocationFacade { @@ -167,6 +179,10 @@ describe('CartPickupOptionsContainerComponent', () => { provide: IntendedPickupLocationFacade, useClass: MockIntendedPickupLocationFacade, }, + { + provide: FeatureConfigService, + useClass: MockFeatureConfigService, + }, ], }); @@ -198,10 +214,11 @@ describe('CartPickupOptionsContainerComponent', () => { }); it('should trigger and open dialog', () => { - component.openDialog(); + const triggerElement = new ElementRef({}); + component.openDialog(triggerElement); expect(launchDialogService.openDialog).toHaveBeenCalledWith( LAUNCH_CALLER.PICKUP_IN_STORE, - component.element, + triggerElement, component['vcr'], { productCode: 'productCode1', entryNumber: 1, quantity: 1 } ); @@ -210,7 +227,12 @@ describe('CartPickupOptionsContainerComponent', () => { it('should not openDialog if display name is not set and ship it is selected', () => { spyOn(component, 'openDialog'); component['displayNameIsSet'] = false; - component.onPickupOptionChange('delivery'); + const pickupOption: PickupOption = 'delivery'; + const event = { + option: pickupOption, + triggerElement: new ElementRef({}), + }; + component.onPickupOptionChange(event); expect(component.openDialog).not.toHaveBeenCalled(); }); @@ -218,6 +240,10 @@ describe('CartPickupOptionsContainerComponent', () => { const entryNumber = 2; const pickupOption: PickupOption = 'pickup'; const quantity = 3; + const event = { + option: pickupOption, + triggerElement: new ElementRef({}), + }; component.entryNumber = entryNumber; component.quantity = quantity; @@ -226,7 +252,7 @@ describe('CartPickupOptionsContainerComponent', () => { spyOn(pickupOptionService, 'setPickupOption'); spyOn(activeCartService, 'updateEntry'); - component.onPickupOptionChange(pickupOption); + component.onPickupOptionChange(event); expect(pickupOptionService.setPickupOption).toHaveBeenCalledWith( entryNumber, pickupOption diff --git a/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.ts b/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.ts index ecfdee85fed..5abd5ec2cfc 100644 --- a/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.ts +++ b/feature-libs/pickup-in-store/components/container/cart-pickup-options-container/cart-pickup-options-container.component.ts @@ -7,6 +7,7 @@ import { Component, ElementRef, + inject, OnDestroy, OnInit, Optional, @@ -18,7 +19,7 @@ import { CartType, OrderEntry, } from '@spartacus/cart/base/root'; -import { CmsService, Page } from '@spartacus/core'; +import { CmsService, FeatureConfigService, Page } from '@spartacus/core'; import { cartWithIdAndUserId, getProperty, @@ -30,8 +31,8 @@ import { RequiredDeepPath, } from '@spartacus/pickup-in-store/root'; import { - LaunchDialogService, LAUNCH_CALLER, + LaunchDialogService, OutletContextData, } from '@spartacus/storefront'; import { EMPTY, iif, Observable, of, Subscription } from 'rxjs'; @@ -79,6 +80,12 @@ export function orderEntryWithRequiredFields( templateUrl: 'cart-pickup-options-container.component.html', }) export class CartPickupOptionsContainerComponent implements OnInit, OnDestroy { + // TODO: Remove element reference once 'a11yDialogTriggerRefocus' feature flag is removed. + /** + * @deprecated since 2211.28.0 + * This reference does not point to any element and will be removed at earliest convinience. + * The 'triggerElement' is passed through 'PickupOptionChange' event instead. + */ @ViewChild('open') element: ElementRef; pickupOption$: Observable; @@ -98,6 +105,7 @@ export class CartPickupOptionsContainerComponent implements OnInit, OnDestroy { private displayNameIsSet = false; page?: string; readonly CartType = CartType; + private featureConfigService = inject(FeatureConfigService); constructor( protected activeCartFacade: ActiveCartFacade, protected launchDialogService: LaunchDialogService, @@ -253,40 +261,92 @@ export class CartPickupOptionsContainerComponent implements OnInit, OnDestroy { tap((_) => (this.displayNameIsSet = true)) ); } - - onPickupOptionChange(pickupOption: PickupOption): void { - this.pickupOptionFacade.setPickupOption(this.entryNumber, pickupOption); - if (pickupOption === 'delivery') { - this.activeCartFacade.updateEntry( - this.entryNumber, - this.quantity, - undefined, - true - ); - return; - } - [pickupOption] - .filter((option) => option === 'pickup') - .forEach(() => { - this.subscription.add( - this.storeDetails$ - .pipe( - filter(({ name }) => !!name), - tap(({ name }) => - this.activeCartFacade.updateEntry( - this.entryNumber, - this.quantity, - name, - true + // TODO: Remove 'PickupOption' argument type once 'a11yDialogTriggerRefocus' feature flag is removed. + /** + * @deprecated since 2211.28.0 - Use event param instead of option. + * @param event - Object containing the selected option and the element that triggered the change. + */ + onPickupOptionChange(pickupOption: PickupOption): void; + // eslint-disable-next-line @typescript-eslint/unified-signatures + onPickupOptionChange(event: { + option: PickupOption; + triggerElement: ElementRef; + }): void; + onPickupOptionChange( + event: { option: PickupOption; triggerElement: ElementRef } | PickupOption + ): void { + /* istanbul ignore else */ + if ( + this.featureConfigService.isEnabled('a11yDialogTriggerRefocus') && + typeof event === 'object' + ) { + this.pickupOptionFacade.setPickupOption(this.entryNumber, event.option); + if (event.option === 'delivery') { + this.activeCartFacade.updateEntry( + this.entryNumber, + this.quantity, + undefined, + true + ); + return; + } + [event.option] + .filter((option) => option === 'pickup') + .forEach(() => { + this.subscription.add( + this.storeDetails$ + .pipe( + filter(({ name }) => !!name), + tap(({ name }) => + this.activeCartFacade.updateEntry( + this.entryNumber, + this.quantity, + name, + true + ) ) ) - ) - .subscribe() + .subscribe() + ); + }); + + if (!this.displayNameIsSet) { + this.openDialog(event.triggerElement); + } + } else if (typeof event === 'string') { + this.pickupOptionFacade.setPickupOption(this.entryNumber, event); + if (event === 'delivery') { + this.activeCartFacade.updateEntry( + this.entryNumber, + this.quantity, + undefined, + true ); - }); + return; + } + [event] + .filter((option) => option === 'pickup') + .forEach(() => { + this.subscription.add( + this.storeDetails$ + .pipe( + filter(({ name }) => !!name), + tap(({ name }) => + this.activeCartFacade.updateEntry( + this.entryNumber, + this.quantity, + name, + true + ) + ) + ) + .subscribe() + ); + }); - if (!this.displayNameIsSet) { - this.openDialog(); + if (!this.displayNameIsSet) { + this.openDialog(); + } } } @@ -294,10 +354,15 @@ export class CartPickupOptionsContainerComponent implements OnInit, OnDestroy { this.subscription.unsubscribe(); } - openDialog(): void { + // TODO: Make argument required once 'a11yDialogTriggerRefocus' feature flag is removed. + /** + * @deprecated since 2211.28.0 - The use of TriggerElement param will become mandatory. + * @param triggerElement - The reference of element that triggered the dialog. Used to refocus on it after the dialog is closed. + */ + openDialog(triggerElement?: ElementRef): void { const dialog = this.launchDialogService.openDialog( LAUNCH_CALLER.PICKUP_IN_STORE, - this.element, + triggerElement ? triggerElement : this.element, this.vcr, { productCode: this.productCode, diff --git a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts index 0fac75b627a..864374fdc58 100644 --- a/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts +++ b/projects/core/src/features-config/feature-toggles/config/feature-toggles.ts @@ -562,7 +562,7 @@ export interface FeatureTogglesInterface { /** * When enabled, the focus will be returned to the trigger element after the dialog is closed. - * Affected components: 'AddtoCartComponent', 'PickupOptionsComponent' + * Affected components: 'AddtoCartComponent', 'PickupOptionsComponent', CartPickupOptionsContainerComponent, PDPPickupOptionsContainerComponent */ a11yDialogTriggerRefocus?: boolean; diff --git a/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/focus-management.e2e.cy.ts b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/focus-management.e2e.cy.ts index 3dea1a4b423..310a4877ee1 100644 --- a/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/focus-management.e2e.cy.ts +++ b/projects/storefrontapp-e2e-cypress/cypress/e2e/accessibility/focus-management.e2e.cy.ts @@ -4,6 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ +import * as cart from '../../helpers/cart'; import * as checkout from '../../helpers/checkout-flow'; describe('Focus managment for a11y', () => { @@ -34,8 +35,17 @@ describe('Focus managment for a11y', () => { }); context('Pick up in store modal', () => { - it('Should re-focus the element triggering the modal after it closes', () => { + it('Should re-focus the element triggering the modal on PDP after it closes', () => { + cy.visit(`/product/266685`); + cy.contains('Select Store').click(); + cy.get('[aria-label="Close"]').click(); + cy.contains('Select Store').should('have.focus'); + }); + + it('Should re-focus the element triggering the modal in Cart after it closes', () => { cy.visit(`/product/266685`); + cart.addProductAsAnonymous(); + cy.visit('/cart'); cy.contains('Select Store').click(); cy.get('[aria-label="Close"]').click(); cy.contains('Select Store').should('have.focus');