react-dnd-accessible-backend
Version:
An add-on backend for react-dnd that provides support for keyboards and screenreaders by default.
162 lines • 6.12 kB
JavaScript
import DragAnnouncer from "./DragAnnouncer";
import DragPreviewer from "./DragPreviewer";
import { DropTargetNavigator } from "./DropTargetNavigator";
import getNodeClientOffset from "./util/getNodeClientOffset";
import isKeyboardDragTrigger from "./util/isKeyboardDragTrigger";
import stopEvent from "./util/stopEvent";
const Trigger = {
DROP: [" ", "Enter"],
CANCEL_DRAG: ["Escape"],
};
function isTrigger(event, trigger) {
return trigger.includes(event.key);
}
export class KeyboardBackend {
static isSetUp;
// React-DnD Dependencies
manager;
actions;
monitor;
context;
options;
// Internal State
sourceNodes;
sourcePreviewNodes;
sourcePreviewNodeOptions;
targetNodes;
_navigator;
_previewer;
_announcer;
_previewEnabled;
_isDragTrigger;
_handlingFirstEvent = false;
constructor(manager, context, options) {
this.manager = manager;
this.actions = manager.getActions();
this.monitor = manager.getMonitor();
this.context = context;
this.options = options;
this._isDragTrigger = options?.isDragTrigger ?? isKeyboardDragTrigger;
this._previewEnabled = options?.preview ?? true;
this.sourceNodes = new Map();
this.sourcePreviewNodes = new Map();
this.sourcePreviewNodeOptions = new Map();
this.targetNodes = new Map();
this._previewer = new DragPreviewer(context.document, options);
this._announcer = new DragAnnouncer(options);
}
setup() {
if (KeyboardBackend.isSetUp) {
throw new Error("Cannot have two Keyboard backends at the same time.");
}
KeyboardBackend.isSetUp = true;
this._handlingFirstEvent = true;
this.context.window?.addEventListener("keydown", this.handleGlobalKeyDown, { capture: true });
this._previewer.attach();
}
teardown() {
KeyboardBackend.isSetUp = false;
this.context.window?.removeEventListener("keydown", this.handleGlobalKeyDown, {
capture: true,
});
this.endDrag();
this._previewer.detach();
this._announcer.destroy();
}
handleGlobalKeyDown = (event) => {
if (this.monitor.isDragging() && isTrigger(event, Trigger.CANCEL_DRAG)) {
this.endDrag(event);
const sourceId = String(this.monitor.getSourceId());
const sourceNode = this.sourceNodes.get(sourceId);
this._announcer.announceCancel(sourceNode ?? null, sourceId);
}
};
setDndMode(enabled) {
this.options?.onDndModeChanged?.(enabled);
}
profile() {
return {
sourcePreviewNodes: this.sourcePreviewNodes.size,
sourcePreviewNodeOptions: this.sourcePreviewNodeOptions.size,
sourceNodes: this.sourceNodes.size,
};
}
connectDragSource(sourceId, node) {
const handleDragStart = this.handleDragStart.bind(this, sourceId);
this.sourceNodes.set(sourceId, node);
node.addEventListener("keydown", handleDragStart);
return () => {
this.sourceNodes.delete(sourceId);
node.removeEventListener("keydown", handleDragStart);
};
}
connectDragPreview(sourceId, node, options) {
this.sourcePreviewNodeOptions.set(sourceId, options);
this.sourcePreviewNodes.set(sourceId, node);
return () => {
this.sourcePreviewNodes.delete(sourceId);
this.sourcePreviewNodeOptions.delete(sourceId);
};
}
connectDropTarget(targetId, node) {
this.targetNodes.set(targetId, node);
node.addEventListener("keydown", this.handleDrop);
// Ensure that the target will be focusable by the navigator
node.tabIndex = Math.max(-1, node.tabIndex);
return () => {
this.targetNodes.delete(targetId);
node.removeEventListener("keydown", this.handleDrop);
};
}
getSourceClientOffset = (sourceId) => {
return getNodeClientOffset(this.sourceNodes.get(sourceId));
};
handleDragStart = (sourceId, event) => {
if (!this._isDragTrigger(event, this._handlingFirstEvent))
return;
this._handlingFirstEvent = false;
if (!this.monitor.canDragSource(sourceId))
return;
if (this.monitor.isDragging()) {
this.actions.publishDragSource();
return;
}
stopEvent(event);
const sourceNode = this.sourceNodes.get(sourceId);
if (sourceNode == null)
return;
this._navigator = new DropTargetNavigator(sourceNode, this.targetNodes, this.manager, this._previewer, this._announcer);
this._previewer.createDragPreview(this.sourcePreviewNodes.get(sourceId) ?? sourceNode);
this.actions.beginDrag([sourceId], {
clientOffset: this.getSourceClientOffset(sourceId),
getSourceClientOffset: this.getSourceClientOffset,
publishSource: false,
item: this.monitor.getItem(),
itemType: this.monitor.getItemType(),
});
this.actions.publishDragSource();
this._previewer.render(this.monitor);
this.setDndMode(true);
this._announcer.announceDrag(sourceNode, sourceId);
};
handleDrop = (event) => {
if (!isTrigger(event, Trigger.DROP))
return;
const sourceId = String(this.monitor.getSourceId());
const sourceNode = this.sourceNodes.get(sourceId);
this._announcer.announceDrop(sourceNode ?? null, sourceId);
this.actions.drop();
this.endDrag(event);
};
endDrag(event) {
event != null && stopEvent(event);
this._navigator?.disconnect();
this._previewer.clear();
if (this.monitor.isDragging())
this.actions.endDrag();
this.setDndMode(false);
}
}
const createKeyboardBackendFactory = (manager, context, options) => new KeyboardBackend(manager, context, options);
export default createKeyboardBackendFactory;
//# sourceMappingURL=KeyboardBackend.js.map