Skip to main content

PadDroppable

PadDroppable es un droppable especializado para trabajar junto con PadDND y PadObject<T>.
Es el “slot de respuesta” donde se sueltan los números, letras u opciones del pad.

A diferencia de Droppable, este componente no conecta directamente con el slice de DnD:
se usa normalmente dentro del children de un Droppable, como capa de presentación.


API

import { PadObject } from "./PadDnD";

interface PadDroppableProps<T> {
/** Ítem que está siendo arrastrado encima de este slot (si hay) */
isOver: PadObject<T> | null;

/** Ítem actualmente almacenado en este slot (último drop) */
data?: PadObject<T> | null;

/**
* Estado de corrección de este slot:
* - null → aún no evaluado / editable
* - true → respuesta correcta (slot se bloquea + animación de estrella)
* - false → respuesta incorrecta (slot se bloquea)
*/
isCorrectAnswer: boolean | null;

/** Dimensiones del workspace (para tamaños responsivos) */
workSpaceDimensions: {
width: number;
height: number;
};

/** Función para “deshacer” el último drop en este slot (viene del Droppable base) */
antiDrop: () => void;

/**
* Ref compartido con todos los slots para registrar qué se dejó
* en cada índice: { [index]: PadObject<T> }
*/
droppedItems: React.MutableRefObject<Record<number, PadObject<T>>>;

/** Índice del slot (0, 1, 2, ...) para indexar en droppedItems.current */
index: number;
}

Uso típico (esquema):

const droppedItems = useRef<Record<number, PadObject<MyExtra>> >({});

{Array.from({ length: answers }, (_, index) => (
<Droppable<PadObject<MyExtra>> key={index}>
{({ isOver, data, antiDrop }) => {
// aquí podrías sincronizar droppedItems.current[index] = data;
return (
<PadDroppable<MyExtra>
isOver={isOver}
data={data}
workSpaceDimensions={workspaceDimensions}
isCorrectAnswer={/* null | true | false */}
antiDrop={antiDrop}
droppedItems={droppedItems}
index={index}
/>
);
}}
</Droppable>
))}

Comportamiento visual

Internamente PadDroppable renderiza un FactoryLiteThemedContainer con configuración fija orientada a “slot de respuesta”:

<FactoryLiteThemedContainer
disable={isCorrectAnswer !== null} // bloquea si ya fue evaluado
workspaceDimensions={workSpaceDimensions}
type="text"
containerStyle={[styles.droppableContainer]}
contentStyle={[
styles.droppableContent,
!data
? isOver
? { backgroundColor: "#c4c3c3" } // highlight gris cuando está vacío y el drag pasa encima
: {}
: {},
]}
textVariant="textBig"
/* ... colorCombination, etc. ... */
text={data?.text ?? "Arrastra aquí"} // placeholder por defecto
textStyle={[
data ? styles.droppableText : styles.placeholderText,
{ color: isOver || data ? "white" : "#757575" },
]}
interactive
isCorrect={isCorrectAnswer}
onPress={() => {
// Limpia este slot
if (droppedItems.current[index]) {
delete droppedItems.current[index];
}
antiDrop(); // sincroniza con el Droppable base / slice
}}
starsAnimation={isCorrectAnswer === true} // animación de estrellas si es correcto
startsStyle={styles.starStyle}
borderRadius={workSpaceDimensions.height * 0.02}
reverse={!data}
/>

