UNPKG

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
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