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

- 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:
sdarbinyan
2026-08-13 10:12:41 +04:00
parent 178b5f0dc7
commit bad3002006
7 changed files with 55 additions and 3 deletions

View File

@@ -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(() => {