Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Keyboard interactions #76

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"typesVersions": {},
"sideEffects": false,
"scripts": {
"dev": "tsup --watch",
"build": "tsup",
"prepublishOnly": "npm run build",
"release": "release-it"
Expand Down
12 changes: 10 additions & 2 deletions src/create-draggable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,9 +76,17 @@ const createDraggable = (id: Id, data: Record<string, any> = {}): 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 ?? null);
const style = transformStyle(resolvedTransform);
element.style.setProperty("transform", style.transform);
} else {
element.style.removeProperty("transform");
}
Expand Down
4 changes: 2 additions & 2 deletions src/create-droppable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ const createDroppable = (id: Id, data: Record<string, any> = {}): Droppable => {
createEffect(() => {
const resolvedTransform = transform();
if (!transformsAreEqual(resolvedTransform, noopTransform())) {
const style = transformStyle(transform());
element.style.setProperty("transform", style.transform ?? null);
const style = transformStyle(resolvedTransform);
element.style.setProperty("transform", style.transform);
} else {
element.style.removeProperty("transform");
}
Expand Down
163 changes: 163 additions & 0 deletions src/create-keyboard-sensor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,163 @@
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":
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 };
8 changes: 6 additions & 2 deletions src/create-pointer-sensor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { onCleanup, onMount } from "solid-js";
import { onCleanup, onMount, untrack } from "solid-js";

import {
Coordinates,
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 2 additions & 2 deletions src/create-sortable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,8 @@ const createSortable = (id: Id, data: Record<string, any> = {}): Sortable => {
createEffect(() => {
const resolvedTransform = transform();
if (!transformsAreEqual(resolvedTransform, noopTransform())) {
const style = transformStyle(transform());
element.style.setProperty("transform", style.transform ?? null);
const style = transformStyle(resolvedTransform);
element.style.setProperty("transform", style.transform);
} else {
element.style.removeProperty("transform");
}
Expand Down
31 changes: 24 additions & 7 deletions src/drag-drop-context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -170,6 +174,7 @@ const DragDropProvider: ParentComponent<DragDropContextProps> = (
? state.droppables[state.active.droppableId]
: null;
},
forceImmediateTransition: false,
sensorId: null,
get sensor(): Sensor | null {
return state.active.sensorId !== null
Expand Down Expand Up @@ -229,7 +234,10 @@ const DragDropProvider: ParentComponent<DragDropContextProps> = (
}) => {
const existingDraggable = state.draggables[id];

const draggable = {
const draggable: Omit<
Draggable,
"transform" | "transformed" | "transformers"
> = {
id,
node,
layout,
Expand Down Expand Up @@ -346,7 +354,10 @@ const DragDropProvider: ParentComponent<DragDropContextProps> = (
}) => {
const existingDroppable = state.droppables[id];

const droppable = {
const droppable: Omit<
Droppable,
"transform" | "transformed" | "transformers"
> = {
id,
node,
layout,
Expand Down Expand Up @@ -536,15 +547,21 @@ const DragDropProvider: ParentComponent<DragDropContextProps> = (
});
};

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);
});
};

Expand Down Expand Up @@ -820,4 +837,4 @@ export type {
Overlay,
SensorActivator,
Transformer,
};
};
2 changes: 1 addition & 1 deletion src/drag-drop-debugger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
2 changes: 2 additions & 0 deletions src/drag-drop-sensors.tsx
Original file line number Diff line number Diff line change
@@ -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}</>;
};

Expand Down
2 changes: 1 addition & 1 deletion src/drag-overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ const DragOverlay: ParentComponent<DragOverlayProps> = (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`,
Expand Down
1 change: 1 addition & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -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";
Expand Down
2 changes: 1 addition & 1 deletion src/style.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)` };
};

Expand Down