fix: WCAG 2.2.2 hero autoplay pause control, invisible keyboard-focusable cart button, literal hex token
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
Some checks failed
Architecture Governance / architecture (push) Has been cancelled
- Hero widget autoplay had no pause control and ignored prefers-reduced-motion (WCAG 2.2.2 requires a way to pause auto-updating content lasting >5s). Added a pause/resume toggle button and skip autoplay entirely when the OS prefers reduced motion. - Cart's swipe-reveal delete-btn-mobile was reachable by Tab even while invisible (opacity: 0, only the touch-swipe gesture could reveal it) - a confusing, unusable focus stop for keyboard users. Now tabindex=-1 + aria-hidden until swiped. Keyboard users already had a full removal path via the always-visible header remove button; this just stops the redundant hidden button from being a dead tab stop. - stars.component.scss hardcoded #cdd6d5 for the unfilled-star color instead of the --border-color design token. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -61,6 +61,15 @@ const SWIPE_THRESHOLD_PX = 50;
|
||||
(click)="goTo($index)"
|
||||
></button>
|
||||
}
|
||||
@if (data?.autoplay) {
|
||||
<button
|
||||
type="button"
|
||||
class="hero-widget__pause"
|
||||
[attr.aria-label]="(isPaused() ? 'common.resumeAutoplay' : 'common.pauseAutoplay') | translate"
|
||||
[attr.aria-pressed]="isPaused()"
|
||||
(click)="toggleAutoplay()"
|
||||
>{{ isPaused() ? '▶' : '⏸' }}</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -171,6 +180,23 @@ const SWIPE_THRESHOLD_PX = 50;
|
||||
&:focus-visible { outline: 2px solid var(--primary-color, #497671); outline-offset: 2px; }
|
||||
}
|
||||
|
||||
.hero-widget__pause {
|
||||
margin-left: var(--space-sm, 8px);
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border-radius: 50%;
|
||||
border: 1px solid var(--border-color, #d3dad9);
|
||||
background: #fff;
|
||||
cursor: pointer;
|
||||
font-size: 0.7rem;
|
||||
line-height: 1;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&:focus-visible { outline: 2px solid var(--primary-color, #497671); outline-offset: 2px; }
|
||||
}
|
||||
|
||||
@keyframes hero-widget-in {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
@@ -199,6 +225,7 @@ export class HeroWidgetComponent implements OnChanges, OnDestroy {
|
||||
@Output() ctaClicked = new EventEmitter<void>();
|
||||
|
||||
readonly activeIndex = signal(0);
|
||||
readonly isPaused = signal(false);
|
||||
private readonly dataSignal = signal<HeroWidgetData | null>(null);
|
||||
private autoplayHandle: ReturnType<typeof setInterval> | null = null;
|
||||
private swipeStartX: number | null = null;
|
||||
@@ -237,6 +264,7 @@ export class HeroWidgetComponent implements OnChanges, OnDestroy {
|
||||
if (changes['data']) {
|
||||
this.dataSignal.set(this.data);
|
||||
this.activeIndex.set(0);
|
||||
this.isPaused.set(false);
|
||||
this.setupAutoplay();
|
||||
}
|
||||
}
|
||||
@@ -289,10 +317,20 @@ export class HeroWidgetComponent implements OnChanges, OnDestroy {
|
||||
this.ctaClicked.emit();
|
||||
}
|
||||
|
||||
/** WCAG 2.2.2: auto-updating content lasting >5s needs a way to pause it. */
|
||||
toggleAutoplay(): void {
|
||||
this.isPaused.update(paused => !paused);
|
||||
this.setupAutoplay();
|
||||
}
|
||||
|
||||
private prefersReducedMotion(): boolean {
|
||||
return typeof window !== 'undefined' && !!window.matchMedia?.('(prefers-reduced-motion: reduce)').matches;
|
||||
}
|
||||
|
||||
private setupAutoplay(): void {
|
||||
this.clearAutoplay();
|
||||
const slides = this.allSlides();
|
||||
if (!this.data?.autoplay || slides.length <= 1) {
|
||||
if (!this.data?.autoplay || slides.length <= 1 || this.isPaused() || this.prefersReducedMotion()) {
|
||||
return;
|
||||
}
|
||||
this.autoplayHandle = setInterval(() => {
|
||||
|
||||
Reference in New Issue
Block a user