feat: implement accessibility improvements by ensuring alt text is always present on image error events and initialize accessibility checking in App component
This commit is contained in:
@@ -176,6 +176,12 @@ class Images extends Component {
|
||||
fetchPriority="high"
|
||||
loading="eager"
|
||||
alt={this.props.productName || 'Produktbild'}
|
||||
onError={(e) => {
|
||||
// Ensure alt text is always present even on error
|
||||
if (!e.target.alt) {
|
||||
e.target.alt = this.props.productName || 'Produktbild';
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
cursor: 'pointer',
|
||||
@@ -240,6 +246,12 @@ class Images extends Component {
|
||||
component="img"
|
||||
height="80"
|
||||
alt={`${this.props.productName || 'Produktbild'} - Bild ${originalIndex + 1}`}
|
||||
onError={(e) => {
|
||||
// Ensure alt text is always present even on error
|
||||
if (!e.target.alt) {
|
||||
e.target.alt = `${this.props.productName || 'Produktbild'} - Bild ${originalIndex + 1}`;
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
cursor: 'pointer',
|
||||
@@ -314,6 +326,12 @@ class Images extends Component {
|
||||
<CardMedia
|
||||
component="img"
|
||||
alt={this.props.productName || 'Produktbild'}
|
||||
onError={(e) => {
|
||||
// Ensure alt text is always present even on error
|
||||
if (!e.target.alt) {
|
||||
e.target.alt = this.props.productName || 'Produktbild';
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
width: '90vw',
|
||||
@@ -368,6 +386,12 @@ class Images extends Component {
|
||||
component="img"
|
||||
height="60"
|
||||
alt={`${this.props.productName || 'Produktbild'} - Miniaturansicht ${originalIndex + 1}`}
|
||||
onError={(e) => {
|
||||
// Ensure alt text is always present even on error
|
||||
if (!e.target.alt) {
|
||||
e.target.alt = `${this.props.productName || 'Produktbild'} - Miniaturansicht ${originalIndex + 1}`;
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
cursor: 'pointer',
|
||||
|
||||
@@ -308,6 +308,12 @@ class Product extends Component {
|
||||
alt={name}
|
||||
fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'}
|
||||
loading={this.props.priority === 'high' ? 'eager' : 'lazy'}
|
||||
onError={(e) => {
|
||||
// Ensure alt text is always present even on error
|
||||
if (!e.target.alt) {
|
||||
e.target.alt = name || 'Produktbild';
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
borderTopLeftRadius: '8px',
|
||||
@@ -323,6 +329,12 @@ class Product extends Component {
|
||||
alt={name}
|
||||
fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'}
|
||||
loading={this.props.priority === 'high' ? 'eager' : 'lazy'}
|
||||
onError={(e) => {
|
||||
// Ensure alt text is always present even on error
|
||||
if (!e.target.alt) {
|
||||
e.target.alt = name || 'Produktbild';
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
objectFit: 'contain',
|
||||
borderTopLeftRadius: '8px',
|
||||
|
||||
@@ -33,6 +33,12 @@ const ProductImage = ({
|
||||
alt={product.name}
|
||||
fetchPriority="high"
|
||||
loading="eager"
|
||||
onError={(e) => {
|
||||
// Ensure alt text is always present even on error
|
||||
if (!e.target.alt) {
|
||||
e.target.alt = product.name || 'Produktbild';
|
||||
}
|
||||
}}
|
||||
sx={{ objectFit: "cover" }}
|
||||
/>
|
||||
)}
|
||||
|
||||
221
src/utils/accessibilityUtils.js
Normal file
221
src/utils/accessibilityUtils.js
Normal file
@@ -0,0 +1,221 @@
|
||||
// Accessibility utility functions for ensuring proper button labels
|
||||
|
||||
/**
|
||||
* Checks if all IconButtons on the page have proper aria-labels
|
||||
* This function can be called in development to identify missing labels
|
||||
*/
|
||||
export const validateIconButtonAccessibility = () => {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
// Find all IconButton elements
|
||||
const iconButtons = document.querySelectorAll('[class*="MuiIconButton"]');
|
||||
const missingLabels = [];
|
||||
|
||||
iconButtons.forEach((button, index) => {
|
||||
const hasAriaLabel = button.hasAttribute('aria-label');
|
||||
const hasAriaLabelledBy = button.hasAttribute('aria-labelledby');
|
||||
const hasTitle = button.hasAttribute('title');
|
||||
const isInTooltip = button.closest('[role="tooltip"]') !== null;
|
||||
|
||||
// Check if button has any form of accessible name
|
||||
if (!hasAriaLabel && !hasAriaLabelledBy && !hasTitle && !isInTooltip) {
|
||||
missingLabels.push({
|
||||
element: button,
|
||||
index,
|
||||
classes: button.className,
|
||||
parentElement: button.parentElement?.tagName,
|
||||
parentClasses: button.parentElement?.className
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (missingLabels.length > 0) {
|
||||
console.warn('IconButtons missing accessibility labels:', missingLabels);
|
||||
return missingLabels;
|
||||
}
|
||||
|
||||
console.log('✅ All IconButtons have proper accessibility labels');
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Checks if all images on the page have proper alt attributes
|
||||
* This function can be called in development to identify missing alt text
|
||||
*/
|
||||
export const validateImageAccessibility = () => {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
// Find all img elements
|
||||
const images = document.querySelectorAll('img');
|
||||
const missingAltText = [];
|
||||
|
||||
images.forEach((img, index) => {
|
||||
const hasAlt = img.hasAttribute('alt');
|
||||
const altValue = img.getAttribute('alt');
|
||||
const isDecorative = altValue === '';
|
||||
const src = img.src;
|
||||
|
||||
// Check if image has alt attribute
|
||||
if (!hasAlt) {
|
||||
missingAltText.push({
|
||||
element: img,
|
||||
index,
|
||||
src: src,
|
||||
classes: img.className,
|
||||
parentElement: img.parentElement?.tagName,
|
||||
parentClasses: img.parentElement?.className,
|
||||
issue: 'Missing alt attribute'
|
||||
});
|
||||
} else if (altValue && altValue.trim() === '' && !isDecorative) {
|
||||
// Empty alt text for non-decorative images
|
||||
missingAltText.push({
|
||||
element: img,
|
||||
index,
|
||||
src: src,
|
||||
classes: img.className,
|
||||
parentElement: img.parentElement?.tagName,
|
||||
parentClasses: img.parentElement?.className,
|
||||
issue: 'Empty alt text for informative image'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if (missingAltText.length > 0) {
|
||||
console.warn('Images missing accessibility alt text:', missingAltText);
|
||||
return missingAltText;
|
||||
}
|
||||
|
||||
console.log('✅ All images have proper alt attributes');
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically adds alt attributes to images that are missing them
|
||||
* This is a fallback solution for development
|
||||
*/
|
||||
export const addFallbackAltText = () => {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
const images = document.querySelectorAll('img:not([alt])');
|
||||
|
||||
images.forEach((img) => {
|
||||
const src = img.src || '';
|
||||
let fallbackAlt = 'Bild';
|
||||
|
||||
// Try to determine alt text from src or context
|
||||
if (src.includes('prod')) {
|
||||
fallbackAlt = 'Produktbild';
|
||||
} else if (src.includes('cat')) {
|
||||
fallbackAlt = 'Kategoriebild';
|
||||
} else if (src.includes('logo')) {
|
||||
fallbackAlt = 'Logo';
|
||||
} else if (src.includes('nopicture')) {
|
||||
fallbackAlt = 'Kein Bild verfügbar';
|
||||
} else if (src.includes('404')) {
|
||||
fallbackAlt = '404 - Seite nicht gefunden';
|
||||
} else if (src.includes('seeds')) {
|
||||
fallbackAlt = 'Seeds';
|
||||
} else if (src.includes('cutlings')) {
|
||||
fallbackAlt = 'Stecklinge';
|
||||
} else if (src.includes('filiale')) {
|
||||
fallbackAlt = 'Filiale';
|
||||
} else if (src.includes('presse')) {
|
||||
fallbackAlt = 'Presse';
|
||||
}
|
||||
|
||||
img.setAttribute('alt', fallbackAlt);
|
||||
console.warn(`Added fallback alt text "${fallbackAlt}" to image:`, img);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Automatically adds aria-labels to IconButtons that are missing them
|
||||
* This is a fallback solution for development
|
||||
*/
|
||||
export const addFallbackAriaLabels = () => {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
const iconButtons = document.querySelectorAll('[class*="MuiIconButton"]:not([aria-label]):not([aria-labelledby]):not([title])');
|
||||
|
||||
iconButtons.forEach((button) => {
|
||||
// Try to determine button function from context
|
||||
const icon = button.querySelector('[class*="MuiSvgIcon"]');
|
||||
const iconClass = icon?.className || '';
|
||||
|
||||
let fallbackLabel = 'Button';
|
||||
|
||||
// Determine label based on icon type or context
|
||||
if (iconClass.includes('Close')) {
|
||||
fallbackLabel = 'Schließen';
|
||||
} else if (iconClass.includes('Search')) {
|
||||
fallbackLabel = 'Suchen';
|
||||
} else if (iconClass.includes('Add')) {
|
||||
fallbackLabel = 'Hinzufügen';
|
||||
} else if (iconClass.includes('Remove')) {
|
||||
fallbackLabel = 'Entfernen';
|
||||
} else if (iconClass.includes('Delete')) {
|
||||
fallbackLabel = 'Löschen';
|
||||
} else if (iconClass.includes('Edit')) {
|
||||
fallbackLabel = 'Bearbeiten';
|
||||
} else if (iconClass.includes('Expand')) {
|
||||
fallbackLabel = 'Erweitern';
|
||||
} else if (iconClass.includes('ShoppingCart')) {
|
||||
fallbackLabel = 'Warenkorb';
|
||||
} else if (iconClass.includes('Zoom')) {
|
||||
fallbackLabel = 'Vergrößern';
|
||||
}
|
||||
|
||||
button.setAttribute('aria-label', fallbackLabel);
|
||||
console.warn(`Added fallback aria-label "${fallbackLabel}" to IconButton:`, button);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Comprehensive accessibility validation
|
||||
*/
|
||||
export const validateAllAccessibility = () => {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
console.log('🔍 Running comprehensive accessibility validation...');
|
||||
|
||||
const iconButtonIssues = validateIconButtonAccessibility();
|
||||
const imageIssues = validateImageAccessibility();
|
||||
|
||||
if (iconButtonIssues.length === 0 && imageIssues.length === 0) {
|
||||
console.log('✅ All accessibility checks passed!');
|
||||
} else {
|
||||
console.warn(`❌ Found ${iconButtonIssues.length + imageIssues.length} accessibility issues`);
|
||||
|
||||
// Auto-fix issues in development
|
||||
if (iconButtonIssues.length > 0) {
|
||||
addFallbackAriaLabels();
|
||||
}
|
||||
if (imageIssues.length > 0) {
|
||||
addFallbackAltText();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Initialize accessibility checking on page load
|
||||
*/
|
||||
export const initializeAccessibilityChecking = () => {
|
||||
if (process.env.NODE_ENV !== 'development') return;
|
||||
|
||||
// Check after initial render
|
||||
setTimeout(() => {
|
||||
validateAllAccessibility();
|
||||
}, 1000);
|
||||
|
||||
// Check after dynamic content loads
|
||||
const observer = new MutationObserver(() => {
|
||||
setTimeout(() => {
|
||||
validateAllAccessibility();
|
||||
}, 100);
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user