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
Draggablepasa encima (isOverno esnull), pinta el fondo en gris (#c4c3c3) como hover.
- Muestra el texto
-
Cuando tiene dato (
data):- Muestra
data.textcon estilo de texto de respuesta. - Colorea el texto en blanco; el contenedor usa estilos de respuesta (
answerblue, etc., segúnFactoryLiteThemedContainer).
- Muestra
-
Corrección:
-
isCorrectAnswer === null:- Slot editable: puedes seguir soltando cosas o limpiar con
onPress.
- Slot editable: puedes seguir soltando cosas o limpiar con
-
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”.
- Se activa
-
isCorrectAnswer === false:disable={true}→ también se bloquea, peroisCorrect={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 elDroppablebase / slice de DnD. - Efecto práctico: “vacía” el slot (tanto visualmente como en el estado global).
- Si hay un elemento registrado en
Props en detalle
| Prop | Tipo | Descripción |
|---|---|---|
isOver | PadObject<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). |
data | PadObject<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. |
isCorrectAnswer | boolean | null | Controla 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 | () => void | Función que se debe llamar cuando quieres “deshacer” el drop en este droppable; normalmente viene directo del Droppable base (antiDrop en el render prop). |
droppedItems | React.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). |
index | number | Í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. placeholderTextydroppableTextusan tipografíaslato-*y tamaños relativos al alto.starStyledefine 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:
-
PadDNDgenera los draggables (números, letras u opciones). -
Cada
Droppablebase detecta drops y exponeisOver,data,antiDrop. -
PadDroppablese 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()).