feat(a11y): consolidate ARIA announcements on interactive cards (#133)

- Added comprehensive `aria-label` to the parent `<Link>` in `SubpageGridView`.
- Hid redundant textual child elements using `aria-hidden="true"`.
- Removed redundant `alt` text from decorative thumbnail `Image`.
- Removed unused `albumMap` prop in `AlbumGrid`.

Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
Ralksta
2026-04-21 22:51:26 +02:00
committed by GitHub
parent fef4c73e86
commit e8709b9ae9
2 changed files with 15 additions and 18 deletions

View File

@@ -5,3 +5,7 @@
## 2024-05-25 - SVG Icon Accessibility ## 2024-05-25 - SVG Icon Accessibility
**Learning:** Adding `aria-hidden="true"` to decorative `<svg>` icons within interactive elements (like `<button>` or `<a>`) that already have an `aria-label` is crucial. Otherwise, screen readers may read the raw vector nodes or fall back to confusing announcements in addition to the label. Similarly, when adding text to visual spinners, use `role="alert"` and `aria-live="polite"` on the container while hiding the SVG graphic. **Learning:** Adding `aria-hidden="true"` to decorative `<svg>` icons within interactive elements (like `<button>` or `<a>`) that already have an `aria-label` is crucial. Otherwise, screen readers may read the raw vector nodes or fall back to confusing announcements in addition to the label. Similarly, when adding text to visual spinners, use `role="alert"` and `aria-live="polite"` on the container while hiding the SVG graphic.
**Action:** When adding or modifying interactive elements with icon graphics, always ensure the graphic is explicitly hidden from screen readers using `aria-hidden="true"` if a text alternative (`aria-label`) is provided. **Action:** When adding or modifying interactive elements with icon graphics, always ensure the graphic is explicitly hidden from screen readers using `aria-hidden="true"` if a text alternative (`aria-label`) is provided.
## 2024-05-26 - Interactive Card ARIA Labels
**Learning:** When building interactive card components that contain multiple pieces of text or badges (like titles and photo counts), screen readers may announce them in a fragmented or confusing way.
**Action:** Always apply a comprehensive `aria-label` to the parent link/container that encompasses all meaningful visual data (e.g., titles and item counts). Use `aria-hidden="true"` on redundant text or visual child elements, and set `alt=""` on decorative images to consolidate screen reader announcements into a single, accurate interaction point.

View File

@@ -27,12 +27,10 @@ interface SubpageGridViewProps {
function AlbumGrid({ function AlbumGrid({
albums, albums,
albumMap,
placeholderMap, placeholderMap,
slug, slug,
}: { }: {
albums: SubpageAlbum[]; albums: SubpageAlbum[];
albumMap: Map<string, SubpageAlbum>;
placeholderMap: Map<string, Placeholder | null>; placeholderMap: Map<string, Placeholder | null>;
slug: string; slug: string;
}) { }) {
@@ -46,23 +44,28 @@ function AlbumGrid({
href={`/${slug}/${album.slug}`} href={`/${slug}/${album.slug}`}
className="subpage-grid__item" className="subpage-grid__item"
style={ph?.dominantColor ? { backgroundColor: ph.dominantColor } : undefined} style={ph?.dominantColor ? { backgroundColor: ph.dominantColor } : undefined}
aria-label={`${album.albumName}, ${album.assetCount} ${album.assetCount === 1 ? 'photo' : 'photos'}`}
> >
{album.albumThumbnailAssetId ? ( {album.albumThumbnailAssetId ? (
<Image <Image
src={imageUrl(album.albumThumbnailAssetId, 'preview')} src={imageUrl(album.albumThumbnailAssetId, 'preview')}
alt={album.albumName} alt=""
fill fill
sizes="(max-width: 600px) 100vw, (max-width: 1000px) 50vw, 33vw" sizes="(max-width: 600px) 100vw, (max-width: 1000px) 50vw, 33vw"
loading="lazy" loading="lazy"
{...(ph ? { placeholder: 'blur' as const, blurDataURL: ph.blurDataURL } : {})} {...(ph ? { placeholder: 'blur' as const, blurDataURL: ph.blurDataURL } : {})}
/> />
) : ( ) : (
<div className="skeleton" style={{ width: '100%', height: '100%' }} /> <div
className="skeleton"
style={{ width: '100%', height: '100%' }}
aria-hidden="true"
/>
)} )}
<span className="subpage-grid__item-badge"> <span className="subpage-grid__item-badge" aria-hidden="true">
{album.assetCount} {album.assetCount === 1 ? 'photo' : 'photos'} {album.assetCount} {album.assetCount === 1 ? 'photo' : 'photos'}
</span> </span>
<div className="subpage-grid__item-overlay"> <div className="subpage-grid__item-overlay" aria-hidden="true">
<span className="subpage-grid__item-title">{album.albumName}</span> <span className="subpage-grid__item-title">{album.albumName}</span>
<span className="subpage-grid__item-count"> <span className="subpage-grid__item-count">
{album.assetCount} {album.assetCount === 1 ? 'photo' : 'photos'} {album.assetCount} {album.assetCount === 1 ? 'photo' : 'photos'}
@@ -126,23 +129,13 @@ export function SubpageGridView({
{sec.description && <p className="subpage-section__desc">{sec.description}</p>} {sec.description && <p className="subpage-section__desc">{sec.description}</p>}
<div className="subpage-section__rule" aria-hidden="true" /> <div className="subpage-section__rule" aria-hidden="true" />
</header> </header>
<AlbumGrid <AlbumGrid albums={sectionAlbums} placeholderMap={placeholderMap} slug={slug} />
albums={sectionAlbums}
albumMap={albumMap}
placeholderMap={placeholderMap}
slug={slug}
/>
</section> </section>
); );
}) })
) : ( ) : (
/* Flat layout (no sections) */ /* Flat layout (no sections) */
<AlbumGrid <AlbumGrid albums={albums} placeholderMap={placeholderMap} slug={slug} />
albums={albums}
albumMap={albumMap}
placeholderMap={placeholderMap}
slug={slug}
/>
)} )}
</div> </div>
); );