- Actualizar el disparador de acciones para usar solo un icono con etiqueta accesible. - Alinear y anclar la columna de acciones a la derecha, asegurando que no se oculte ni se ordene. - Ajustar las pruebas para reflejar estos cambios en la interfaz.
146 lines
3.7 KiB
TypeScript
146 lines
3.7 KiB
TypeScript
import { Menu } from "@base-ui/react/menu";
|
||
import { EllipsisIcon } from "lucide-react";
|
||
import type { ReactNode } from "react";
|
||
|
||
import { entityListLabels, entityListPages } from "../lib/entity-list";
|
||
|
||
import { Button } from "./button";
|
||
import { SelectWithAction } from "./select-with-action";
|
||
|
||
const ENTITY_PAGE_SIZES = [10, 25, 50, 100];
|
||
|
||
export function EntityRowActions({ children, label }: { children: ReactNode; label: string }) {
|
||
return (
|
||
<Menu.Root>
|
||
<Menu.Trigger
|
||
aria-label={label}
|
||
render={<Button size="icon-sm" variant="ghost" />}
|
||
title={label}
|
||
>
|
||
<EllipsisIcon aria-hidden="true" />
|
||
</Menu.Trigger>
|
||
<Menu.Portal>
|
||
<Menu.Positioner align="end" className="z-50" sideOffset={4}>
|
||
<Menu.Popup className="min-w-44 rounded-md border bg-popover p-1 text-popover-foreground shadow-md outline-none">
|
||
{children}
|
||
</Menu.Popup>
|
||
</Menu.Positioner>
|
||
</Menu.Portal>
|
||
</Menu.Root>
|
||
);
|
||
}
|
||
export function EntityAction({
|
||
children,
|
||
onClick,
|
||
disabled = false,
|
||
className,
|
||
}: {
|
||
children: ReactNode;
|
||
onClick: () => void;
|
||
disabled?: boolean;
|
||
className?: string;
|
||
}) {
|
||
return (
|
||
<Menu.Item
|
||
className={`cursor-pointer rounded-sm px-3 py-2 text-sm outline-none data-highlighted:bg-accent data-disabled:pointer-events-none data-disabled:opacity-50 ${className ?? ""}`}
|
||
disabled={disabled}
|
||
onClick={onClick}
|
||
>
|
||
{children}
|
||
</Menu.Item>
|
||
);
|
||
}
|
||
export function EntityStatus({
|
||
children,
|
||
active = true,
|
||
}: {
|
||
children: ReactNode;
|
||
active?: boolean;
|
||
}) {
|
||
return (
|
||
<span className="inline-flex items-center gap-2 text-sm">
|
||
<span
|
||
aria-hidden="true"
|
||
className={
|
||
active
|
||
? "size-2 shrink-0 rounded-full bg-primary"
|
||
: "size-2 shrink-0 rounded-full bg-muted-foreground"
|
||
}
|
||
/>
|
||
{children}
|
||
</span>
|
||
);
|
||
}
|
||
export function EntityListPagination({
|
||
locale,
|
||
page,
|
||
pages,
|
||
pageSize,
|
||
onPage,
|
||
onPageSize,
|
||
id,
|
||
}: {
|
||
locale: string;
|
||
page: number;
|
||
pages: number;
|
||
pageSize: number;
|
||
onPage: (page: number) => void;
|
||
onPageSize: (size: number) => void;
|
||
id: string;
|
||
}) {
|
||
const labels = entityListLabels(locale);
|
||
return (
|
||
<nav aria-label={labels.page} className="flex flex-wrap items-center gap-2">
|
||
<div className="flex items-center gap-2">
|
||
<label className="text-xs text-muted-foreground" htmlFor={`${id}-size`}>
|
||
{labels.pageSize}
|
||
</label>
|
||
<SelectWithAction
|
||
id={`${id}-size`}
|
||
label={labels.pageSize}
|
||
onValueChange={(value) => {
|
||
const size = Number(value);
|
||
if (ENTITY_PAGE_SIZES.includes(size)) {
|
||
onPageSize(size);
|
||
}
|
||
}}
|
||
options={ENTITY_PAGE_SIZES.map((size) => ({ value: String(size), label: String(size) }))}
|
||
placeholder={labels.pageSize}
|
||
value={String(pageSize)}
|
||
/>
|
||
</div>
|
||
|
||
<Button
|
||
aria-label={labels.previous}
|
||
disabled={page === 0}
|
||
onClick={() => onPage(page - 1)}
|
||
size="sm"
|
||
variant="outline"
|
||
>
|
||
‹
|
||
</Button>
|
||
{entityListPages(page, pages).map((index) => (
|
||
<Button
|
||
aria-current={index === page ? "page" : undefined}
|
||
aria-label={`${labels.page} ${index + 1}`}
|
||
key={index}
|
||
onClick={() => onPage(index)}
|
||
size="sm"
|
||
variant={index === page ? "secondary" : "ghost"}
|
||
>
|
||
{index + 1}
|
||
</Button>
|
||
))}
|
||
<Button
|
||
aria-label={labels.next}
|
||
disabled={page + 1 >= pages}
|
||
onClick={() => onPage(page + 1)}
|
||
size="sm"
|
||
variant="outline"
|
||
>
|
||
›
|
||
</Button>
|
||
</nav>
|
||
);
|
||
}
|