All files / src/components/ui segmented-control.tsx

100% Statements 5/5
100% Branches 7/7
100% Functions 3/3
100% Lines 5/5

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66                                              3x               76x                   152x 152x         7x                                    
// SPDX-License-Identifier: GPL-3.0-only
// Copyright (C) 2026 Max Boksem. See NOTICE for additional terms under GPLv3 section 7.
 
import type { ReactNode } from "react";
import { cn } from "@/lib/utils";
 
export interface SegmentedOption<T> {
	value: T;
	label: string;
	icon?: ReactNode;
}
 
interface SegmentedControlProps<T> {
	value: T;
	onChange: (value: T) => void;
	options: SegmentedOption<T>[];
	/** Stretches the options to share the full width instead of sizing to their labels. */
	fill?: boolean;
	className?: string;
	"aria-label"?: string;
}
 
/** A row of buttons where exactly one is chosen, like Off and On. */
export const SegmentedControl = <T extends string | number | boolean>({
	value,
	onChange,
	options,
	fill = false,
	className,
	"aria-label": ariaLabel,
}: SegmentedControlProps<T>) => (
	<div
		role="group"
		aria-label={ariaLabel}
		className={cn(
			"flex bg-secondary rounded-lg",
			fill ? "gap-1 p-1" : "gap-0.5 p-0.5",
			className
		)}
	>
		{options.map((option) => {
			const selected = option.value === value;
			return (
				<button
					key={String(option.value)}
					type="button"
					aria-pressed={selected}
					onClick={() => onChange(option.value)}
					className={cn(
						"rounded-md text-xs font-medium transition-colors",
						fill
							? "flex-1 flex items-center justify-center gap-1.5 py-1.5 px-3"
							: "px-3 py-1",
						selected
							? "bg-background text-foreground shadow-xs"
							: "text-muted-foreground hover:text-foreground"
					)}
				>
					{option.icon}
					{option.label}
				</button>
			);
		})}
	</div>
);