Resumen del comportamiento:

  • Cuando está vacío (!data):

    • Muestra el texto "Arrastra aquí" en gris.
    • Si un Draggable pasa encima (isOver no es null), pinta el fondo en gris (#c4c3c3) como hover.
  • Cuando tiene dato (data):

    • Muestra data.text con estilo de texto de respuesta.
    • Colorea el texto en blanco; el contenedor usa estilos de respuesta (answerblue, etc., según FactoryLiteThemedContainer).
  • Corrección:

    • isCorrectAnswer === null:

      • Slot editable: puedes seguir soltando cosas o limpiar con onPress.
    • isCorrectAnswer === true:

      • Se activa starsAnimation (estrellas sobre el slot).
      • disable={true} → ya no acepta interacción.
      • isCorrect={true} → el tema del contenedor se muestra como “correcto”.
    • isCorrectAnswer === false:

      • disable={true} → también se bloquea, pero isCorrect={false} permite estilos de “incorrecto” (según el tema).
  • Click / tap (onPress):

    • Si hay un elemento registrado en droppedItems.current[index], se elimina.
    • Se llama antiDrop() para limpiar el estado en el Droppable base / slice de DnD.
    • Efecto práctico: “vacía” el slot (tanto visualmente como en el estado global).

Props en detalle

PropTipoDescripción
isOverPadObject<T> | nullÍtem que se está arrastrando sobre este slot, tomado directamente del Droppable base (isOver del render prop). Se usa para saber si hay hover y cambiar estilos (fondo gris).
dataPadObject<T> | null | undefinedÍtem actualmente almacenado en el slot (último drop exitoso). Viene del Droppable base (data del render prop). Si falta o es null, el slot se considera vacío.
isCorrectAnswerboolean | nullControla bloqueo y feedback de corrección. null significa “todavía sin corregir”; true/false bloquean el slot y disparan estilos correspondientes.
workSpaceDimensions{ width: number; height: number }Dimensiones del espacio de trabajo. PadDroppable usa especialmente height para calcular tamaños y radios (borderRadius, tamaños de estrella, etc.).
antiDrop() => voidFunción que se debe llamar cuando quieres “deshacer” el drop en este droppable; normalmente viene directo del Droppable base (antiDrop en el render prop).
droppedItemsReact.MutableRefObject<Record<number, PadObject<T>>>Ref compartido entre todos los slots para llevar un registro paralelo index → PadObject. PadDroppable lo usa solo para eliminar su propia entrada al limpiarse; el llenado se hace desde fuera (normalmente en el mismo children de Droppable).
indexnumberÍndice de este slot. Se usa como clave dentro de droppedItems.current[index]. Suele coincidir con la posición del slot en el arreglo de respuestas.

Estilos internos

PadDroppable define estilos responsivos en función de workSpaceDimensions:

const getStyles = ({ width, height }: { width: number; height: number }) =>
StyleSheet.create({
workspace: {
position: "absolute",
top: 0,
left: 0,
width,
height: height * 2,
alignItems: "center",
justifyContent: "flex-start",
},
topText: {
fontFamily: "lato-bold",
},
answerRow: {
flexDirection: "row",
gap: height * 0.02,
},
// (entre medio: droppableContainer, droppableContent, placeholderText, droppableText, etc.)
starStyle: {
width: height * 0.26,
height: height * 0.26,
position: "absolute",
left: -height * 0.26 * 0.25,
top: -height * 0.26 * 0.25,
alignSelf: "center",
justifyContent: "center",
alignItems: "center",
},
});

En la práctica:

  • El slot es un cuadrado/rectángulo cuyo tamaño depende de height.
  • placeholderText y droppableText usan tipografías lato-* y tamaños relativos al alto.
  • starStyle define una imagen o animación de estrella relativamente grande, centrada sobre el slot (desplazada un poco hacia arriba/izquierda).

Patrón típico de integración

Ejemplo simplificado de una fila de slots de respuesta junto con un PadDND numérico:

import { useRef, useState } from "react";
import { View } from "react-native";
import { DNDProvider, Droppable } from "@components/DnDRevolution";
import PadDND, { PadObject } from "./PadDnD";
import PadDroppable from "./PadDroppable";

type ExtraData = { correctValue: string };

const workspaceDimensions = { width: 360, height: 640 };

export const NumericPadWithSlots = () => {
const droppedItems = useRef<Record<number, PadObject<ExtraData>>>({});
const [answersState, setAnswersState] = useState<(boolean | null)[]>([
null,
null,
null,
null,
]);

return (
<DNDProvider<PadObject<ExtraData>>
workSpaceDimensions={workspaceDimensions}
mode="remplacement-multi"
onDrop={(dataByDroppable) => {
// Aquí podrías evaluar y actualizar answersState según el mapping droppableId -> PadObject
}}
>
{/* Slots de respuesta */}
<View style={{ flexDirection: "row", justifyContent: "center", gap: 8 }}>
{answersState.map((state, index) => (
<Droppable<PadObject<ExtraData>> key={index}>
{({ isOver, data, antiDrop }) => {
// sincronizar droppedItems con el valor actual del slot
if (data) droppedItems.current[index] = data;

return (
<PadDroppable<ExtraData>
isOver={isOver}
data={data}
isCorrectAnswer={state}
workSpaceDimensions={workspaceDimensions}
antiDrop={antiDrop}
droppedItems={droppedItems}
index={index}
/>
);
}}
</Droppable>
))}
</View>

{/* Pad de números */}
<PadDND<ExtraData>
workspaceDimensions={workspaceDimensions}
useNumbers
showOrderIndicator={false}
answers={answersState.length}
/>
</DNDProvider>
);
};

En este patrón:

  • PadDND genera los draggables (números, letras u opciones).

  • Cada Droppable base detecta drops y expone isOver, data, antiDrop.

  • PadDroppable se encarga de:

    • Visual de slot (“Arrastra aquí” / respuesta / hover).
    • Bloqueo por corrección (isCorrectAnswer).
    • Limpiar el slot tanto en UI como en el estado global (droppedItems + antiDrop()).