🎨 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
|
## 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.
|
**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.
|
**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 (
|
return (
|
||||||
<Link href={href} className="album-header__back" aria-label={`Back to ${label}`}>
|
<Link href={href} className="album-header__back" aria-label={`Back to ${label}`}>
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|||||||
@@ -40,6 +40,7 @@ export function Footer() {
|
|||||||
aria-label="Instagram"
|
aria-label="Instagram"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -58,6 +59,7 @@ export function Footer() {
|
|||||||
{footer?.email && (
|
{footer?.email && (
|
||||||
<a href={`mailto:${footer.email}`} className="footer__link" aria-label="Email">
|
<a href={`mailto:${footer.email}`} className="footer__link" aria-label="Email">
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -81,6 +83,7 @@ export function Footer() {
|
|||||||
aria-label="Website"
|
aria-label="Website"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev }: Ligh
|
|||||||
{/* Close button */}
|
{/* Close button */}
|
||||||
<button className={styles.close} onClick={onClose} aria-label="Close" title="Close (Esc)">
|
<button className={styles.close} onClick={onClose} aria-label="Close" title="Close (Esc)">
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -131,6 +132,7 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev }: Ligh
|
|||||||
title="Previous photo (Left arrow)"
|
title="Previous photo (Left arrow)"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
@@ -171,6 +173,7 @@ export function Lightbox({ assets, currentIndex, onClose, onNext, onPrev }: Ligh
|
|||||||
title="Next photo (Right arrow)"
|
title="Next photo (Right arrow)"
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke="currentColor"
|
stroke="currentColor"
|
||||||
|
|||||||
@@ -151,7 +151,10 @@ export function MapView() {
|
|||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
return (
|
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>
|
<p>{error}</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -162,27 +165,30 @@ export function MapView() {
|
|||||||
{loading && (
|
{loading && (
|
||||||
<div
|
<div
|
||||||
className="map-container__loading"
|
className="map-container__loading"
|
||||||
style={{
|
role="alert"
|
||||||
position: 'absolute',
|
aria-live="polite"
|
||||||
inset: 0,
|
style={{
|
||||||
|
position: 'absolute',
|
||||||
|
inset: 0,
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
flexDirection: 'column',
|
flexDirection: 'column',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
background: 'rgba(0, 0, 0, 0.5)',
|
background: 'rgba(0, 0, 0, 0.5)',
|
||||||
color: '#fff'
|
color: '#fff',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
width="24"
|
aria-hidden="true"
|
||||||
height="24"
|
width="24"
|
||||||
viewBox="0 0 24 24"
|
height="24"
|
||||||
fill="none"
|
viewBox="0 0 24 24"
|
||||||
stroke="currentColor"
|
fill="none"
|
||||||
strokeWidth="2"
|
stroke="currentColor"
|
||||||
strokeLinecap="round"
|
strokeWidth="2"
|
||||||
strokeLinejoin="round"
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
style={{ animation: 'spin 1s linear infinite', marginBottom: '8px' }}
|
style={{ animation: 'spin 1s linear infinite', marginBottom: '8px' }}
|
||||||
>
|
>
|
||||||
<circle cx="12" cy="12" r="10" strokeOpacity="0.25"></circle>
|
<circle cx="12" cy="12" r="10" strokeOpacity="0.25"></circle>
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ export function ThemeToggle() {
|
|||||||
{theme === 'dark' ? (
|
{theme === 'dark' ? (
|
||||||
// Sun icon
|
// Sun icon
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
@@ -85,6 +86,7 @@ export function ThemeToggle() {
|
|||||||
) : (
|
) : (
|
||||||
// Moon icon
|
// Moon icon
|
||||||
<svg
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
viewBox="0 0 24 24"
|
viewBox="0 0 24 24"
|
||||||
|
|||||||
Reference in New Issue
Block a user