feat(ui): mejora en el selector de años en PublicationsTable y ajustes en SwordProfileSelect
Se reemplaza el elemento <select> por el componente CustomSelect en PublicationsTable para una mejor experiencia de usuario al seleccionar años. Además, se ajusta el estilo del componente SwordProfileSelect para mejorar la presentación de las opciones. Se asegura que la funcionalidad de selección se mantenga intacta.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
import { useEffect, useMemo, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import { AlertIcon, ChevronDownIcon, FilterIcon, SearchIcon, SparkleIcon } from "../ui/Icons";
|
import { AlertIcon, ChevronDownIcon, FilterIcon, SearchIcon, SparkleIcon } from "../ui/Icons";
|
||||||
|
import { CustomSelect } from "../ui/CustomSelect";
|
||||||
import { Spinner } from "../ui/Spinner";
|
import { Spinner } from "../ui/Spinner";
|
||||||
import { Badge } from "../ui/Badge";
|
import { Badge } from "../ui/Badge";
|
||||||
|
|
||||||
@@ -314,20 +315,16 @@ export function PublicationsTable({
|
|||||||
>
|
>
|
||||||
Desde año
|
Desde año
|
||||||
</label>
|
</label>
|
||||||
<select
|
<CustomSelect
|
||||||
id="year-from"
|
id="year-from"
|
||||||
value={yearFrom}
|
value={yearFrom}
|
||||||
onChange={(e) => handleYearFromChange(e.target.value)}
|
onChange={handleYearFromChange}
|
||||||
disabled={availableYears.length === 0}
|
disabled={availableYears.length === 0}
|
||||||
className="rounded-md border border-surface-border-strong bg-surface-primary px-2.5 py-1.5 text-[13px] text-ink-primary outline-none focus:border-brand-accent disabled:cursor-not-allowed disabled:opacity-50"
|
options={availableYears.map((y) => ({
|
||||||
>
|
value: String(y),
|
||||||
<option value="">Cualquiera</option>
|
label: String(y),
|
||||||
{availableYears.map((y) => (
|
}))}
|
||||||
<option key={y} value={y}>
|
/>
|
||||||
{y}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<label
|
<label
|
||||||
@@ -336,20 +333,16 @@ export function PublicationsTable({
|
|||||||
>
|
>
|
||||||
Hasta año
|
Hasta año
|
||||||
</label>
|
</label>
|
||||||
<select
|
<CustomSelect
|
||||||
id="year-to"
|
id="year-to"
|
||||||
value={yearTo}
|
value={yearTo}
|
||||||
onChange={(e) => handleYearToChange(e.target.value)}
|
onChange={handleYearToChange}
|
||||||
disabled={availableYears.length === 0}
|
disabled={availableYears.length === 0}
|
||||||
className="rounded-md border border-surface-border-strong bg-surface-primary px-2.5 py-1.5 text-[13px] text-ink-primary outline-none focus:border-brand-accent disabled:cursor-not-allowed disabled:opacity-50"
|
options={availableYears.map((y) => ({
|
||||||
>
|
value: String(y),
|
||||||
<option value="">Cualquiera</option>
|
label: String(y),
|
||||||
{availableYears.map((y) => (
|
}))}
|
||||||
<option key={y} value={y}>
|
/>
|
||||||
{y}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
{hasYearFilter && (
|
{hasYearFilter && (
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ export function SwordProfileSelect({
|
|||||||
<div
|
<div
|
||||||
role="listbox"
|
role="listbox"
|
||||||
aria-labelledby={id}
|
aria-labelledby={id}
|
||||||
className="absolute left-0 top-[calc(100%+6px)] z-50 min-w-[260px] overflow-hidden rounded-xl border border-surface-border-strong bg-surface-primary shadow-lg"
|
className="absolute left-0 top-[calc(100%+6px)] z-50 min-w-80 overflow-hidden rounded-xl border border-surface-border-strong bg-surface-primary shadow-lg"
|
||||||
>
|
>
|
||||||
{options.map(({ value: optionValue, label, desc }, idx) => (
|
{options.map(({ value: optionValue, label, desc }, idx) => (
|
||||||
<button
|
<button
|
||||||
@@ -91,7 +91,9 @@ export function SwordProfileSelect({
|
|||||||
<div className="text-sm font-medium text-ink-primary">
|
<div className="text-sm font-medium text-ink-primary">
|
||||||
{label}
|
{label}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs text-ink-tertiary">{desc}</div>
|
<div className="whitespace-nowrap text-xs text-ink-tertiary">
|
||||||
|
{desc}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
|
import { ChevronDownIcon } from "./Icons";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Desplegable personalizado (sustituto del `<select>` nativo).
|
||||||
|
*/
|
||||||
|
export function CustomSelect({
|
||||||
|
id,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
options = [],
|
||||||
|
disabled = false,
|
||||||
|
emptyLabel = "Cualquiera",
|
||||||
|
className = "",
|
||||||
|
menuClassName = "",
|
||||||
|
}) {
|
||||||
|
const [open, setOpen] = useState(false);
|
||||||
|
const rootRef = useRef(null);
|
||||||
|
|
||||||
|
const allOptions = useMemo(
|
||||||
|
() => [{ value: "", label: emptyLabel }, ...options],
|
||||||
|
[options, emptyLabel],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selected =
|
||||||
|
allOptions.find((opt) => opt.value === value) ?? allOptions[0];
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
function handleClick(event) {
|
||||||
|
if (rootRef.current && !rootRef.current.contains(event.target)) {
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
document.addEventListener("mousedown", handleClick);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClick);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
function handlePick(optionValue) {
|
||||||
|
onChange(optionValue);
|
||||||
|
setOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={rootRef} className={`relative ${className}`.trim()}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
id={id}
|
||||||
|
disabled={disabled}
|
||||||
|
aria-haspopup="listbox"
|
||||||
|
aria-expanded={open}
|
||||||
|
onClick={() => !disabled && setOpen((o) => !o)}
|
||||||
|
className="inline-flex w-full min-w-[7.5rem] items-center gap-2 rounded-lg border border-surface-border-strong bg-surface-primary px-3 py-2 text-[13px] font-medium text-ink-primary transition-colors enabled:hover:bg-surface-secondary disabled:cursor-not-allowed disabled:opacity-50"
|
||||||
|
>
|
||||||
|
<span className="truncate">{selected.label}</span>
|
||||||
|
<ChevronDownIcon
|
||||||
|
className={`ml-auto shrink-0 transition-transform ${open ? "rotate-180" : ""}`}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
{open && !disabled && (
|
||||||
|
<div
|
||||||
|
role="listbox"
|
||||||
|
aria-labelledby={id}
|
||||||
|
className={`absolute left-0 top-[calc(100%+4px)] z-50 max-h-56 min-w-full overflow-auto rounded-xl border border-surface-border-strong bg-surface-primary py-1 shadow-lg ${menuClassName}`.trim()}
|
||||||
|
>
|
||||||
|
{allOptions.map((opt) => (
|
||||||
|
<button
|
||||||
|
key={opt.value || "__any__"}
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected={opt.value === value}
|
||||||
|
onClick={() => handlePick(opt.value)}
|
||||||
|
className={`flex w-full px-3 py-2 text-left text-[13px] transition-colors hover:bg-surface-secondary ${
|
||||||
|
opt.value === value
|
||||||
|
? "bg-surface-secondary/80 font-medium text-ink-primary"
|
||||||
|
: "text-ink-secondary"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{opt.label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user