Skip to content

Commit

Permalink
Merge branch 'develop' into feature/CXSPA-9009
Browse files Browse the repository at this point in the history
  • Loading branch information
petarmarkov9449 authored Jan 16, 2025
2 parents f9172f1 + e7b7e65 commit a79c18f
Show file tree
Hide file tree
Showing 16 changed files with 183 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"
type="submit"
[disabled]="disabled || quantity <= 0 || quantity > maxQuantity"
[attr.aria-describedby]="productCode + '_header'"
>
<span
*ngIf="
Expand Down
Original file line number Diff line number Diff line change
@@ -1,30 +1,69 @@
<div
[attr.data-preferred-store]="pointOfServiceName.name"
[attr.data-store-is-selected]="
pointOfServiceName.name === (storeSelected$ | async)?.name
"
class="setpreferredstore-container"
(click)="setAsPreferred()"
>
<ng-container *cxFeature="'a11yRepeatingButtonsUniqueLabels'">
<ng-container *ngIf="{ storeSelected: storeSelected$ | async } as data">
<div
[attr.data-preferred-store]="pointOfServiceName.name"
[attr.data-store-is-selected]="
pointOfServiceName.name === data.storeSelected?.name
"
class="setpreferredstore-container"
(click)="setAsPreferred()"
>
<div
[ngClass]="
pointOfServiceName.name === data.storeSelected?.name
? 'icon-selected'
: 'icon-not-selected'
"
>
<cx-icon aria-hidden="true" [type]="ICON_TYPE.HEART"></cx-icon>
</div>
<button
data-text="setPreferredStore.myStore"
[attr.data-preferred-store]="pointOfServiceName.name"
class="set-preferred-heading"
[attr.aria-label]="
(getSetStoreButtonLabel(data.storeSelected?.name || '')
| cxTranslate) +
', ' +
pointOfServiceName.displayName
"
>
{{
getSetStoreButtonLabel(data.storeSelected?.name || '') | cxTranslate
}}
</button>
</div>
</ng-container>
</ng-container>
<ng-container *cxFeature="'!a11yRepeatingButtonsUniqueLabels'">
<div
[ngClass]="{
'icon-selected':
pointOfServiceName.name === (storeSelected$ | async)?.name,
'icon-not-selected':
pointOfServiceName.name !== (storeSelected$ | async)?.name,
}"
>
<cx-icon aria-hidden="true" [type]="ICON_TYPE.HEART"></cx-icon>
</div>
<button
data-text="setPreferredStore.myStore"
[attr.data-preferred-store]="pointOfServiceName.name"
class="set-preferred-heading"
>
{{
[attr.data-store-is-selected]="
pointOfServiceName.name === (storeSelected$ | async)?.name
? ('setPreferredStore.myStore' | cxTranslate)
: ('setPreferredStore.makeThisMyStore' | cxTranslate)
}}
</button>
</div>
"
class="setpreferredstore-container"
(click)="setAsPreferred()"
>
<div
[ngClass]="{
'icon-selected':
pointOfServiceName.name === (storeSelected$ | async)?.name,
'icon-not-selected':
pointOfServiceName.name !== (storeSelected$ | async)?.name,
}"
>
<cx-icon aria-hidden="true" [type]="ICON_TYPE.HEART"></cx-icon>
</div>
<button
data-text="setPreferredStore.myStore"
[attr.data-preferred-store]="pointOfServiceName.name"
class="set-preferred-heading"
>
{{
pointOfServiceName.name === (storeSelected$ | async)?.name
? ('setPreferredStore.myStore' | cxTranslate)
: ('setPreferredStore.makeThisMyStore' | cxTranslate)
}}
</button>
</div>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
PreferredStoreFacade,
} from '@spartacus/pickup-in-store/root';
import { IconTestingModule, OutletContextData } from '@spartacus/storefront';
import { MockFeatureDirectivesModule } from 'projects/storefrontlib/shared/test/mock-feature-directives.module';
import { of } from 'rxjs';
import { MockPreferredStoreService } from '../../../core/services/preferred-store.service.spec';
import { SetPreferredStoreComponent } from './set-preferred-store.component';
Expand All @@ -24,7 +25,12 @@ describe('SetPreferredStoreComponent without outlet.context$', () => {
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [SetPreferredStoreComponent],
imports: [I18nTestingModule, IconTestingModule, CommonModule],
imports: [
I18nTestingModule,
IconTestingModule,
CommonModule,
MockFeatureDirectivesModule,
],
providers: [
{ provide: PreferredStoreFacade, useClass: MockPreferredStoreService },
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,15 @@ export class SetPreferredStoreComponent implements OnInit, OnDestroy {
ngOnDestroy() {
this.subscription.unsubscribe();
}

setAsPreferred(): boolean {
this.preferredStoreFacade.setPreferredStore(this.pointOfServiceName);
return false;
}

getSetStoreButtonLabel(storeName: string): string {
return this.pointOfServiceName.name === storeName
? 'setPreferredStore.myStore'
: 'setPreferredStore.makeThisMyStore';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { I18nModule } from '@spartacus/core';
import { FeaturesConfigModule, I18nModule } from '@spartacus/core';
import { StoreFinderOutlets } from '@spartacus/storefinder/root';
import {
IconModule,
Expand All @@ -16,7 +16,7 @@ import {
import { SetPreferredStoreComponent } from './set-preferred-store.component';

@NgModule({
imports: [CommonModule, IconModule, I18nModule],
imports: [CommonModule, IconModule, I18nModule, FeaturesConfigModule],
exports: [SetPreferredStoreComponent],
declarations: [SetPreferredStoreComponent],
providers: [
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@
<button
(click)="toggleOpenHours()"
[attr.aria-expanded]="openHoursOpen"
[attr.aria-label]="
('store.viewHours' | cxTranslate) +
', ' +
storeDetails.displayName
"
class="cx-store-opening-hours-toggle"
>
{{ 'store.viewHours' | cxTranslate }}
Expand Down Expand Up @@ -65,6 +70,9 @@
class="btn btn-secondary btn-block"
[disabled]="!isInStock"
[attr.data-pickup-in-store-button]="storeDetails.name"
[attr.aria-label]="
('store.pickupFromHere' | cxTranslate) + ', ' + storeDetails.displayName
"
>
{{ 'store.pickupFromHere' | cxTranslate }}
</button>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,7 @@
bindValue="isocode"
placeholder="{{ 'addressForm.selectOne' | cxTranslate }}"
(change)="countrySelected($event)"
[cxNgSelectA11y]="{
ariaLabel: 'addressForm.country' | cxTranslate,
}"
[cxNgSelectA11y]="{}"
>
</ng-select>
<cx-form-errors
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ describe('OpfResourceLoaderService', () => {
const mockScriptResource = {
url: 'script-url',
type: OpfDynamicScriptResourceType.SCRIPT,
attributes: [{ key: 'opf-load-once', value: 'true' }],
};

const mockStyleResource = {
url: 'style-url',
type: OpfDynamicScriptResourceType.STYLES,
attributes: [{ key: 'opf-load-once', value: 'true' }],
};

spyOn<any>(opfResourceLoaderService, 'loadScript').and.callThrough();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@ export class OpfResourceLoaderService {
protected document = inject(DOCUMENT);
protected platformId = inject(PLATFORM_ID);

protected readonly OPF_RESOURCE_ATTRIBUTE_KEY = 'data-opf-resource';
protected readonly CORS_DEFAULT_VALUE = 'anonymous';
protected readonly OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY = 'opf-load-once';
protected readonly OPF_RESOURCE_ATTRIBUTE_KEY = 'data-opf-resource';

protected embedStyles(embedOptions: {
attributes?: OpfKeyValueMap[];
attributes?: { [key: string]: string };
src: string;
sri?: string;
callback?: EventListener;
Expand All @@ -38,22 +39,18 @@ export class OpfResourceLoaderService {
link.href = src;
link.rel = 'stylesheet';
link.type = 'text/css';
link.setAttribute(this.OPF_RESOURCE_ATTRIBUTE_KEY, 'true');
if (sri) {
link.integrity = sri;
const corsKeyvalue = attributes?.find(
(attr) => attr.key === 'crossorigin' && !!attr.value?.length
);
link.crossOrigin = corsKeyvalue?.value ?? this.CORS_DEFAULT_VALUE;
link.crossOrigin = attributes?.['crossorigin'] ?? this.CORS_DEFAULT_VALUE;
delete attributes?.['crossorigin'];
}
if (attributes?.length) {
attributes.forEach((attribute) => {
const { key, value } = attribute;

attributes &&
Object.keys(attributes)?.forEach((key) => {
if (!(key in link)) {
link.setAttribute(key, value);
link.setAttribute(key, attributes[key as keyof object]);
}
});
}

if (callback) {
link.addEventListener('load', callback);
Expand All @@ -74,39 +71,56 @@ export class OpfResourceLoaderService {
return this.scriptLoader.hasScript(src);
}

/**
* Create attributes intended to script and link elements.
*
* Return attributes list including keyValueList and OPF specific attribute with below logic:
*
* 1. Resource loads only once: 'opf-load-once' key detected, no additional attribute added.
* 2. Resource deleted at page/payment change: 'data-opf-resource' attribute is added.
*/

protected createAttributesList(keyValueList?: OpfKeyValueMap[] | undefined): {
[key: string]: string;
} {
const attributes: { [key: string]: string } = {};
keyValueList?.forEach((keyValue: OpfKeyValueMap) => {
attributes[keyValue.key] = keyValue.value;
});
if (
!attributes?.[this.OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY] ||
attributes[this.OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY] !== 'true'
) {
attributes[this.OPF_RESOURCE_ATTRIBUTE_KEY] = 'true';
}
delete attributes?.[this.OPF_RESOURCE_LOAD_ONCE_ATTRIBUTE_KEY];
return attributes;
}

/**
* Loads a script specified in the resource object.
*
* The returned Promise is resolved when the script is loaded or already present.
* The returned Promise is rejected when a loading error occurs.
*/

protected loadScript(resource: OpfDynamicScriptResource): Promise<void> {
return new Promise((resolve, reject) => {
const attributes: any = {
const attributes: { [key: string]: string } = {
type: 'text/javascript',
[this.OPF_RESOURCE_ATTRIBUTE_KEY]: true,
...this.createAttributesList(resource.attributes),
};

if (resource?.sri) {
attributes['integrity'] = resource.sri;
const corsKeyvalue: OpfKeyValueMap | undefined =
resource?.attributes?.find(
(attr) => attr.key === 'crossorigin' && !!attr.value?.length
);
attributes['crossOrigin'] =
corsKeyvalue?.value ?? this.CORS_DEFAULT_VALUE;
}

if (resource.attributes) {
resource.attributes.forEach((attribute) => {
attributes[attribute.key] = attribute.value;
});
attributes?.['crossorigin'] ?? this.CORS_DEFAULT_VALUE;
delete attributes?.['crossorigin'];
}

if (resource.url && !this.hasScript(resource.url)) {
if (resource?.url && !this.hasScript(resource.url)) {
this.scriptLoader.embedScript({
attributes,
src: resource.url,
attributes: attributes,
callback: () => resolve(),
errorCallback: () => reject(),
disableKeyRestriction: true,
Expand All @@ -123,11 +137,12 @@ export class OpfResourceLoaderService {
* The returned Promise is resolved when the stylesheet is loaded or already present.
* The returned Promise is rejected when a loading error occurs.
*/

protected loadStyles(resource: OpfDynamicScriptResource): Promise<void> {
return new Promise((resolve, reject) => {
if (resource.url && !this.hasStyles(resource.url)) {
this.embedStyles({
attributes: resource?.attributes,
attributes: this.createAttributesList(resource?.attributes),
src: resource.url,
sri: resource?.sri,
callback: () => resolve(),
Expand Down Expand Up @@ -167,6 +182,7 @@ export class OpfResourceLoaderService {
* The returned Promise is resolved when all resources are loaded.
* The returned Promise is also resolved (not rejected!) immediately when any loading error occurs.
*/

loadResources(
scripts: OpfDynamicScriptResource[] = [],
styles: OpfDynamicScriptResource[] = []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,11 @@ export interface FeatureTogglesInterface {
*/
a11yPopoverFocus?: boolean;

/**
* Fix popover appearance when a High Contrast Theme is applied.
*/
a11yPopoverHighContrast?: boolean;

/**
* Adds Datepicker and Combobox label and corrects heading order for 'CheckoutScheduleReplenishmentOrderComponent'.
*/
Expand Down Expand Up @@ -815,6 +820,12 @@ export interface FeatureTogglesInterface {
*/
a11yHideConsentButtonWhenBannerVisible?: boolean;

/**
* Adds a unique `aria-label` to repeating buttons that contain the same text.
* Affects: SetPreferredStoreComponent
*/
a11yRepeatingButtonsUniqueLabels?: boolean;

/**
* Ensures that borders across all UI elements are visible and meet accessibility standards in high-contrast dark and light themes.
* This change is applied globally to enhance usability for users relying on high-contrast modes.
Expand Down Expand Up @@ -984,6 +995,7 @@ export const defaultFeatureToggles: Required<FeatureTogglesInterface> = {
a11yPreventHorizontalScroll: false,
a11yReorderDialog: true,
a11yPopoverFocus: true,
a11yPopoverHighContrast: false,
a11yScheduleReplenishment: true,
a11yScrollToTop: true,
a11ySavedCartsZoom: true,
Expand Down Expand Up @@ -1085,6 +1097,7 @@ export const defaultFeatureToggles: Required<FeatureTogglesInterface> = {
a11yFocusOnCardAfterSelecting: false,
a11ySearchableDropdownFirstElementFocus: false,
a11yHideConsentButtonWhenBannerVisible: false,
a11yRepeatingButtonsUniqueLabels: false,
a11yHighContrastBorders: false,
occCartNameAndDescriptionInHttpRequestBody: false,
cmsBottomHeaderSlotUsingFlexStyles: false,
Expand Down
Loading

0 comments on commit a79c18f

Please sign in to comment.