Ir al contenido
Formularios

DropTarget

Soltar archivos sobre cualquier contenido —una sección, una card— para abrir su formulario con el archivo, sin volverlo un recuadro de subida.

import { DropTarget } from "sebs7n-ui/drop-target"

Ejemplos

Soltar sobre una sección

Arrastrá un PDF sobre la card de facturas: se pinta el anillo, y al soltar se «abre el formulario» con el archivo. El botón «Agregar factura» sigue andando, y es el camino sin arrastrar.

import { Button } from "sebs7n-ui/button"
import { Card, CardAction, CardContent, CardHeader, CardTitle } from "sebs7n-ui/card"
import { DropTarget } from "sebs7n-ui/drop-target"
import { FileTextIcon, PlusIcon } from "lucide-react"
import { List, ListRow } from "sebs7n-ui/list-row"
import { useState } from "react"

function Section() {
  const [invoices, setInvoices] = useState(["factura-0012.pdf", "factura-0013.pdf"])
  const [draft, setDraft] = useState<string | null>(null)
  return (
    <DropTarget accept=".pdf" className="w-full max-w-md" maxSize={5 * 1024 * 1024} onDrop={([file]) => setDraft(file!.name)}>
      <Card>
        <CardHeader>
          <CardTitle>Facturas del cliente</CardTitle>
          <CardAction>
            <Button onClick={() => setDraft("")} size="sm" variant="secondary">
              <PlusIcon aria-hidden="true" />
              Agregar factura
            </Button>
          </CardAction>
        </CardHeader>
        <CardContent className="flex flex-col gap-3">
          <List>
            {invoices.map((name) => (
              <ListRow icon={<FileTextIcon />} key={name} title={name} />
            ))}
          </List>
          {draft !== null && (
            <div className="flex items-center justify-between gap-3 rounded-field bg-fill-1 px-3 py-2 text-callout text-label">
              <span className="truncate">{draft ? `Nueva factura con ${draft}` : "Nueva factura, sin archivo"}</span>
              <Button
                onClick={() => {
                  if (draft) setInvoices([...invoices, draft])
                  setDraft(null)
                }}
                size="sm"
              >
                Guardar
              </Button>
            </div>
          )}
        </CardContent>
      </Card>
    </DropTarget>
  )
}

Props

Generadas del TypeScript del paquete. Las propias del componente, más las heredadas del primitivo que tienen algo que explicar —marcadas «heredada de Base UI»—. El resto está en la línea «hereda de».

DropTarget

Hereda las props de <div>.

PropTipoPor defectoDescripción
onDrop*(files: File[]) => void—Los archivos que pasaron accept, maxSize y validate (uno solo sin multiple). Lo usual: abrir el formulario con el archivo ya puesto.
acceptstring—Los tipos que acepta, como el accept de un <input type="file">: ".pdf,image/*".
disabledbooleanfalseApaga la interacción y lo marca con data-disabled, que es el atributo del que cuelgan los estilos de apagado.
formatSize(bytes: number) => string—Cómo se escribe el tamaño en el error de maxSize. Por defecto, en base 1024 con Intl.
labelsPartial<DropTargetLabels>—Textos: drop, added, invalidType, tooLarge, tooMany y locale, los de labels.dropZone.
maxSizenumber—El tamaño máximo de cada archivo, en bytes.
multiplebooleanfalseMás de un archivo por vez. Sin multiple, entra el primero y los demás se avisan.
validate(file: File) => string | Promise<string>—Una validación propia por archivo, como la de DropZone: el texto que devuelve es su error. Puede ser asíncrona; mientras tanto la zona lleva aria-busy, y sin multiple gana el último soltado.
classNamestring—Clases del div que envuelve (es relative).

Teclado

—
No agrega paradas de Tab: el contenido de adentro sigue con las suyas. El camino con teclado es el botón de la sección («Agregar factura»).

Accesibilidad

  • No tiene rol ni foco: envuelve el contenido, que sigue siendo interactivo. Arrastrar es un atajo; el contenido tiene que traer su botón para el mismo resultado sin arrastrar (WCAG 2.5.7).
  • El anillo y el tinte mientras se arrastra son decorativos (aria-hidden) y no reciben el puntero.
  • Lo que entra se anuncia en una región viva («Archivos agregados: factura.pdf»), que se vacía antes de cada anuncio para que soltar el mismo archivo dos veces se lea dos veces; los rechazos aparecen abajo del contenido con role="alert". Mientras corre un validate asíncrono lleva aria-busy.

Reglas de uso

  • Para una sección que ya tiene su «Agregar»: soltar un archivo encima abre el mismo formulario con el archivo puesto (onDrop={([file]) => openForm(file)}), y ahí el DropZone del formulario lo muestra. Para elegir y listar archivos en un formulario, DropZone.
  • Valida como DropZone: accept, maxSize (base 1024, formatSize para escribirlo de otra forma) y validate (puede ser asíncrona). Sin multiple llega uno solo, el resto se avisa, y si se suelta otro mientras valida gana el último.
  • Lo que toma algo de adentro es de eso: una DropTarget anidada, un DropZone o un <input type="file"> nativo reciben su archivo y la de afuera solo se apaga. Con un DropZone scope="window" en la página, un soltar sobre la DropTarget es de ella. La marca es el preventDefault del soltar, no stopPropagation: un soltar propio que haga preventDefault también la deja afuera. No guarda archivos ni tiene name.
  • Los rechazos quedan abajo hasta el próximo arrastre o soltar: no hay botón para cerrarlos ni se van solos con el tiempo.
  • Los textos son los de DropZone (labels.dropZone: drop, added, invalidType, tooLarge, tooMany, locale). Solo por subpath (sebs7n-ui/drop-target).

Relacionados