metis-desk/packages/shadcn-ui/src/components/entity-list-controls.tsx
David da21f90615 Mejorar la accesibilidad y el comportamiento de las acciones en las tablas:
- 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.
2026-10-07 18:29:44 +02:00

146 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}