Skip to content

Commit dd23e20

Browse files
authored
fix: persist focus on consent (#21309)
1 parent 2b7db39 commit dd23e20

8 files changed

Lines changed: 75 additions & 4 deletions

File tree

projects/core/src/features-config/feature-toggles/config/feature-toggles.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -469,6 +469,14 @@ export interface FeatureTogglesInterface {
469469
* When enabled, displays required field asterisks for form fields.
470470
*/
471471
showRequiredAsterisks?: boolean;
472+
473+
/**
474+
* Preserves keyboard focus on consent checkboxes after toggling.
475+
* Treats Space/Enter on checkbox/radio as navigation in VisibleFocusDirective
476+
* and restores focus after the consent form is temporarily disabled.
477+
* Affects: VisibleFocusDirective, ConsentManagementFormComponent, ConsentManagementComponent
478+
*/
479+
a11yConsentManagementFocusPreservation?: boolean;
472480
}
473481

474482
export const defaultFeatureToggles: Required<FeatureTogglesInterface> = {
@@ -521,6 +529,7 @@ export const defaultFeatureToggles: Required<FeatureTogglesInterface> = {
521529
a11yCartItemListHideEmptyOutlets: false,
522530
a11yReviewsKeyboardControls: false,
523531
a11yCartQuickOrderFormEnableSubmitAndAddValidation: false,
532+
a11yConsentManagementFocusPreservation: false,
524533
a11yVocalizeDropdownItemCount: false,
525534
useEnhancedSecurePasswordValidators: false,
526535
enableRemoveVoucherEndpoint: false,

projects/storefrontapp/src/app/spartacus/spartacus-features.module.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,7 @@ if (environment.cpq) {
345345
a11yCartItemListHideEmptyOutlets: true,
346346
a11yReviewsKeyboardControls: true,
347347
a11yCartQuickOrderFormEnableSubmitAndAddValidation: true,
348+
a11yConsentManagementFocusPreservation: true,
348349
a11yVocalizeDropdownItemCount: true,
349350
useEnhancedSecurePasswordValidators: true,
350351
enableRemoveVoucherEndpoint: true,

projects/storefrontlib/cms-components/myaccount/consent-management/components/consent-form/consent-management-form.component.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
>
55
<label>
66
<input
7+
#checkboxInput
78
type="checkbox"
89
class="form-check-input"
910
(change)="onConsentChange()"

projects/storefrontlib/cms-components/myaccount/consent-management/components/consent-form/consent-management-form.component.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@
66

77
import { NgIf, NgTemplateOutlet } from '@angular/common';
88
import {
9+
AfterViewChecked,
910
Component,
11+
ElementRef,
1012
EventEmitter,
13+
inject,
1114
Input,
1215
OnChanges,
1316
OnInit,
1417
Output,
1518
SimpleChanges,
19+
ViewChild,
1620
} from '@angular/core';
1721
import {
1822
ANONYMOUS_CONSENT_STATUS,
1923
AnonymousConsent,
2024
ConsentTemplate,
25+
FeatureConfigService,
2126
TranslatePipe,
2227
} from '@spartacus/core';
2328

@@ -26,7 +31,9 @@ import {
2631
templateUrl: './consent-management-form.component.html',
2732
imports: [NgIf, NgTemplateOutlet, TranslatePipe],
2833
})
29-
export class ConsentManagementFormComponent implements OnInit, OnChanges {
34+
export class ConsentManagementFormComponent
35+
implements OnInit, OnChanges, AfterViewChecked
36+
{
3037
consentGiven = false;
3138

3239
@Input()
@@ -49,6 +56,12 @@ export class ConsentManagementFormComponent implements OnInit, OnChanges {
4956
template: ConsentTemplate;
5057
}>();
5158

59+
@ViewChild('checkboxInput') checkboxInput: ElementRef<HTMLInputElement>;
60+
61+
private hadFocus = false;
62+
private document = inject(ElementRef).nativeElement.ownerDocument;
63+
private featureConfigService = inject(FeatureConfigService);
64+
5265
constructor() {
5366
// Intentional empty constructor
5467
}
@@ -61,6 +74,29 @@ export class ConsentManagementFormComponent implements OnInit, OnChanges {
6174
if (changes.consent || changes.consentTemplate) {
6275
this.updateConsentGiven();
6376
}
77+
if (
78+
changes.disabled?.currentValue === true &&
79+
this.featureConfigService.isEnabled(
80+
'a11yConsentManagementFocusPreservation'
81+
)
82+
) {
83+
this.hadFocus =
84+
this.checkboxInput?.nativeElement === this.document.activeElement;
85+
}
86+
}
87+
88+
ngAfterViewChecked(): void {
89+
if (
90+
this.hadFocus &&
91+
!this.disabled &&
92+
this.checkboxInput?.nativeElement &&
93+
this.featureConfigService.isEnabled(
94+
'a11yConsentManagementFocusPreservation'
95+
)
96+
) {
97+
this.hadFocus = false;
98+
this.checkboxInput.nativeElement.focus();
99+
}
64100
}
65101

66102
onConsentChange(): void {

projects/storefrontlib/cms-components/myaccount/consent-management/components/consent-management.component.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,10 @@
3535
<div class="cx-consent-toggles">
3636
<div class="col-sm-12 col-md-8 col-lg-6">
3737
<cx-consent-management-form
38-
*ngFor="let consentTemplate of templateList"
38+
*ngFor="
39+
let consentTemplate of templateList;
40+
trackBy: trackByTemplateId
41+
"
3942
[consentTemplate]="consentTemplate"
4043
[requiredConsents]="requiredConsents"
4144
[disabled]="!!data.loading"

projects/storefrontlib/cms-components/myaccount/consent-management/components/consent-management.component.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,10 @@ export class ConsentManagementComponent implements OnInit, OnDestroy {
347347
return checkTimesLoaded$;
348348
}
349349

350+
trackByTemplateId(_index: number, item: ConsentTemplate): string {
351+
return item.id ?? '';
352+
}
353+
350354
private isRequiredConsent(template: ConsentTemplate): boolean {
351355
return Boolean(
352356
template.id &&

projects/storefrontlib/layout/a11y/keyboard-focus/visible/visible-focus.directive.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import { Directive, HostBinding, HostListener } from '@angular/core';
7+
import { Directive, HostBinding, HostListener, inject } from '@angular/core';
8+
import { FeatureConfigService } from '@spartacus/core';
89
import { BaseFocusDirective } from '../base/base-focus.directive';
910
import { VisibleFocusConfig } from '../keyboard-focus.model';
1011

@@ -26,6 +27,8 @@ export class VisibleFocusDirective extends BaseFocusDirective {
2627
disableMouseFocus: true,
2728
};
2829

30+
private featureConfigService = inject(FeatureConfigService);
31+
2932
// @Input('cxVisibleFocus')
3033
protected config: VisibleFocusConfig;
3134

@@ -65,7 +68,22 @@ export class VisibleFocusDirective extends BaseFocusDirective {
6568
return true;
6669
}
6770
// If the user fill in a form, we don't considering it part of storefront navigation.
71+
// However, pressing Space/Enter on a checkbox or radio button is a toggle action
72+
// (not typing), so we treat it as navigation to preserve the focus outline.
6873
if (['INPUT', 'TEXTAREA'].includes((event.target as HTMLElement).tagName)) {
74+
if (
75+
this.featureConfigService.isEnabled(
76+
'a11yConsentManagementFocusPreservation'
77+
)
78+
) {
79+
const inputType = (event.target as HTMLInputElement).type;
80+
if (
81+
(event.code === 'Space' || event.code === 'Enter') &&
82+
(inputType === 'checkbox' || inputType === 'radio')
83+
) {
84+
return true;
85+
}
86+
}
6987
return false;
7088
}
7189
return true;

projects/storefrontlib/shared/components/anonymous-consents-dialog/anonymous-consent-dialog.component.html

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,6 @@ <h3 id="dialogTitle">
4141
[type]="message.type"
4242
[isVisibleCloseButton]="true"
4343
(closeMessage)="closeMessage()"
44-
[cxFocus]="{ autofocus: '.cx-message' }"
4544
></cx-message>
4645
</div>
4746
<!-- Actions -->

0 commit comments

Comments
 (0)