From 1182022454b79c440507a7aef16a537a1208f14e Mon Sep 17 00:00:00 2001 From: RoyEden Date: Mon, 16 Jan 2023 23:49:55 -0300 Subject: [PATCH 1/2] * Add dev command (runs rollup watch). * Reuse already computed transforms. * Add support for keyboard sensors. * Add support for transitions when using keyboard sensors. --- package.json | 1 + src/create-draggable.ts | 12 ++- src/create-droppable.ts | 4 +- src/create-keyboard-sensor.ts | 164 ++++++++++++++++++++++++++++++++++ src/create-pointer-sensor.ts | 8 +- src/create-sortable.ts | 4 +- src/drag-drop-context.tsx | 31 +++++-- src/drag-drop-debugger.tsx | 2 +- src/drag-drop-sensors.tsx | 2 + src/drag-overlay.tsx | 2 +- src/index.tsx | 1 + 11 files changed, 214 insertions(+), 17 deletions(-) create mode 100644 src/create-keyboard-sensor.ts diff --git a/package.json b/package.json index a88e154..d8e89af 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "sideEffects": false, "scripts": { + "dev": "rollup -cw", "build": "rollup -c", "prepublishOnly": "npm run build", "release": "release-it" diff --git a/src/create-draggable.ts b/src/create-draggable.ts index 8e134ef..6fd4212 100644 --- a/src/create-draggable.ts +++ b/src/create-draggable.ts @@ -76,9 +76,17 @@ const createDraggable = (id: Id, data: Record = {}): Draggable => { createEffect(() => { const resolvedTransform = transform(); + if (state.active.forceImmediateTransition) { + element.style.setProperty("transition-property", "transform"); + element.style.setProperty("transition-duration", "0ms"); + } else { + element.style.removeProperty("transition-property"); + element.style.removeProperty("transition-duration"); + } + if (!transformsAreEqual(resolvedTransform, noopTransform())) { - const style = transformStyle(transform()); - element.style.setProperty("transform", style.transform); + const style = transformStyle(resolvedTransform); + element.style.setProperty("transform", style.transform as string); } else { element.style.removeProperty("transform"); } diff --git a/src/create-droppable.ts b/src/create-droppable.ts index 523de3f..ba1d73b 100644 --- a/src/create-droppable.ts +++ b/src/create-droppable.ts @@ -54,8 +54,8 @@ const createDroppable = (id: Id, data: Record = {}): Droppable => { createEffect(() => { const resolvedTransform = transform(); if (!transformsAreEqual(resolvedTransform, noopTransform())) { - const style = transformStyle(transform()); - element.style.setProperty("transform", style.transform); + const style = transformStyle(resolvedTransform); + element.style.setProperty("transform", style.transform as string); } else { element.style.removeProperty("transform"); } diff --git a/src/create-keyboard-sensor.ts b/src/create-keyboard-sensor.ts new file mode 100644 index 0000000..6f59b7d --- /dev/null +++ b/src/create-keyboard-sensor.ts @@ -0,0 +1,164 @@ +import { onCleanup, onMount, untrack } from "solid-js"; + +import { + Coordinates, + Id, + SensorActivator, + useDragDropContext, +} from "./drag-drop-context"; +import { Transform } from "./layout"; + +const activateKeys = [ + " ", + "Enter", +] as const + +type ActivateKeys = typeof activateKeys[number] + +const sensorKeys = [ + ...activateKeys, + "Escape", + "ArrowLeft", + "ArrowRight", + "ArrowUp", + "ArrowDown", +] as const; + +type SensorKey = typeof sensorKeys[number]; + +const createKeyboardSensor = (id: Id = "keyboard-sensor"): void => { + const [ + state, + { + addSensor, + removeSensor, + sensorStart, + sensorMove, + sensorEnd, + dragStart, + dragEnd, + }, + ] = useDragDropContext()!; + const activationDelay = 250; // milliseconds + const activationDistance = 10; // pixels + const speed = 5 // pixels per keypress + + onMount(() => { + addSensor({ + id, + activators: { keydown: attach }, + }); + }); + + onCleanup(() => { + removeSensor(id); + }); + + const isActiveSensor = () => state.active.sensorId === id; + + const initialCoordinates: Coordinates = { x: 0, y: 0 }; + + let activationDelayTimeoutId: number | null = null; + let activationDraggableId: Id | null = null; + + const attach: SensorActivator<"keydown"> = (event, draggableId) => { + if (activateKeys.includes(event.key as ActivateKeys)) { + event.preventDefault(); + event.stopPropagation(); + const rect = (event.currentTarget as HTMLElement).getBoundingClientRect(); + document.addEventListener("keydown", onKeyDown); + activationDraggableId = draggableId; + initialCoordinates.x = rect.x + rect.width / 2; + initialCoordinates.y = rect.y + rect.height / 2; + + activationDelayTimeoutId = window.setTimeout(onActivate, activationDelay); + } + }; + + const detach = (): void => { + if (activationDelayTimeoutId) { + clearTimeout(activationDelayTimeoutId); + activationDelayTimeoutId = null; + } + + document.removeEventListener("keydown", onKeyDown); + }; + + const onActivate = (): void => { + if (!state.active.sensor) { + sensorStart(id, initialCoordinates); + dragStart(activationDraggableId!); + + clearSelection(); + document.addEventListener("selectionchange", clearSelection); + } else if (!isActiveSensor()) { + detach(); + } + }; + + const onKeyDown = (event: KeyboardEvent): void => { + if (sensorKeys.includes(event.key as SensorKey)) { + const sensor = untrack(() => state.active.sensor); + if (sensor) { + const coordinates: Coordinates = { ...sensor.coordinates.current }; + const prevCoordinates: Coordinates = { ...coordinates }; + switch (event.key as SensorKey) { + case "Escape": + sensorMove(initialCoordinates); + case " ": + case "Enter": + case "Escape": + detach(); + if (isActiveSensor()) { + event.preventDefault(); + dragEnd(); + sensorEnd(); + } + break; + case "ArrowLeft": + coordinates.x -= speed; + break; + case "ArrowRight": + coordinates.x += speed; + break; + case "ArrowUp": + coordinates.y -= speed; + break; + case "ArrowDown": + coordinates.y += speed; + break; + } + + if ( + prevCoordinates.x !== coordinates.x || + prevCoordinates.y !== coordinates.y + ) { + event.preventDefault(); + if (!state.active.sensor) { + const transform: Transform = { + x: coordinates.x - initialCoordinates.x, + y: coordinates.y - initialCoordinates.y, + }; + + if ( + Math.sqrt(transform.x ** 2 + transform.y ** 2) > + activationDistance + ) { + onActivate(); + } + } + + if (isActiveSensor()) { + sensorMove(coordinates); + } + } + } + } + }; + + const clearSelection = () => { + window.getSelection()?.removeAllRanges(); + }; +}; + +export { createKeyboardSensor }; diff --git a/src/create-pointer-sensor.ts b/src/create-pointer-sensor.ts index 3696983..5450006 100644 --- a/src/create-pointer-sensor.ts +++ b/src/create-pointer-sensor.ts @@ -1,4 +1,4 @@ -import { onCleanup, onMount } from "solid-js"; +import { onCleanup, onMount, untrack } from "solid-js"; import { Coordinates, @@ -91,11 +91,15 @@ const createPointerSensor = (id: Id = "pointer-sensor"): void => { if (isActiveSensor()) { event.preventDefault(); - sensorMove(coordinates); + sensorMove(coordinates, true); } }; const onPointerUp = (event: PointerEvent): void => { + const sensor = untrack(() => state.active.sensor); + if (sensor) { + sensorMove(sensor.coordinates.current); + } detach(); if (isActiveSensor()) { event.preventDefault(); diff --git a/src/create-sortable.ts b/src/create-sortable.ts index 08214e6..96c0740 100644 --- a/src/create-sortable.ts +++ b/src/create-sortable.ts @@ -83,8 +83,8 @@ const createSortable = (id: Id, data: Record = {}): Sortable => { createEffect(() => { const resolvedTransform = transform(); if (!transformsAreEqual(resolvedTransform, noopTransform())) { - const style = transformStyle(transform()); - element.style.setProperty("transform", style.transform); + const style = transformStyle(resolvedTransform); + element.style.setProperty("transform", style.transform as string); } else { element.style.removeProperty("transform"); } diff --git a/src/drag-drop-context.tsx b/src/drag-drop-context.tsx index a18c541..d06a7c5 100644 --- a/src/drag-drop-context.tsx +++ b/src/drag-drop-context.tsx @@ -82,6 +82,7 @@ interface DragDropState { draggable: Draggable | null; droppableId: Id | null; droppable: Droppable | null; + forceImmediateTransition: boolean; sensorId: Id | null; sensor: Sensor | null; overlay: Overlay | null; @@ -115,7 +116,10 @@ interface DragDropActions { detectCollisions(): void; draggableActivators(draggableId: Id, asHandlers?: boolean): Listeners; sensorStart(id: Id, coordinates: Coordinates): void; - sensorMove(coordinates: Coordinates): void; + sensorMove( + coordinates: Coordinates, + forceImmediateTransition?: boolean + ): void; sensorEnd(): void; dragStart(draggableId: Id): void; dragEnd(): void; @@ -170,6 +174,7 @@ const DragDropProvider: ParentComponent = ( ? state.droppables[state.active.droppableId] : null; }, + forceImmediateTransition: false, sensorId: null, get sensor(): Sensor | null { return state.active.sensorId !== null @@ -229,7 +234,10 @@ const DragDropProvider: ParentComponent = ( }) => { const existingDraggable = state.draggables[id]; - const draggable = { + const draggable: Omit< + Draggable, + "transform" | "transformed" | "transformers" + > = { id, node, layout, @@ -346,7 +354,10 @@ const DragDropProvider: ParentComponent = ( }) => { const existingDroppable = state.droppables[id]; - const droppable = { + const droppable: Omit< + Droppable, + "transform" | "transformed" | "transformers" + > = { id, node, layout, @@ -536,15 +547,21 @@ const DragDropProvider: ParentComponent = ( }); }; - const sensorMove: DragDropActions["sensorMove"] = (coordinates) => { + const sensorMove: DragDropActions["sensorMove"] = ( + coordinates, + forceImmediateTransition = false + ) => { const sensorId = state.active.sensorId; if (!sensorId) { console.warn("Cannot move sensor when no sensor active."); return; } - setState("sensors", sensorId, "coordinates", "current", { - ...coordinates, + batch(() => { + setState("sensors", sensorId, "coordinates", "current", { + ...coordinates, + }); + setState("active", "forceImmediateTransition", forceImmediateTransition); }); }; @@ -820,4 +837,4 @@ export type { Overlay, SensorActivator, Transformer, -}; \ No newline at end of file +}; diff --git a/src/drag-drop-debugger.tsx b/src/drag-drop-debugger.tsx index 54509cd..23a3181 100644 --- a/src/drag-drop-debugger.tsx +++ b/src/drag-drop-debugger.tsx @@ -10,7 +10,7 @@ import { import { Portal } from "solid-js/web"; import { Id, useDragDropContext } from "./drag-drop-context"; -import { Layout, Transform } from "./layout"; +import { Layout } from "./layout"; import { layoutStyle, transformStyle } from "./style"; interface HighlighterProps { diff --git a/src/drag-drop-sensors.tsx b/src/drag-drop-sensors.tsx index a6ba945..ff29494 100644 --- a/src/drag-drop-sensors.tsx +++ b/src/drag-drop-sensors.tsx @@ -1,9 +1,11 @@ import { ParentComponent } from "solid-js"; import { createPointerSensor } from "./create-pointer-sensor"; +import { createKeyboardSensor } from "./create-keyboard-sensor"; const DragDropSensors: ParentComponent = (props) => { createPointerSensor(); + createKeyboardSensor(); return <>{props.children}; }; diff --git a/src/drag-overlay.tsx b/src/drag-overlay.tsx index 039e205..31b74c8 100644 --- a/src/drag-overlay.tsx +++ b/src/drag-overlay.tsx @@ -46,7 +46,7 @@ const DragOverlay: ParentComponent = (props) => { return { position: "fixed", - transition: "transform 0s", + transition: state.active.forceImmediateTransition ? "transform 0s" : "", top: `${overlay.layout.top}px`, left: `${overlay.layout.left}px`, "min-width": `${draggable.layout.width}px`, diff --git a/src/index.tsx b/src/index.tsx index 4f05634..40d2c44 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,6 +1,7 @@ export { DragDropProvider, useDragDropContext } from "./drag-drop-context"; export { DragDropSensors } from "./drag-drop-sensors"; export { createPointerSensor } from "./create-pointer-sensor"; +export { createKeyboardSensor } from "./create-keyboard-sensor"; export { createDraggable } from "./create-draggable"; export { createDroppable } from "./create-droppable"; export { DragOverlay } from "./drag-overlay"; From 249c9317d4df345530881fc0484cf34588b2ff54 Mon Sep 17 00:00:00 2001 From: RoyEden Date: Sun, 5 Feb 2023 13:36:09 -0300 Subject: [PATCH 2/2] * Removed duplicate case in switch. * Added intersection type to avoid unnecessary casts/checks --- src/create-draggable.ts | 2 +- src/create-droppable.ts | 2 +- src/create-keyboard-sensor.ts | 1 - src/create-sortable.ts | 2 +- src/style.ts | 2 +- 5 files changed, 4 insertions(+), 5 deletions(-) diff --git a/src/create-draggable.ts b/src/create-draggable.ts index 6fd4212..9029235 100644 --- a/src/create-draggable.ts +++ b/src/create-draggable.ts @@ -86,7 +86,7 @@ const createDraggable = (id: Id, data: Record = {}): Draggable => { if (!transformsAreEqual(resolvedTransform, noopTransform())) { const style = transformStyle(resolvedTransform); - element.style.setProperty("transform", style.transform as string); + element.style.setProperty("transform", style.transform); } else { element.style.removeProperty("transform"); } diff --git a/src/create-droppable.ts b/src/create-droppable.ts index ba1d73b..1d4f8e0 100644 --- a/src/create-droppable.ts +++ b/src/create-droppable.ts @@ -55,7 +55,7 @@ const createDroppable = (id: Id, data: Record = {}): Droppable => { const resolvedTransform = transform(); if (!transformsAreEqual(resolvedTransform, noopTransform())) { const style = transformStyle(resolvedTransform); - element.style.setProperty("transform", style.transform as string); + element.style.setProperty("transform", style.transform); } else { element.style.removeProperty("transform"); } diff --git a/src/create-keyboard-sensor.ts b/src/create-keyboard-sensor.ts index 6f59b7d..9de95e5 100644 --- a/src/create-keyboard-sensor.ts +++ b/src/create-keyboard-sensor.ts @@ -107,7 +107,6 @@ const createKeyboardSensor = (id: Id = "keyboard-sensor"): void => { sensorMove(initialCoordinates); case " ": case "Enter": - case "Escape": detach(); if (isActiveSensor()) { event.preventDefault(); diff --git a/src/create-sortable.ts b/src/create-sortable.ts index 96c0740..9909d4f 100644 --- a/src/create-sortable.ts +++ b/src/create-sortable.ts @@ -84,7 +84,7 @@ const createSortable = (id: Id, data: Record = {}): Sortable => { const resolvedTransform = transform(); if (!transformsAreEqual(resolvedTransform, noopTransform())) { const style = transformStyle(resolvedTransform); - element.style.setProperty("transform", style.transform as string); + element.style.setProperty("transform", style.transform); } else { element.style.removeProperty("transform"); } diff --git a/src/style.ts b/src/style.ts index d6e93c0..df4d64f 100644 --- a/src/style.ts +++ b/src/style.ts @@ -11,7 +11,7 @@ const layoutStyle = (layout: Layout): JSX.CSSProperties => { }; }; -const transformStyle = (transform: Transform): JSX.CSSProperties => { +const transformStyle = (transform: Transform): JSX.CSSProperties & { transform: string } => { return { transform: `translate3d(${transform.x}px, ${transform.y}px, 0)` }; };