Skip to content

Commit

Permalink
feat: a11y improvements for product configurator group menu (#12348)
Browse files Browse the repository at this point in the history
Replaces links with buttons, and improves keyboard navigation for the product configurator group menu

Closes #12089
  • Loading branch information
Larisa-Staroverova authored Jun 11, 2021
1 parent 2695f21 commit bab6215
Show file tree
Hide file tree
Showing 23 changed files with 1,189 additions and 472 deletions.
3 changes: 2 additions & 1 deletion docs/migration/4_0.md
Original file line number Diff line number Diff line change
Expand Up @@ -399,7 +399,8 @@ The display of the guest checkout button relies on the presence of the `forced`
`ConfiguratorAttributeCheckboxListComponent` now also requires `ConfiguratorAttributeQuantityService`.
`ConfiguratorAttributeDropDownComponent` now also requires `ConfiguratorAttributeQuantityService`.
`ConfiguratorAttributeRadiButtonComponent` now also requires `ConfiguratorAttributeQuantityService`.
`ConfiguratorStorefrontUtilsService` now also requires `KeyboardFocusService`.
`ConfiguratorStorefrontUtilsService` now also requires `WindowRef` and `KeyboardFocusService`.
`ConfiguratorFormComponent` now also requires `ConfiguratorStorefrontUtilsService`.

### Product variants changes

Expand Down
4 changes: 3 additions & 1 deletion docs/migration/css-changes-in-version-4.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,10 @@ title: Changes to Styles in 4.0

* `cx-configurator-truncate-content` mixin has been added on `%cx-configurator-product-title` for `cx-detail-title`, `cx-code` and `cx-description` selectors to enable the truncation for the small widgets.

## Change in Configurator Group Menu Component
## Changes in Configurator Group Menu Component

* `cx-group-menu` class replace `ul` element on `%cx-configurator-group-menu`.

* `cx-configurator-truncate-content` mixin has been added on `%cx-configurator-group-menu` for `span` selector to enable the configuration group title truncation for the small widgets.

## Changes in Configurator Form Component
Expand Down
Original file line number Diff line number Diff line change
@@ -1,134 +1,136 @@
<ng-container *ngIf="configuration$ | async as configuration">
<ng-container *ngIf="currentGroup$ | async as currentGroup">
<ng-container *ngIf="isConflictGroupType(currentGroup.groupType)">
<cx-configurator-conflict-description
[currentGroup]="currentGroup"
></cx-configurator-conflict-description>
</ng-container>
<div
class="cx-group-attribute"
*ngFor="
let attribute of currentGroup.attributes;
let indexOfAttribute = index
"
>
<div id="{{ createGroupId(currentGroup.id) }}" role="tabpanel">
<ng-container *ngIf="isConflictGroupType(currentGroup.groupType)">
<cx-configurator-conflict-suggestion
<cx-configurator-conflict-description
[currentGroup]="currentGroup"
[attribute]="attribute"
[suggestionNumber]="indexOfAttribute"
></cx-configurator-conflict-suggestion>
></cx-configurator-conflict-description>
</ng-container>
<cx-configurator-attribute-header
[attribute]="attribute"
[owner]="configuration.owner"
[groupId]="currentGroup.id"
[groupType]="currentGroup.groupType"
></cx-configurator-attribute-header>
<div
class="cx-group-attribute"
*ngFor="
let attribute of currentGroup.attributes;
let indexOfAttribute = index
"
>
<ng-container *ngIf="isConflictGroupType(currentGroup.groupType)">
<cx-configurator-conflict-suggestion
[currentGroup]="currentGroup"
[attribute]="attribute"
[suggestionNumber]="indexOfAttribute"
></cx-configurator-conflict-suggestion>
</ng-container>
<cx-configurator-attribute-header
[attribute]="attribute"
[owner]="configuration.owner"
[groupId]="currentGroup.id"
[groupType]="currentGroup.groupType"
></cx-configurator-attribute-header>

<!-- Single Selection Radio Button -->
<cx-configurator-attribute-radio-button
*ngIf="attribute.uiType === uiType.RADIOBUTTON"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-radio-button>
<!-- Single Selection Radio Button -->
<cx-configurator-attribute-radio-button
*ngIf="attribute.uiType === uiType.RADIOBUTTON"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-radio-button>

<!-- Single Selection Product Bundle -->
<cx-configurator-attribute-single-selection-bundle
(selectionChange)="updateConfiguration($event)"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
*ngIf="attribute.uiType === uiType.RADIOBUTTON_PRODUCT"
></cx-configurator-attribute-single-selection-bundle>
<!-- Single Selection Product Bundle -->
<cx-configurator-attribute-single-selection-bundle
(selectionChange)="updateConfiguration($event)"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
*ngIf="attribute.uiType === uiType.RADIOBUTTON_PRODUCT"
></cx-configurator-attribute-single-selection-bundle>

<!-- Multi Selection Product Bundle -->
<cx-configurator-attribute-multi-selection-bundle
(selectionChange)="updateConfiguration($event)"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
*ngIf="attribute.uiType === uiType.CHECKBOXLIST_PRODUCT"
></cx-configurator-attribute-multi-selection-bundle>
<!-- Multi Selection Product Bundle -->
<cx-configurator-attribute-multi-selection-bundle
(selectionChange)="updateConfiguration($event)"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
*ngIf="attribute.uiType === uiType.CHECKBOXLIST_PRODUCT"
></cx-configurator-attribute-multi-selection-bundle>

<!-- Single Selection Dropdown Product Bundle -->
<cx-configurator-attribute-single-selection-bundle-dropdown
*ngIf="attribute.uiType === uiType.DROPDOWN_PRODUCT"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-single-selection-bundle-dropdown>
<!-- Single Selection Dropdown Product Bundle -->
<cx-configurator-attribute-single-selection-bundle-dropdown
*ngIf="attribute.uiType === uiType.DROPDOWN_PRODUCT"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-single-selection-bundle-dropdown>

<!-- Single Selection Dropdown -->
<cx-configurator-attribute-drop-down
*ngIf="attribute.uiType === uiType.DROPDOWN"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-drop-down>
<cx-configurator-attribute-input-field
*ngIf="attribute.uiType === uiType.STRING"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerType]="configuration.owner.type"
[ownerKey]="configuration.owner.key"
(inputChange)="updateConfiguration($event)"
>
</cx-configurator-attribute-input-field>
<ng-container *ngIf="activeLanguage$ | async as activeLanguage">
<cx-configurator-attribute-numeric-input-field
*ngIf="attribute.uiType === uiType.NUMERIC"
<!-- Single Selection Dropdown -->
<cx-configurator-attribute-drop-down
*ngIf="attribute.uiType === uiType.DROPDOWN"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-drop-down>
<cx-configurator-attribute-input-field
*ngIf="attribute.uiType === uiType.STRING"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerType]="configuration.owner.type"
[ownerKey]="configuration.owner.key"
[language]="activeLanguage"
(inputChange)="updateConfiguration($event)"
></cx-configurator-attribute-numeric-input-field>
</ng-container>
>
</cx-configurator-attribute-input-field>
<ng-container *ngIf="activeLanguage$ | async as activeLanguage">
<cx-configurator-attribute-numeric-input-field
*ngIf="attribute.uiType === uiType.NUMERIC"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerKey]="configuration.owner.key"
[language]="activeLanguage"
(inputChange)="updateConfiguration($event)"
></cx-configurator-attribute-numeric-input-field>
</ng-container>

