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"
|
fetchPriority="high"
|
||||||
loading="eager"
|
loading="eager"
|
||||||
alt={this.props.productName || 'Produktbild'}
|
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={{
|
sx={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -240,6 +246,12 @@ class Images extends Component {
|
|||||||
component="img"
|
component="img"
|
||||||
height="80"
|
height="80"
|
||||||
alt={`${this.props.productName || 'Produktbild'} - Bild ${originalIndex + 1}`}
|
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={{
|
sx={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
@@ -314,6 +326,12 @@ class Images extends Component {
|
|||||||
<CardMedia
|
<CardMedia
|
||||||
component="img"
|
component="img"
|
||||||
alt={this.props.productName || 'Produktbild'}
|
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={{
|
sx={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
width: '90vw',
|
width: '90vw',
|
||||||
@@ -368,6 +386,12 @@ class Images extends Component {
|
|||||||
component="img"
|
component="img"
|
||||||
height="60"
|
height="60"
|
||||||
alt={`${this.props.productName || 'Produktbild'} - Miniaturansicht ${originalIndex + 1}`}
|
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={{
|
sx={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
|
|||||||
@@ -308,6 +308,12 @@ class Product extends Component {
|
|||||||
alt={name}
|
alt={name}
|
||||||
fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'}
|
fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'}
|
||||||
loading={this.props.priority === 'high' ? 'eager' : 'lazy'}
|
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={{
|
sx={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
borderTopLeftRadius: '8px',
|
borderTopLeftRadius: '8px',
|
||||||
@@ -323,6 +329,12 @@ class Product extends Component {
|
|||||||
alt={name}
|
alt={name}
|
||||||
fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'}
|
fetchPriority={this.props.priority === 'high' ? 'high' : 'auto'}
|
||||||
loading={this.props.priority === 'high' ? 'eager' : 'lazy'}
|
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={{
|
sx={{
|
||||||
objectFit: 'contain',
|
objectFit: 'contain',
|
||||||
borderTopLeftRadius: '8px',
|
borderTopLeftRadius: '8px',
|
||||||
|
|||||||
@@ -33,6 +33,12 @@ const ProductImage = ({
|
|||||||
alt={product.name}
|
alt={product.name}
|
||||||
fetchPriority="high"
|
fetchPriority="high"
|
||||||
loading="eager"
|
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" }}
|
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