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

@@ -7,7 +7,7 @@
}
.star {
color: #cdd6d5;
color: var(--border-color);
}
.star.filled {

View File

@@ -1129,6 +1129,8 @@ export const en: Translations = {
nextProducts: 'Next products',
previousSlide: 'Previous slide',
nextSlide: 'Next slide',
pauseAutoplay: 'Pause slideshow',
resumeAutoplay: 'Resume slideshow',
closeDialog: 'Close dialog',
dismiss: 'Dismiss',
qrCode: 'QR Code',

View File

@@ -1129,6 +1129,8 @@ export const hy: Translations = {
nextProducts: 'Հաջորդ ապրանքները',
previousSlide: 'Նախորդ սլայդը',
nextSlide: 'Հաջորդ սլայդը',
pauseAutoplay: 'Դադարեցնել սլայդշոուն',
resumeAutoplay: 'Վերսկսել սլայդշոուն',
closeDialog: 'Փակել պատուհանը',
dismiss: 'Փակել',
qrCode: 'QR կոդ',

View File

@@ -1129,6 +1129,8 @@ export const ru: Translations = {
nextProducts: 'Следующие товары',
previousSlide: 'Предыдущий слайд',
nextSlide: 'Следующий слайд',
pauseAutoplay: 'Приостановить слайд-шоу',
resumeAutoplay: 'Возобновить слайд-шоу',
closeDialog: 'Закрыть диалог',
dismiss: 'Скрыть',
qrCode: 'QR-код',

View File

@@ -1128,6 +1128,8 @@ export interface Translations {
nextProducts: string;
previousSlide: string;
nextSlide: string;
pauseAutoplay: string;
resumeAutoplay: string;
closeDialog: string;
dismiss: string;
qrCode: string;

View File

@@ -98,7 +98,13 @@
</div>
</div>
<button class="delete-btn-mobile" (click)="removeItem(item)" [attr.aria-label]="'cart.removeItem' | translate">
<button
class="delete-btn-mobile"
(click)="removeItem(item)"
[attr.aria-label]="'cart.removeItem' | translate"
[attr.tabindex]="swipedItemId() === item.itemID ? null : -1"
[attr.aria-hidden]="swipedItemId() === item.itemID ? null : 'true'"
>
<app-icon name="trash" [size]="20" />
</button>
</div>

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