Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CXSPA-8968: Domain Values on Demand implementation #19876

Draft
wants to merge 31 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
3dce3d1
CXSPA-8968: Extend normalizer
Larisa-Staroverova Jan 16, 2025
ef4125d
CXSPA-8968: Define new show-options component and add it to header co…
Larisa-Staroverova Jan 17, 2025
a448c9b
style: CXSPA-8968 - style show more options for attribute header
Uli-Tiger Jan 17, 2025
9684f63
fix: CXSPA-8968 - remove readOnDemand UI Type mapping, as we get prop…
Uli-Tiger Jan 17, 2025
368ed22
fix: CXSPA-8968 switch uiType DDLB to uiType LAZY_LOAD_DDLB for domai…
Uli-Tiger Jan 17, 2025
b4e5fbe
feature: CXSPA-8968 read domain on demand
Uli-Tiger Jan 17, 2025
b4bcccd
test: CXSPA-8968 - fix existing tests, add tests for header component
Uli-Tiger Jan 17, 2025
7826c0d
test: CXSPA-8986 show more options component test
Uli-Tiger Jan 17, 2025
6f1a586
test: CXSPA-8968 - add more tests
Uli-Tiger Jan 20, 2025
2389189
test: CXSPA-8968 - add ReadAttributeDomain action and its effect
Uli-Tiger Jan 20, 2025
04b2e7a
test: CXSPA-8968 - update tests for OccConfiguratorVariantNormalizer …
Uli-Tiger Jan 20, 2025
a57843b
Merge branch 'develop' into feature/CXSPA-8968
Larisa-Staroverova Jan 22, 2025
8b01064
Merge branch 'develop' into feature/CXSPA-8968
Uli-Tiger Jan 22, 2025
cb72336
fix: CXSPA-8968 - update padding properties in SCSS for configurator …
Uli-Tiger Jan 22, 2025
756d3f3
feature: CXSPA-8968 - update according to new UI concept
Uli-Tiger Jan 22, 2025
b00fe77
review: CXSPA-8968 changes due to review
Uli-Tiger Jan 22, 2025
3139a2c
Merge branch 'develop' into feature/CXSPA-8968
Uli-Tiger Jan 22, 2025
07ce200
Merge branch 'develop' into feature/CXSPA-8968
Larisa-Staroverova Jan 23, 2025
a98ccc0
fix: CXSPA-8968 fix after merge - add standlone:false to new components
Uli-Tiger Jan 23, 2025
c85b200
Merge branch 'develop' into feature/CXSPA-8968
Uli-Tiger Jan 23, 2025
cca5970
Add styling files for show-options component and bind them accordingly
Larisa-Staroverova Jan 23, 2025
a7f41f6
Merge branch 'develop' into feature/CXSPA-8968
Uli-Tiger Jan 24, 2025
6db8231
Merge branch 'develop' into feature/CXSPA-8968
Larisa-Staroverova Jan 28, 2025
396f124
Merge branch 'develop' into feature/CXSPA-8968
Larisa-Staroverova Jan 29, 2025
9b82621
Merge branch 'develop' into feature/CXSPA-8968
Larisa-Staroverova Jan 30, 2025
b8aa828
CXSPA-8968: aria attributes
steinsebastian Jan 31, 2025
73fcff4
CXSPA-8968: Accessibility for show options link
steinsebastian Feb 5, 2025
43eac04
CXSPA-8968: Prettier
steinsebastian Feb 5, 2025
66887f3
Merge branch 'develop' into feature/CXSPA-8968
Uli-Tiger Feb 5, 2025
3cca25c
CXSPA-8968: Incorporate review feedback
steinsebastian Feb 7, 2025
0c32e08
Merge branch 'feature/CXSPA-8968' of https://github.com/SAP/spartacus…
steinsebastian Feb 7, 2025
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 @@ -72,7 +72,8 @@
"cancelConfiguration": "Cancel Configuration",
"cancelConfigurationMobile": "Cancel",
"filterOverview": "Filter",
"filterOverviewWithCount": "Filter ({{numAppliedFilters}})"
"filterOverviewWithCount": "Filter ({{numAppliedFilters}})",
"showOptions": "Show Options"
},
"icon": {
"groupComplete": "Complete",
Expand Down Expand Up @@ -232,7 +233,8 @@
"filterOverviewByGroup": "Filter configuration overview by group {{groupName}}",
"closeConflictSolverModal": "Close conflict solver modal",
"closeRestartDialog": "Close the \"Unfinished Configuration\" dialog and navigate back to the product details page",
"description": "Click to see a description for value {{ value }}"
"description": "Click to see a description for value {{ value }}",
"showOptionsForAttribute": "Show options for attribute {{ attribute }}"
},
"variantCarousel": {
"title": "Pre-configured Versions"
Expand Down
2 changes: 1 addition & 1 deletion feature-libs/product-configurator/rulebased/_index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ $configurator-rulebased-components: cx-configurator-template,
cx-configurator-overview-notification-banner,
cx-configuration-conflict-and-error-messages, cx-configurator-variant-carousel,
cx-configurator-conflict-solver-dialog, cx-configurator-restart-dialog,
cx-configurator-exit-button !default;
cx-configurator-exit-button, cx-configurator-show-options !default;

$configurator-rulebased-pages: VariantConfigurationTemplate,
VariantConfigurationOverviewTemplate, CpqConfigurationTemplate,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@
{{ 'configurator.attribute.notVisibleAttributeMsg' | cxTranslate }}
</div>

<label
id="{{ createAttributeUiKey('label', attribute.name) }}"
[class.cx-required-error]="showRequiredMessageForDomainAttribute$ | async"
[attr.aria-label]="
!attribute.required
? ('configurator.a11y.attribute'
| cxTranslate: { attribute: attribute.label })
: ('configurator.a11y.requiredAttribute'
| cxTranslate: { param: attribute.label })
"
><span
[class.cx-required-icon]="attribute.required"
[attr.aria-describedby]="createAttributeUiKey('label', attribute.name)"
>{{ getLabel(expMode, attribute.label, attribute.name) }}</span
></label
>
<div class="cx-header-label-container">
<label
id="{{ createAttributeUiKey('label', attribute.name) }}"
[class.cx-required-error]="showRequiredMessageForDomainAttribute$ | async"
[attr.aria-label]="
!attribute.required
? ('configurator.a11y.attribute'
| cxTranslate: { attribute: attribute.label })
: ('configurator.a11y.requiredAttribute'
| cxTranslate: { param: attribute.label })
"
><span
[class.cx-required-icon]="attribute.required"
[attr.aria-describedby]="createAttributeUiKey('label', attribute.name)"
>{{ getLabel(expMode, attribute.label, attribute.name) }}</span
></label
>

<cx-configurator-show-options
*ngIf="attribute.domainOnDemand"
[attributeComponentContext]="attributeComponentContext"
></cx-configurator-show-options>
</div>

<cx-configurator-show-more
*ngIf="attribute.description"
[text]="attribute.description"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ class MockConfiguratorShowMoreComponent {
@Input() productName: string;
}

@Component({
selector: 'cx-configurator-show-options',
template: '',
standalone: false,
})
class MockConfiguratorShowOptionsComponent {
@Input() attributeComponentContext: ConfiguratorAttributeCompositionContext;
}

export class MockIconFontLoaderService {
useSvg(_iconType: ICON_TYPE) {
return false;
Expand Down Expand Up @@ -131,6 +140,7 @@ describe('ConfigAttributeHeaderComponent', () => {
declarations: [
ConfiguratorAttributeHeaderComponent,
MockConfiguratorShowMoreComponent,
MockConfiguratorShowOptionsComponent,
],
providers: [
{ provide: IconLoaderService, useClass: MockIconFontLoaderService },
Expand Down Expand Up @@ -184,6 +194,7 @@ describe('ConfigAttributeHeaderComponent', () => {
component.groupId = 'testGroup';
component.attribute.required = false;
component.attribute.incomplete = true;
component.attribute.domainOnDemand = false;
component.attribute.uiType = Configurator.UiType.RADIOBUTTON;
component.groupType = Configurator.GroupType.ATTRIBUTE_GROUP;
component.isNavigationToGroupEnabled = true;
Expand Down Expand Up @@ -320,6 +331,23 @@ describe('ConfigAttributeHeaderComponent', () => {
);
});

it('should not render "Show Options" button when domainOnDemand is false', () => {
CommonConfiguratorTestUtilsService.expectElementNotPresent(
expect,
htmlElem,
'cx-configurator-show-options'
);
});
it('should render "Show Options" button when domainOnDemand is true', () => {
component.attribute.domainOnDemand = true;
fixture.detectChanges();
CommonConfiguratorTestUtilsService.expectElementPresent(
expect,
htmlElem,
'cx-configurator-show-options'
);
});

it('should render a label as required', () => {
component.attribute.required = true;
fixture.detectChanges();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { IconModule } from '@spartacus/storefront';
import { ConfiguratorAttributeCompositionConfig } from '../composition/configurator-attribute-composition.config';
import { ConfiguratorAttributeHeaderComponent } from './configurator-attribute-header.component';
import { ConfiguratorShowMoreModule } from '../../show-more/configurator-show-more.module';
import { ConfiguratorShowOptionsModule } from '../show-options/configurator-show-options.module';

@NgModule({
imports: [
Expand All @@ -23,6 +24,7 @@ import { ConfiguratorShowMoreModule } from '../../show-more/configurator-show-mo
IconModule,
NgSelectModule,
ConfiguratorShowMoreModule,
ConfiguratorShowOptionsModule,
],
providers: [
provideDefaultConfig(<ConfiguratorAttributeCompositionConfig>{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ export * from './header/index';
export * from './price-change/index';
export * from './product-card/index';
export * from './quantity/index';
export * from './show-options/index';
export * from './types/base/index';
export * from './types/checkbox-list/index';
export * from './types/checkbox/index';
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
<ng-container>
<button
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have to talk to our accessibility expert whether Show Options buttons is read properly

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we miss a reference to the attribute name at this point.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@steinsebastian: maybe we need a aria text describing what the button does?

class="btn btn-tertiary"
tabindex="0"
(click)="showOptions()"
[attr.title]="'configurator.button.showOptions' | cxTranslate"
[attr.aria-label]="
'configurator.a11y.showOptionsForAttribute'
| cxTranslate: { attribute: attributeComponentContext.attribute.label }
"
[attr.aria-describedby]="
'cx-configurator--label--' + attributeComponentContext.attribute.name
"
>
{{ 'configurator.button.showOptions' | cxTranslate }}
</button>
</ng-container>
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ConfiguratorShowOptionsComponent } from './configurator-show-options.component';
import { I18nTestingModule } from '@spartacus/core';
import { ConfiguratorCommonsService } from '../../../core/facade/configurator-commons.service';
import { CommonConfiguratorTestUtilsService } from '../../../../common/testing/common-configurator-test-utils.service';
import { ConfiguratorStorefrontUtilsService } from '../../service/configurator-storefront-utils.service';
import { By } from '@angular/platform-browser';
import { getTestScheduler } from 'jasmine-marbles';
import { Observable, of } from 'rxjs';
import { ConfiguratorTestUtils } from '../../../testing/configurator-test-utils';

class MockConfiguratorCommonsService {
readAttributeDomain() {}
isConfigurationLoading(): Observable<boolean> {
return of(false);
}
}

class MockConfiguratorStorefrontUtilsService {
focusFirstActiveElement() {}
createAttributeUiKey() {}
}

describe('ConfiguratorShowOptionsComponent', () => {
let component: ConfiguratorShowOptionsComponent;
let fixture: ComponentFixture<ConfiguratorShowOptionsComponent>;
let htmlElem: HTMLElement;
let configuratorCommonsService: ConfiguratorCommonsService;
let configuratorStorefrontUtilsService: ConfiguratorStorefrontUtilsService;

beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ConfiguratorShowOptionsComponent],
imports: [I18nTestingModule],
providers: [
{
provide: ConfiguratorCommonsService,
useClass: MockConfiguratorCommonsService,
},
{
provide: ConfiguratorStorefrontUtilsService,
useClass: MockConfiguratorStorefrontUtilsService,
},
],
}).compileComponents();

configuratorCommonsService = TestBed.inject(ConfiguratorCommonsService);
configuratorStorefrontUtilsService = TestBed.inject(
ConfiguratorStorefrontUtilsService
);
spyOn(configuratorCommonsService, 'readAttributeDomain');
fixture = TestBed.createComponent(ConfiguratorShowOptionsComponent);
component = fixture.componentInstance;
htmlElem = fixture.nativeElement;

component.attributeComponentContext =
ConfiguratorTestUtils.getAttributeContext();
fixture.detectChanges();
});

it('should create', () => {
expect(component).toBeTruthy();
});

it('should render "Show Options" button', () => {
CommonConfiguratorTestUtilsService.expectElementPresent(
expect,
htmlElem,
'.btn'
);
});

it('should delegate to configurator commons service when clicking "Show Options" button', () => {
fixture.debugElement.query(By.css('.btn')).nativeElement.click();
expect(configuratorCommonsService.readAttributeDomain).toHaveBeenCalledWith(
component.attributeComponentContext.owner,
component.attributeComponentContext.group,
component.attributeComponentContext.attribute
);
});

describe('focusFirstValue', () => {
it('should call focusFirstActiveElement of configurator storefront utils service ', () => {
//we need to run the test in a test scheduler
//because of the delay() in method focusFirstValue
getTestScheduler().run(({ cold, flush }) => {
const configurationLoading = cold('-a-b-c', {
a: false,
b: true,
c: false,
});
spyOn(
configuratorCommonsService,
'isConfigurationLoading'
).and.returnValue(configurationLoading);
spyOn(
configuratorStorefrontUtilsService,
'focusFirstActiveElement'
).and.callThrough();
component['focusFirstValue']();
flush();
expect(
configuratorStorefrontUtilsService.focusFirstActiveElement
).toHaveBeenCalledTimes(1);
});
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <spartacus-team@sap.com>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { Component, Input } from '@angular/core';
import { delay, take, distinctUntilChanged, skip } from 'rxjs/operators';
import { ConfiguratorCommonsService } from '../../../core/facade/configurator-commons.service';
import { ConfiguratorAttributeCompositionContext } from '../composition/configurator-attribute-composition.model';
import { ConfiguratorStorefrontUtilsService } from '../../service/configurator-storefront-utils.service';

@Component({
selector: 'cx-configurator-show-options',
templateUrl: './configurator-show-options.component.html',
standalone: false,
})
export class ConfiguratorShowOptionsComponent {
@Input() attributeComponentContext: ConfiguratorAttributeCompositionContext;

constructor(
protected configuratorCommonsService: ConfiguratorCommonsService,
protected configuratorStorefrontUtilsService: ConfiguratorStorefrontUtilsService
) {}

/**
* fires a request to read the attribute domain,
* so that all options of the attribute become visible on the UI
*/
showOptions() {
Uli-Tiger marked this conversation as resolved.
Show resolved Hide resolved
this.focusFirstValue();
this.configuratorCommonsService.readAttributeDomain(
this.attributeComponentContext.owner,
this.attributeComponentContext.group,
this.attributeComponentContext.attribute
);
}

protected focusFirstValue(): void {
this.configuratorCommonsService
.isConfigurationLoading(this.attributeComponentContext.owner)
.pipe(
distinctUntilChanged(),
skip(2), // first isLoading=false as it is called before the readAttributeDomain, second is Loading=true, third is loading=false
take(1),
delay(0) // we need to consider the re-rendering of the page
)
.subscribe(() =>
this.configuratorStorefrontUtilsService.focusFirstActiveElement(
'#' +
this.configuratorStorefrontUtilsService.createAttributeUiKey(
'group-attribute',
this.attributeComponentContext.attribute.name
)
)
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <spartacus-team@sap.com>
*
* SPDX-License-Identifier: Apache-2.0
*/

import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { I18nModule } from '@spartacus/core';
import { ConfiguratorShowOptionsComponent } from './configurator-show-options.component';

@NgModule({
imports: [CommonModule, I18nModule],
providers: [],
declarations: [ConfiguratorShowOptionsComponent],
exports: [ConfiguratorShowOptionsComponent],
})
export class ConfiguratorShowOptionsModule {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/*
* SPDX-FileCopyrightText: 2025 SAP Spartacus team <spartacus-team@sap.com>
*
* SPDX-License-Identifier: Apache-2.0
*/

export * from './configurator-show-options.component';
export * from './configurator-show-options.module';
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
</ng-container>
<div
class="cx-group-attribute"
id="{{ createAttributeUiKey('group-attribute', attribute.name) }}"
[class.cx-hidden]="!attribute.visible"
*ngFor="
let attribute of group.attributes;
Expand Down
Loading