797aa5e350
AnimatedModal: ariaLabelledBy prop, EntityCombobox: combobox/listbox pattern, FilterBar: role="search", SortableColumnHeader: aria-sort, global-error: html lang attr, eslint label rule depth config. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
85 lines
2.2 KiB
TypeScript
85 lines
2.2 KiB
TypeScript
import { InfoTooltip } from "./InfoTooltip.js";
|
|
|
|
interface SortIconProps {
|
|
dir: "asc" | "desc" | null;
|
|
}
|
|
|
|
function SortIcon({ dir }: SortIconProps) {
|
|
return (
|
|
<span className="inline-flex flex-col leading-none ml-0.5">
|
|
<svg width="8" height="12" viewBox="0 0 8 12" fill="none" aria-hidden>
|
|
{/* Up chevron */}
|
|
<path
|
|
d="M1 5L4 2L7 5"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className={dir === "asc" ? "stroke-brand-600" : "stroke-gray-300"}
|
|
/>
|
|
{/* Down chevron */}
|
|
<path
|
|
d="M1 7L4 10L7 7"
|
|
strokeWidth="1.5"
|
|
strokeLinecap="round"
|
|
strokeLinejoin="round"
|
|
className={dir === "desc" ? "stroke-brand-600" : "stroke-gray-300"}
|
|
/>
|
|
</svg>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
interface SortableColumnHeaderProps {
|
|
label: string;
|
|
field: string;
|
|
sortField: string | null;
|
|
sortDir: "asc" | "desc" | null;
|
|
onSort: (field: string) => void;
|
|
className?: string;
|
|
align?: "left" | "right" | "center";
|
|
tooltip?: string;
|
|
tooltipWidth?: string;
|
|
}
|
|
|
|
export function SortableColumnHeader({
|
|
label,
|
|
field,
|
|
sortField,
|
|
sortDir,
|
|
onSort,
|
|
className = "",
|
|
align = "left",
|
|
tooltip,
|
|
tooltipWidth,
|
|
}: SortableColumnHeaderProps) {
|
|
const activeDir = sortField === field ? sortDir : null;
|
|
const alignClass =
|
|
align === "right" ? "justify-end" : align === "center" ? "justify-center" : "justify-start";
|
|
const ariaSortValue =
|
|
activeDir === "asc" ? "ascending" : activeDir === "desc" ? "descending" : undefined;
|
|
|
|
return (
|
|
<th
|
|
aria-sort={ariaSortValue}
|
|
className={`px-3 py-3 text-xs font-medium text-gray-500 uppercase tracking-wider ${className}`}
|
|
>
|
|
<div className={`flex items-center gap-0.5 ${alignClass}`}>
|
|
<button
|
|
type="button"
|
|
onClick={() => onSort(field)}
|
|
className="flex items-center gap-0.5 hover:text-gray-700 transition-colors group"
|
|
>
|
|
<span>{label}</span>
|
|
<SortIcon dir={activeDir} />
|
|
</button>
|
|
{tooltip && (
|
|
<InfoTooltip
|
|
content={tooltip}
|
|
{...(tooltipWidth !== undefined ? { width: tooltipWidth } : {})}
|
|
/>
|
|
)}
|
|
</div>
|
|
</th>
|
|
);
|
|
}
|