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

feat: added axis limitation option #593

Open
wants to merge 5 commits into
base: master
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ An options-object with the following attributes:
| `transformDraggedElement` | Function | No | `() => {}` | A function that is invoked when the draggable element enters the dnd-zone or hover overs a new index in the current dnd-zone. <br />Signature:<br />function(element, data, index) {}<br />**element**: The dragged element. <br />**data**: The data of the item from the items array.<br />**index**: The index the dragged element will become in the new dnd-zone.<br /><br />This allows you to override properties on the dragged element, such as innerHTML to change how it displays. If what you are after is altering styles, do it to the children, not to the dragged element itself |
| `autoAriaDisabled` | Boolean | No | `false` | Setting it to true will disable all the automatically added aria attributes and aria alerts (for example when the user starts/ stops dragging using the keyboard).<br /> **Use it only if you intend to implement your own custom instructions, roles and alerts.** In such a case, you might find the exported function `alertToScreenReader(string)` useful. |
| `centreDraggedOnCursor` | Boolean | No | `false` | Setting it to true will cause elements from this dnd-zone to position their center on the cursor on drag start, effectively turning the cursor to the focal point that triggers all the dnd events (ex: entering another zone). Useful for dnd-zones with large items that can be dragged over small items. |
| `axis` | String | No | `both` | Setting it to `"x"` restricts drag movement to the x-axis, while setting it to `"y"` restricts movement to the y-axis. |

##### Output:

Expand Down
1 change: 1 addition & 0 deletions src/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ function validateOptions(options) {
transformDraggedElement,
autoAriaDisabled,
centreDraggedOnCursor,
axis,
...rest
} = options;
/*eslint-enable*/
Expand Down
99 changes: 69 additions & 30 deletions src/keyboardAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,12 @@ const typeToDropZones = new Map();

let INSTRUCTION_IDs;

function alertCantMoveBecauseAxis(config, axis) {
if (!config.autoAriaDisabled) {
alertToScreenReader(`Item ${focusedItemLabel} can not be moved on the ${axis} axis`);
}
}

/* drop-zones registration management */
function registerDropZone(dropZoneEl, type) {
printDebug(() => "registering drop-zone if absent");
Expand Down Expand Up @@ -161,9 +167,44 @@ export function dndzone(node, options) {
dropFromOthersDisabled: false,
dropTargetStyle: DEFAULT_DROP_TARGET_STYLE,
dropTargetClasses: [],
autoAriaDisabled: false
autoAriaDisabled: false,
axis: "both"
};

function handleMoveItemUp(e) {
if (!isDragging) return;
e.preventDefault(); // prevent scrolling
e.stopPropagation();
const {items} = dzToConfig.get(node);
const children = Array.from(node.children);
const idx = children.indexOf(e.currentTarget);
printDebug(() => ["arrow down", idx]);
if (idx < children.length - 1) {
if (!config.autoAriaDisabled) {
alertToScreenReader(`Moved item ${focusedItemLabel} to position ${idx + 2} in the list ${focusedDzLabel}`);
}
swap(items, idx, idx + 1);
dispatchFinalizeEvent(node, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: focusedItemId, source: SOURCES.KEYBOARD});
}
}

function handleMoveItemDown(e) {
if (!isDragging) return;
e.preventDefault(); // prevent scrolling
e.stopPropagation();
const {items} = dzToConfig.get(node);
const children = Array.from(node.children);
const idx = children.indexOf(e.currentTarget);
printDebug(() => ["arrow up", idx]);
if (idx > 0) {
if (!config.autoAriaDisabled) {
alertToScreenReader(`Moved item ${focusedItemLabel} to position ${idx} in the list ${focusedDzLabel}`);
}
swap(items, idx, idx - 1);
dispatchFinalizeEvent(node, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: focusedItemId, source: SOURCES.KEYBOARD});
}
}