<!-- Multi Selection Checkbox -->
<cx-configurator-attribute-checkbox-list
*ngIf="attribute.uiType === uiType.CHECKBOXLIST"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-checkbox-list>
<cx-configurator-attribute-checkbox
*ngIf="attribute.uiType === uiType.CHECKBOX"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-checkbox>
<cx-configurator-attribute-read-only
*ngIf="attribute.uiType === uiType.READ_ONLY"
[attribute]="attribute"
[group]="currentGroup.id"
></cx-configurator-attribute-read-only>
<cx-configurator-attribute-multi-selection-image
*ngIf="attribute.uiType === uiType.MULTI_SELECTION_IMAGE"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-multi-selection-image>
<cx-configurator-attribute-single-selection-image
*ngIf="attribute.uiType === uiType.SINGLE_SELECTION_IMAGE"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-single-selection-image>
<!-- Multi Selection Checkbox -->
<cx-configurator-attribute-checkbox-list
*ngIf="attribute.uiType === uiType.CHECKBOXLIST"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-checkbox-list>
<cx-configurator-attribute-checkbox
*ngIf="attribute.uiType === uiType.CHECKBOX"
[attribute]="attribute"
[group]="currentGroup.id"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-checkbox>
<cx-configurator-attribute-read-only
*ngIf="attribute.uiType === uiType.READ_ONLY"
[attribute]="attribute"
[group]="currentGroup.id"
></cx-configurator-attribute-read-only>
<cx-configurator-attribute-multi-selection-image
*ngIf="attribute.uiType === uiType.MULTI_SELECTION_IMAGE"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-multi-selection-image>
<cx-configurator-attribute-single-selection-image
*ngIf="attribute.uiType === uiType.SINGLE_SELECTION_IMAGE"
[attribute]="attribute"
[ownerKey]="configuration.owner.key"
(selectionChange)="updateConfiguration($event)"
></cx-configurator-attribute-single-selection-image>

