import UI from "../ui"; import React, { useMemo } from "react"; import type { ImgProps } from "./img.types"; /** * Img - A semantic image component with accessibility and performance best practices. * * This component wraps the native `` element with enhanced features: * - **Responsive images** via optional srcset/sizes * - **Lazy loading** by default for performance * - **Error handling** with configurable fallback placeholders * - **Type safety** with full TypeScript support * * ## Accessibility Patterns (WCAG 2.1 AA) * * ### Decorative Images * Images that are purely visual decoration should use an empty alt attribute. * These images are typically borders, patterns, or visual separators. * * @example * ```tsx * // ✅ GOOD: Decorative border image * * * // ✅ GOOD: Background pattern * * ``` * * ### Semantic Images * Images that convey information must have descriptive alt text that explains * the content and purpose of the image. * * @example * ```tsx * // ✅ GOOD: Informative image with descriptive alt * Sales chart showing 30% revenue growth in Q4 2024 * * // ✅ GOOD: Product photo with context * Silver MacBook Pro 14-inch on wooden desk * ``` * * ## Performance Optimization * * ### Lazy Loading * By default, images use lazy loading to improve page load performance. * Only use `loading="eager"` for above-the-fold images. * * @example * ```tsx * // ✅ GOOD: Lazy load below-the-fold image * Photo * * // ✅ GOOD: Eager load hero image * Hero banner * ``` * * ### Responsive Images * Use srcset and sizes for responsive images to serve appropriate image sizes * based on viewport width, improving performance and bandwidth usage. * * @example * ```tsx * // ✅ GOOD: Responsive image with multiple sizes * Responsive image adapts to viewport * ``` * * ## Error Handling * * @example * ```tsx * // ✅ GOOD: Custom placeholder on error * User profile photo * * // ✅ GOOD: Custom error handler * { * console.error('Image failed to load') * logToAnalytics('image_error', { src: e.currentTarget.src }) * }} * alt="Photo" * /> * ``` * * @param {ImgProps} props - Component props extending native img attributes * @returns {React.ReactElement} Image element with enhanced functionality * * @see {@link ImgProps} for complete prop documentation * @see https://www.w3.org/WAI/WCAG21/Understanding/non-text-content.html */ export const Img = ({ src = "//", alt, width = 480, height, styles, loading = "lazy", placeholder, fetchpriority = "low", decoding = "auto", srcSet, sizes, onError, onLoad, ...props }: ImgProps) => { /** * Generates a performant, responsive SVG gradient placeholder. * Uses data URI to avoid network requests and memoizes based on dimensions. * The SVG uses viewBox for perfect scaling at any size. * * Features: * - Zero network requests (works offline) * - ~900 bytes vs. 5-10KB external image * - Responsive with viewBox * - Attractive gradient (indigo → purple → pink) * - Dimension text for debugging */ const defaultPlaceholder = useMemo(() => { const w = typeof width === "number" ? width : 480; const h = typeof height === "number" ? height : Math.round(w * 0.75); // Responsive SVG with attractive gradient and dimension text const svg = ` ${w}×${h} `; return `data:image/svg+xml,${encodeURIComponent(svg)}`; }, [width, height]); const fallbackPlaceholder = placeholder ?? defaultPlaceholder; /** * Handles image load errors. * Calls custom error handler if provided, then applies fallback placeholder. * The custom handler can prevent the default fallback by calling e.preventDefault(). */ const handleImgError = ( e: React.SyntheticEvent ): void => { // Call custom error handler first (for logging, analytics, etc.) if (onError) { onError(e); } // Apply fallback unless preventDefault() was called if (!e.defaultPrevented) { // Avoid infinite error loop by checking if already showing placeholder if (e.currentTarget.src !== fallbackPlaceholder) { e.currentTarget.src = fallbackPlaceholder; } } }; /** * Handles successful image load. * Calls custom load handler if provided. */ const handleImgLoad = ( e: React.SyntheticEvent ): void => { onLoad?.(e); }; return ( ); }; export default Img; Img.displayName = "Img";