🎨 Palette: [UX improvement] Add aria-hidden to decorative SVGs (#70)
* Added aria-hidden="true" to decorative SVG icons within interactive elements (buttons, links) in Lightbox, ThemeToggle, Footer, BackLink, and MapView. * Added role="alert" and aria-live="polite" to the MapView loading indicator. * Documented learning in .Jules/palette.md Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,3 +1,7 @@
|
||||
## 2024-05-24 - Lightbox ARIA Attributes
|
||||
**Learning:** Adding `role="dialog"`, `aria-modal="true"`, and `aria-label` to custom modal/lightbox overlays ensures screen readers announce them properly instead of just falling back to reading inner content without context. Additionally, toggles that show/hide panels (like the EXIF info button) must use `aria-expanded` and link to the panel via `aria-controls` for proper screen reader communication.
|
||||
**Action:** When building or modifying custom overlays or toggle buttons in the future, always verify that ARIA attributes are set correctly to match the visual behavior.
|
||||
|
||||
## 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.
|
||||
**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.
|
||||
|
||||
@@ -14,6 +14,7 @@ export function BackLink({ href, label }: BackLinkProps) {
|
||||
return (
|
||||
<Link href={href} className="album-header__back" aria-label={`Back to ${label}`}>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -40,6 +40,7 @@ export function Footer() {
|
||||
aria-label="Instagram"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -58,6 +59,7 @@ export function Footer() {
|
||||
{footer?.email && (
|
||||
<a href={`mailto:${footer.email}`} className="footer__link" aria-label="Email">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -81,6 +83,7 @@ export function Footer() {
|
||||
aria-label="Website"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
@@ -111,6 +111,7 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev }: Ligh
|
||||
{/* Close button */}
|
||||
<button className={styles.close} onClick={onClose} aria-label="Close" title="Close (Esc)">
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -131,6 +132,7 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev }: Ligh
|
||||
title="Previous photo (Left arrow)"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
@@ -171,6 +173,7 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev }: Ligh
|
||||
title="Next photo (Right arrow)"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
|
||||
@@ -151,7 +151,10 @@ export function MapView() {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<div className="map-container__loading" style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}>
|
||||
<div
|
||||
className="map-container__loading"
|
||||
style={{ display: 'flex', alignItems: 'center', justifyContent: 'center', height: '100%' }}
|
||||
>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -162,27 +165,30 @@ export function MapView() {
|
||||
{loading && (
|
||||
<div
|
||||
className="map-container__loading"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
role="alert"
|
||||
aria-live="polite"
|
||||
style={{
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
background: 'rgba(0, 0, 0, 0.5)',
|
||||
color: '#fff'
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
style={{ animation: 'spin 1s linear infinite', marginBottom: '8px' }}
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" strokeOpacity="0.25"></circle>
|
||||
|
||||
@@ -63,6 +63,7 @@ export function ThemeToggle() {
|
||||
{theme === 'dark' ? (
|
||||
// Sun icon
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -85,6 +86,7 @@ export function ThemeToggle() {
|
||||
) : (
|
||||
// Moon icon
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
width="16"
|
||||
height="16"
|
||||
viewBox="0 0 24 24"
|
||||
|
||||
Reference in New Issue
Block a user