"use client"
import * as React from "react"
import { ArrowDownWideNarrowIcon, ArrowUpNarrowWideIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
export type SortDirection = "asc" | "desc"
export type SortValue = {
key: string
direction: SortDirection
}
// aria-label is the one div prop that does not reach the root: it names the key
// select inside, which is the control a reader actually operates.
export type SortSelectProps = Omit<React.ComponentProps<"div">, "aria-label"> & {
options: { value: string; label: string }[]
value: SortValue
onValueChange: (value: SortValue) => void
size?: "sm" | "default"
/** Names the key select, e.g. "Sort orders by". */
"aria-label"?: string
}
/** A sort key and its direction as one control: a select for the column, a button for the way. */
function SortSelect({
options,
value,
onValueChange,
size = "default",
className,
"aria-label": ariaLabel = "Sort by",
...props
}: SortSelectProps) {
const ascending = value.direction === "asc"
// A handful of entries rebuilt per render; memoising against a prop array that
// is itself rebuilt every render would buy nothing.
const labels = Object.fromEntries(options.map((option) => [option.value, option.label]))
return (
<div
data-slot="sort-select"
data-size={size}
data-direction={value.direction}
className={cn("flex w-fit items-center gap-1.5", className)}
{...props}
>
<Select
value={value.key}
onValueChange={(key) => {
if (typeof key === "string" && key !== value.key)
onValueChange({ key, direction: value.direction })
}}
>
<SelectTrigger size={size} aria-label={ariaLabel} className="min-w-32">
<SelectValue>{(key) => labels[key as string] ?? key}</SelectValue>
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Button
type="button"
data-slot="sort-select-direction"
variant="outline"
size={size === "sm" ? "icon-sm" : "icon"}
// The label states the direction in force, and pressing flips it. No
// aria-pressed: a toggle's pressed state would contradict the words —
// "Sort descending, not pressed" says the opposite of what is happening.
aria-label={ascending ? "Sort ascending" : "Sort descending"}
onClick={() => onValueChange({ key: value.key, direction: ascending ? "desc" : "asc" })}
>
{ascending ? <ArrowUpNarrowWideIcon /> : <ArrowDownWideNarrowIcon />}
</Button>
</div>
)
}
export { SortSelect }