<em *ngIf="attribute.uiType === uiType.NOT_IMPLEMENTED">{{
'configurator.attribute.notSupported' | cxTranslate
}}</em>
<em *ngIf="attribute.uiType === uiType.NOT_IMPLEMENTED">{{
'configurator.attribute.notSupported' | cxTranslate
}}</em>

<cx-configurator-attribute-footer
[attribute]="attribute"
[owner]="configuration.owner"
[groupId]="currentGroup.id"
></cx-configurator-attribute-footer>
<cx-configurator-attribute-footer
[attribute]="attribute"
[owner]="configuration.owner"
[groupId]="currentGroup.id"
></cx-configurator-attribute-footer>
</div>
</div>
</ng-container>
</ng-container>
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { ConfiguratorAttributeRadioButtonComponent } from '../attribute/types/ra
import { ConfiguratorAttributeReadOnlyComponent } from '../attribute/types/read-only/configurator-attribute-read-only.component';
import { ConfiguratorAttributeSingleSelectionImageComponent } from '../attribute/types/single-selection-image/configurator-attribute-single-selection-image.component';
import { ConfiguratorFormComponent } from './configurator-form.component';
import { ConfiguratorStorefrontUtilsService } from '@spartacus/product-configurator/rulebased';

const PRODUCT_CODE = 'CONF_LAPTOP';
const CONFIGURATOR_ROUTE = 'configureCPQCONFIGURATOR';
Expand Down Expand Up @@ -205,8 +206,11 @@ describe('ConfigurationFormComponent', () => {
provide: ConfiguratorGroupsService,
useClass: MockConfiguratorGroupsService,
},

{ provide: LanguageService, useValue: mockLanguageService },
{
provide: ConfiguratorStorefrontUtilsService,
useClass: ConfiguratorStorefrontUtilsService,
},
],
})
.overrideComponent(ConfiguratorAttributeHeaderComponent, {
Expand Down Expand Up @@ -396,4 +400,18 @@ describe('ConfigurationFormComponent', () => {

expect(configuratorCommonsService.updateConfiguration).toHaveBeenCalled();
});

describe('createGroupId', () => {
it('should return empty string because groupID is null', () => {
expect(createComponent().createGroupId(null)).toBeUndefined();
});

it('should return empty string because groupID is undefined', () => {
expect(createComponent().createGroupId(undefined)).toBeUndefined();
});

it('should return group ID string', () => {
expect(createComponent().createGroupId('1234')).toBe('1234-group');
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { ConfiguratorCommonsService } from '../../core/facade/configurator-commo
import { ConfiguratorGroupsService } from '../../core/facade/configurator-groups.service';
import { Configurator } from '../../core/model/configurator.model';
import { ConfigFormUpdateEvent } from './configurator-form.event';
import { ConfiguratorStorefrontUtilsService } from '../service/configurator-storefront-utils.service';

@Component({
selector: 'cx-configurator-form',
Expand Down Expand Up @@ -46,7 +47,8 @@ export class ConfiguratorFormComponent implements OnInit {
protected configuratorCommonsService: ConfiguratorCommonsService,
protected configuratorGroupsService: ConfiguratorGroupsService,
protected configRouterExtractorService: ConfiguratorRouterExtractorService,
protected languageService: LanguageService
protected languageService: LanguageService,
protected configUtils: ConfiguratorStorefrontUtilsService
) {}

ngOnInit(): void {
Expand Down Expand Up @@ -96,4 +98,14 @@ export class ConfiguratorFormComponent implements OnInit {
isConflictGroupType(groupType: Configurator.GroupType): boolean {
return this.configuratorGroupsService.isConflictGroupType(groupType);
}

/**
* Generates a group ID.
*
* @param {string} groupId - group ID
* @returns {string | undefined} - generated group ID
*/
createGroupId(groupId?: string): string | undefined {
return this.configUtils.createGroupId(groupId);
}
}
Loading

0 comments on commit bab6215

Please sign in to comment.