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:
@@ -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.
|
||||||
|
|||||||
@@ -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>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user