diff --git a/src/web/src/components/common/Table.tsx b/src/web/src/components/common/Table.tsx new file mode 100644 index 0000000..64bf6e9 --- /dev/null +++ b/src/web/src/components/common/Table.tsx @@ -0,0 +1,389 @@ +/** + * A reusable table component for displaying structured data with support for sorting, + * pagination, and custom cell rendering. Implements WCAG 2.1 accessibility standards. + * + * Requirements addressed: + * - User Interface Design (Technical Specification/8.1 User Interface Design) + * Implements responsive table component with sorting and pagination + * - Accessibility Features (Technical Specification/8.1.8 Accessibility Features) + * Ensures WCAG 2.1 compliance with proper ARIA attributes + * - Mobile Responsive Considerations (Technical Specification/8.1.7 Mobile Responsive Considerations) + * Adapts layout for different screen sizes with proper touch targets + */ + +// @version: react ^18.0.0 +import React, { useMemo } from 'react'; +// @version: classnames ^2.3.2 +import classNames from 'classnames'; +import { formatCurrency } from '../../utils/currency.utils'; +import Spinner from './Spinner'; + +// Human tasks: +// 1. Verify color contrast ratios meet WCAG 2.1 AA standards (4.5:1 for normal text) +// 2. Test touch target sizes on mobile devices (minimum 44x44 points) +// 3. Validate table markup with screen readers for proper navigation +// 4. Ensure sort indicators are visible in high contrast mode + +export interface TableColumn { + key: string; + header: string; + sortable?: boolean; + render?: (item: any) => React.ReactNode; + width?: string; + align?: 'left' | 'center' | 'right'; +} + +export interface TableProps { + data: any[]; + columns: TableColumn[]; + loading?: boolean; + hoverable?: boolean; + striped?: boolean; + className?: string; + onRowClick?: (item: any) => void; + pageSize?: number; + currentPage?: number; + onPageChange?: (page: number) => void; + sortKey?: string; + sortDirection?: 'asc' | 'desc'; + onSort?: (key: string) => void; + ariaLabel?: string; + summary?: string; +} + +const renderTableHeader = ( + columns: TableColumn[], + sortKey?: string, + sortDirection?: 'asc' | 'desc', + onSort?: (key: string) => void +): React.ReactNode => { + return ( + + + {columns.map((column) => { + const isSorted = sortKey === column.key; + const headerClasses = classNames('table-header', { + sortable: column.sortable, + 'sorted-asc': isSorted && sortDirection === 'asc', + 'sorted-desc': isSorted && sortDirection === 'desc', + }); + + return ( + + {column.sortable ? ( + + ) : ( + {column.header} + )} + + ); + })} + + + ); +}; + +const renderTableBody = ( + data: any[], + columns: TableColumn[], + onRowClick?: (item: any) => void +): React.ReactNode => { + return ( + + {data.map((item, rowIndex) => ( + onRowClick?.(item)} + className={classNames('table-row', { + clickable: !!onRowClick, + })} + tabIndex={onRowClick ? 0 : undefined} + role={onRowClick ? 'button' : undefined} + onKeyPress={(e) => { + if (onRowClick && (e.key === 'Enter' || e.key === ' ')) { + e.preventDefault(); + onRowClick(item); + } + }} + > + {columns.map((column) => { + const cellValue = item[column.key]; + const cellContent = column.render + ? column.render(item) + : typeof cellValue === 'number' + ? formatCurrency(cellValue) + : cellValue; + + return ( + + {cellContent} + + ); + })} + + ))} + + ); +}; + +const Table: React.FC = ({ + data, + columns, + loading = false, + hoverable = true, + striped = true, + className, + onRowClick, + pageSize, + currentPage = 1, + onPageChange, + sortKey, + sortDirection, + onSort, + ariaLabel, + summary, +}) => { + // Calculate paginated data + const paginatedData = useMemo(() => { + if (!pageSize) return data; + const startIndex = (currentPage - 1) * pageSize; + return data.slice(startIndex, startIndex + pageSize); + }, [data, pageSize, currentPage]); + + // Calculate total pages + const totalPages = pageSize ? Math.ceil(data.length / pageSize) : 0; + + const tableClasses = classNames( + 'table', + { + 'table-hoverable': hoverable, + 'table-striped': striped, + 'table-loading': loading, + }, + className + ); + + return ( +
+ + {renderTableHeader(columns, sortKey, sortDirection, onSort)} + {renderTableBody(paginatedData, columns, onRowClick)} +
+ + {loading && ( + + )} + + {pageSize && totalPages > 1 && ( +
+ + + Page {currentPage} of {totalPages} + + +
+ )} + + +
+ ); +}; + +export default Table; \ No newline at end of file