Skip to content

Custom getKeysForDrag bug - Drag and drop selected items seperatly with GridList #8162

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

Open
JonRC opened this issue Apr 29, 2025 · 4 comments
Labels
enhancement New feature or request help wanted Extra attention is needed

Comments

@JonRC
Copy link

JonRC commented Apr 29, 2025

Provide a general summary of the issue here

The documentation says that getKeysForDrag returns the selected items.
I want to overwrite this function to drag and drop selected items separately.
Even though I overwrite it, drag and drop don't work as expected.

GridList.bug.mp4

Overwriting getKeysForDrag, full example in "Steps to reproduce"

  dragAndDropHooks.useDraggableCollectionState =
    function useDraggableCollectionStateOverride(
      props: DraggableCollectionStateOpts
    ) {
      const draggableHook = useDraggableCollectionState({
        ...props,
        ...options,
      } as DraggableCollectionStateOptions);
      draggableHook.getKeysForDrag = function (key: string) {
        return new Set([key]); //Just the clicked item be a key for drag
      };
      return draggableHook;
    };

🤔 Expected Behavior?

Items of a GridList to be reordered (drag and drop) separately

😯 Current Behavior

All the selected keys are dragged

💁 Possible Solution

I'm not sure, I didn't realize looking at the source code where onReorder evaluates the event.keys

🔦 Context

No response

🖥️ Steps to Reproduce

Repository

You can just clone and run this repository:
https://github.com/JonRC/react-aria-grid-list-bug

Code

Here is the code used in the repository below if you want reproduce by your own

/* eslint-disable @typescript-eslint/no-empty-object-type */
import {
  DraggableCollectionStateOptions,
  useDraggableCollectionState,
  useListData,
} from "react-stately";
import {
  GridList,
  GridListItem,
  useDragAndDrop,
  Checkbox as AriaCheckbox,
  CheckboxProps,
  DragAndDropOptions,
} from "react-aria-components";
import { useMemo } from "react";

interface DraggableCollectionStateOpts
  extends Omit<DraggableCollectionStateOptions, "getItems"> {}

function App() {
  const list = useListData({
    initialItems: [
      { id: 1, name: "Adobe Photoshop" },
      { id: 2, name: "Adobe XD" },
      { id: 3, name: "Adobe Dreamweaver" },
      { id: 4, name: "Adobe InDesign" },
      { id: 5, name: "Adobe Connect" },
    ],
  });

  const options = useMemo<DragAndDropOptions>(
    () => ({
      getItems: (keys) =>
        [...keys].map((key) => ({
          "text/plain": list.getItem(key)?.name ?? "",
        })),
      onReorder(e) {
        if (e.target.dropPosition === "before") {
          list.moveBefore(e.target.key, e.keys);
        } else if (e.target.dropPosition === "after") {
          list.moveAfter(e.target.key, e.keys);
        }
      },
    }),
    [list]
  );

  const { dragAndDropHooks } = useDragAndDrop(options);

  dragAndDropHooks.useDraggableCollectionState =
    function useDraggableCollectionStateOverride(
      props: DraggableCollectionStateOpts
    ) {
      const draggableHook = useDraggableCollectionState({
        ...props,
        ...options,
      } as DraggableCollectionStateOptions);
      draggableHook.getKeysForDrag = function (key: string) {
        return new Set([key]); //Just the clicked item be a key for drag
      };
      return draggableHook;
    };

  return (
    <GridList
      aria-label="Reorderable list"
      selectionMode="multiple"
      items={list.items}
      dragAndDropHooks={dragAndDropHooks}
      style={{ marginLeft: "100px" }}
    >
      {(item) => (
        <GridListItem style={{ display: "flex" }}>
          {() => (
            <>
              <Checkbox slot="selection" />
              {item.name}
            </>
          )}
        </GridListItem>
      )}
    </GridList>
  );
}

export function Checkbox(props: CheckboxProps) {
  return (
    <AriaCheckbox {...props} style={{ display: "flex" }}>
      {({ isSelected, isIndeterminate }) => (
        <>
          <div>{isIndeterminate ? "[ ]" : isSelected ? "[x]" : "[ ]"}</div>
          {props.children}
        </>
      )}
    </AriaCheckbox>
  );
}

export default App;

Version

"react-aria-components": "^1.8.0"

What browsers are you seeing the problem on?

Chrome

If other, please specify.

No response

What operating system are you using?

Linux, Ubuntu

🧢 Your Company/Team

No response

🕷 Tracking Issue

No response

@JonRC
Copy link
Author

JonRC commented Apr 29, 2025

Discovered that this behaviour is hardcoded.
Look at react-spectrum/packages/@react-stately/dnd/src
/useDraggableCollectionState.ts:73

  let getKeys = (key: Key) => {
    // The clicked item is always added to the drag. If it is selected, then all of the
    // other selected items are also dragged. If it is not selected, the only the clicked
    // item is dragged. This matches native macOS behavior.
    let keys = new Set(
      selectionManager.isSelected(key)
        ? new Set([...selectionManager.selectedKeys].filter((key) => !!collection.getItem(key)))
        : [],
    );

    keys.add(key);
    return keys;
  };

I propose to receive an optional parameter called getKeysForDrag and replace the name of getKeys with defaulGetKetsForDrag and use it as default when getKeysForDrag is not passed.

I'd like to receive feedback from the contributor. This way, I can implement this improvement if necessary.

@LFDanLu
Copy link
Member

LFDanLu commented Apr 30, 2025

Mind sharing what overall UX you are trying to accomplish with this? We could perhaps provide the draggedKey that is set here to getKeys so that users can override this behavior like you mentioned

@LFDanLu LFDanLu added the enhancement New feature or request label Apr 30, 2025
@JonRC
Copy link
Author

JonRC commented Apr 30, 2025

I'm trying to drag selected items separately. Look at the video to see how the selected items are dragged together.

I think your idea is a good option.
I will create a pull request implementing it, it's me first contribution here.

@LFDanLu
Copy link
Member

LFDanLu commented Apr 30, 2025

To elaborate, we understand that you want to drag an item independently from the GridList's selection state, but we are curious what high level user experience you are aiming for (would selection be taken into account for other actions? would the user never be able to drag multiple items at once? what kind of app flow would this be used in).

As for an example of the idea, I was thinking along the lines of https://react-spectrum.adobe.com/react-aria/useDraggableCollection.html#example where perhaps the getItems could include the draggedKey as one of its args, and thus the user could instead opt to only return that key's data. Not a fully fleshed out idea though since ideally, would need to try it first. Essentially mimics the api we have for renderPreview.

@dannify dannify added the help wanted Extra attention is needed label May 7, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request help wanted Extra attention is needed
Projects
None yet
Development

No branches or pull requests

3 participants