react-beautiful-dnd
Version:
Beautiful, accessible drag and drop for lists with React.js
197 lines (165 loc) • 4.83 kB
Flow
// @flow
/* eslint-disable no-use-before-define */
import stopEvent from '../util/stop-event';
import createScheduler from '../util/create-scheduler';
import blockStandardKeyEvents from '../util/block-standard-key-events';
import * as keyCodes from '../../key-codes';
import getWindowFromRef from '../../get-window-from-ref';
import getCenterPosition from '../../get-center-position';
import type { Position } from '../../../types';
import type { KeyboardSensor, CreateSensorArgs } from './sensor-types';
import type {
Props,
} from '../drag-handle-types';
type State = {|
isDragging: boolean,
|}
type ExecuteBasedOnDirection = {|
vertical: () => void,
horizontal: () => void,
|}
const noop = () => { };
export default ({
callbacks,
getDraggableRef,
canStartCapturing,
}: CreateSensorArgs): KeyboardSensor => {
let state: State = {
isDragging: false,
};
const setState = (newState: State): void => {
state = newState;
};
const startDragging = (fn?: Function = noop) => {
setState({
isDragging: true,
});
bindWindowEvents();
fn();
};
const stopDragging = (fn?: Function = noop) => {
unbindWindowEvents();
setState({
isDragging: false,
});
fn();
};
const kill = () => stopDragging();
const cancel = () => {
stopDragging(callbacks.onCancel);
};
const isDragging = (): boolean => state.isDragging;
const schedule = createScheduler(callbacks, isDragging);
const onKeyDown = (event: KeyboardEvent, props: Props) => {
const { direction } = props;
// not yet dragging
if (!isDragging()) {
// cannot lift at this time
if (!canStartCapturing(event)) {
return;
}
if (event.keyCode !== keyCodes.space) {
return;
}
stopEvent(event);
const ref: ?HTMLElement = getDraggableRef();
if (!ref) {
console.error('cannot start a keyboard drag without a draggable ref');
return;
}
// using center position as selection
const center: Position = getCenterPosition(ref);
// not allowing scrolling with a mouse when lifting with a keyboard
startDragging(() => callbacks.onLift({ client: center, isScrollAllowed: false }));
return;
}
// Cancelling
if (event.keyCode === keyCodes.escape) {
stopEvent(event);
cancel();
return;
}
// Dropping
if (event.keyCode === keyCodes.space) {
// need to stop parent Draggable's thinking this is a lift
stopEvent(event);
stopDragging(callbacks.onDrop);
return;
}
// Movement
// already dragging
if (!direction) {
console.error('Cannot handle keyboard movement event if direction is not provided');
stopEvent(event);
cancel();
return;
}
const executeBasedOnDirection = (fns: ExecuteBasedOnDirection) => {
if (direction === 'vertical') {
fns.vertical();
return;
}
fns.horizontal();
};
if (event.keyCode === keyCodes.arrowDown) {
stopEvent(event);
executeBasedOnDirection({
vertical: schedule.moveForward,
horizontal: schedule.crossAxisMoveForward,
});
return;
}
if (event.keyCode === keyCodes.arrowUp) {
stopEvent(event);
executeBasedOnDirection({
vertical: schedule.moveBackward,
horizontal: schedule.crossAxisMoveBackward,
});
return;
}
if (event.keyCode === keyCodes.arrowRight) {
stopEvent(event);
executeBasedOnDirection({
vertical: schedule.crossAxisMoveForward,
horizontal: schedule.moveForward,
});
return;
}
if (event.keyCode === keyCodes.arrowLeft) {
stopEvent(event);
executeBasedOnDirection({
vertical: schedule.crossAxisMoveBackward,
horizontal: schedule.moveBackward,
});
}
blockStandardKeyEvents(event);
};
const windowBindings = {
// any mouse down kills a drag
mousedown: cancel,
resize: cancel,
// currently not supporting window scrolling with a keyboard
scroll: cancel,
};
const eventKeys: string[] = Object.keys(windowBindings);
const bindWindowEvents = () => {
const win: HTMLElement = getWindowFromRef(getDraggableRef());
eventKeys.forEach((eventKey: string) => {
win.addEventListener(eventKey, windowBindings[eventKey]);
});
};
const unbindWindowEvents = () => {
const win: HTMLElement = getWindowFromRef(getDraggableRef());
eventKeys.forEach((eventKey: string) => {
win.removeEventListener(eventKey, windowBindings[eventKey]);
});
};
const sensor: KeyboardSensor = {
onKeyDown,
kill,
isDragging,
// a drag starts instantly so capturing is the same as dragging
isCapturing: isDragging,
};
return sensor;
};