function swap(arr, i, j) {
if (arr.length <= 1) return;
arr.splice(j, 1, arr.splice(i, 1, arr[j])[0]);
Expand All @@ -189,39 +230,35 @@ export function dndzone(node, options) {
}
break;
}
case "ArrowDown":
case "ArrowDown": {
if (config.axis === "both" || config.axis === "y") {
handleMoveItemUp(e);
} else {
alertCantMoveBecauseAxis(config, "x");
}
break;
}
case "ArrowRight": {
if (!isDragging) return;
e.preventDefault(); // prevent scrolling
e.stopPropagation();
const {items} = dzToConfig.get(node);
const children = Array.from(node.children);
const idx = children.indexOf(e.currentTarget);
printDebug(() => ["arrow down", idx]);
if (idx < children.length - 1) {
if (!config.autoAriaDisabled) {
alertToScreenReader(`Moved item ${focusedItemLabel} to position ${idx + 2} in the list ${focusedDzLabel}`);
}
swap(items, idx, idx + 1);
dispatchFinalizeEvent(node, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: focusedItemId, source: SOURCES.KEYBOARD});
if (config.axis === "both" || config.axis === "x") {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

as you can see in the original code, arrow right is exactly like arrow down, it moves the item from index i to index i+1 if possible, without concerning itself with where the items are placed in space.
To achieve the axis limitation might be tricky.
It can potentially be achieved by checking bounding client rects relative to the original position of the dragged item but i think even that won't work in some edge cases

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Perhaps it makes sense to only have the limit work for pointer drag, since its mainly useful for mobile devices

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's take a step back for a minute. What is the problem that limiting the drag axis is trying to solve from a UX perspective?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for the use cases I've seen, there are often times where you're only using drag and drop for reorderability. Here's the example that led me to make this PR:
image
I have a music player, and both the queue and playlist views have a reorderable list of songs. I want it to be clear that there's no purpose to dragging other than changing the order, so limiting it to only the y-axis indicates that.

It's also nice on mobile devices in the case where your fingers may move slightly and it keeps the element in line with the list its in

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just thinking out loud here... If the feature was to only "allow dragging" in a direction that has other items you could displace, would that achieve the same goal? In the image you included the user can still drag upwards and leave the drop zone, with this implementation they will be able to drag up until the items becomes the first in the list and then moving the mouse further up (or to the side) won't take it further up (same way that locking to the axis doesn't let it move to the sides)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yea that would effectively achieve the same UX. I don't see a use case for one and not the other. Would be more work to implement tho I think

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah it would be harder to implement but would be in line with the lib supporting any special arrangement of items as it does now (e.g. grid, board etc).
If you're keen I am happy to brainstorm, otherwise I can take a crack at it but not sure when I will have time (maybe over the weekend). There will need to be a util that tracks positions of items or something like that

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to take a crack at it, there may be a relatively straightforward solution by using the indices of the items. I'm also interested in potentially converting the library to TS to make editing/maintenance easier but that would cause major conflicts with open PRs

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like TS and there were attempts at this in the past but because of all the dom manipulation it always ended up as not adding much safety.

handleMoveItemUp(e);
} else {
alertCantMoveBecauseAxis(config, "y");
}
break;
}
case "ArrowUp": {
if (config.axis === "both" || config.axis === "y") {
handleMoveItemDown(e);
} else {
alertCantMoveBecauseAxis(config, "x");
}
break;
}
case "ArrowUp":
case "ArrowLeft": {
if (!isDragging) return;
e.preventDefault(); // prevent scrolling
e.stopPropagation();
const {items} = dzToConfig.get(node);
const children = Array.from(node.children);
const idx = children.indexOf(e.currentTarget);
printDebug(() => ["arrow up", idx]);
if (idx > 0) {
if (!config.autoAriaDisabled) {
alertToScreenReader(`Moved item ${focusedItemLabel} to position ${idx} in the list ${focusedDzLabel}`);
}
swap(items, idx, idx - 1);
dispatchFinalizeEvent(node, items, {trigger: TRIGGERS.DROPPED_INTO_ZONE, id: focusedItemId, source: SOURCES.KEYBOARD});
if (config.axis === "both" || config.axis === "x") {
handleMoveItemDown(e);
} else {
alertCantMoveBecauseAxis(config, "y");
}
break;
}
Expand Down Expand Up @@ -276,7 +313,8 @@ export function dndzone(node, options) {
dropFromOthersDisabled = false,
dropTargetStyle = DEFAULT_DROP_TARGET_STYLE,
dropTargetClasses = [],
autoAriaDisabled = false
autoAriaDisabled = false,
axis = "both"
}) {
config.items = [...items];
config.dragDisabled = dragDisabled;
Expand All @@ -286,6 +324,7 @@ export function dndzone(node, options) {
config.dropTargetStyle = dropTargetStyle;
config.dropTargetClasses = dropTargetClasses;
config.autoAriaDisabled = autoAriaDisabled;
config.axis = axis;
if (config.type && newType !== config.type) {
unregisterDropZone(node, config.type);
}
Expand Down
23 changes: 14 additions & 9 deletions src/pointerAction.js
Original file line number Diff line number Diff line change
Expand Up @@ -212,22 +212,23 @@ function handleDraggedIsOverIndex(e) {
dispatchConsiderEvent(e.currentTarget, items, {trigger: TRIGGERS.DRAGGED_OVER_INDEX, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER});
}

let mouseMoveListener;
// Global mouse/touch-events handlers
function handleMouseMove(e) {
function handleMouseMove(e, config) {
e.preventDefault();
const c = e.touches ? e.touches[0] : e;
currentMousePosition = {x: c.clientX, y: c.clientY};
draggedEl.style.transform = `translate3d(${currentMousePosition.x - dragStartMousePosition.x}px, ${
currentMousePosition.y - dragStartMousePosition.y
draggedEl.style.transform = `translate3d(${config.axis === "both" || config.axis === "x" ? currentMousePosition.x - dragStartMousePosition.x : 0}px, ${
config.axis === "both" || config.axis === "y" ? currentMousePosition.y - dragStartMousePosition.y : 0
}px, 0)`;
}

function handleDrop() {
printDebug(() => "dropped");
finalizingPreviousDrag = true;
// cleanup
window.removeEventListener("mousemove", handleMouseMove);
window.removeEventListener("touchmove", handleMouseMove);
window.removeEventListener("mousemove", mouseMoveListener);
window.removeEventListener("touchmove", mouseMoveListener);
window.removeEventListener("mouseup", handleDrop);
window.removeEventListener("touchend", handleDrop);
unWatchDraggedElement();
Expand Down Expand Up @@ -336,7 +337,8 @@ export function dndzone(node, options) {
dropTargetStyle: DEFAULT_DROP_TARGET_STYLE,
dropTargetClasses: [],
transformDraggedElement: () => {},
centreDraggedOnCursor: false
centreDraggedOnCursor: false,
axis: "both"
};
printDebug(() => [`dndzone good to go options: ${toString(options)}, config: ${toString(config)}`, {node}]);
let elToIdx = new Map();
Expand Down Expand Up @@ -452,8 +454,9 @@ export function dndzone(node, options) {
dispatchConsiderEvent(originDropZone, items, {trigger: TRIGGERS.DRAG_STARTED, id: draggedElData[ITEM_ID_KEY], source: SOURCES.POINTER});

// handing over to global handlers - starting to watch the element
window.addEventListener("mousemove", handleMouseMove, {passive: false});
window.addEventListener("touchmove", handleMouseMove, {passive: false, capture: false});
mouseMoveListener = (e) => handleMouseMove(e, config);
window.addEventListener("mousemove", mouseMoveListener, {passive: false});
window.addEventListener("touchmove", mouseMoveListener, {passive: false, capture: false});
window.addEventListener("mouseup", handleDrop, {passive: false});
window.addEventListener("touchend", handleDrop, {passive: false});
}
Expand All @@ -468,7 +471,8 @@ export function dndzone(node, options) {
dropTargetStyle = DEFAULT_DROP_TARGET_STYLE,
dropTargetClasses = [],
transformDraggedElement = () => {},
centreDraggedOnCursor = false
centreDraggedOnCursor = false,
axis = "both"
}) {
config.dropAnimationDurationMs = dropAnimationDurationMs;
if (config.type && newType !== config.type) {
Expand All @@ -480,6 +484,7 @@ export function dndzone(node, options) {
config.morphDisabled = morphDisabled;
config.transformDraggedElement = transformDraggedElement;
config.centreDraggedOnCursor = centreDraggedOnCursor;
config.axis = axis;

// realtime update for dropTargetStyle
if (
Expand Down
1 change: 1 addition & 0 deletions typings/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export interface Options<T extends Item = Item> {
transformDraggedElement?: TransformDraggedElementFunction;
autoAriaDisabled?: boolean;
centreDraggedOnCursor?: boolean;
axis: "both" | "x" | "y";
}

export interface DndZoneAttributes<T> {
Expand Down