import { DemoBox } from "../../src/components/demo/demo-box";
import {FeaturePageHeader} from "../../src/components/docs-page/feature-page-header";

<FeaturePageHeader
    title="Keyboard Drag and Drop"
    subtitle="Keyboard-controlled Drag and Drop for assistive technologies"
    feature="keyboard-drag-and-drop"
/>

The Keyboard Drag and Drop feature provides a way to use the drag-and-drop functionality of the tree using only the keyboard.
This allows you to implement drag-based functionality while staying compliant with accessibility standards, by providing
the default drag capability with keyboard-controlled hotkeys and a visually hidden assistive text indicating the drag-process
to screen readers.

!Dragging items with Keyboard controls Demo

```ts jsx
import type { Meta } from "@storybook/react";
import React, { Fragment } from "react";
import {
  DragTarget,
  ItemInstance,
  createOnDropHandler,
  dragAndDropFeature,
  hotkeysCoreFeature,
  insertItemsAtTarget,
  keyboardDragAndDropFeature,
  removeItemsFromParents,
  selectionFeature,
  syncDataLoaderFeature,
} from "@headless-tree/core";
import { AssistiveTreeDescription, useTree } from "@headless-tree/react";
import cn from "classnames";
import { DemoItem, createDemoData } from "../utils/data";

const meta = {
  title: "React/Drag and Drop/Visible Assistive Text",
  tags: ["feature/dnd", "homepage"],
} satisfies Meta;

export default meta;

const { syncDataLoader, data } = createDemoData();
let newItemId = 0;
const insertNewItem = (dataTransfer: DataTransfer) => {
  const newId = `new-${newItemId++}`;
  data[newId] = {
    name: dataTransfer.getData("text/plain"),
  };
  return newId;
};

const onDropForeignDragObject = (
  dataTransfer: DataTransfer,
  target: DragTarget<DemoItem>,
) => {
  const newId = insertNewItem(dataTransfer);
  insertItemsAtTarget([newId], target, (item, newChildrenIds) => {
    data[item.getId()].children = newChildrenIds;
  });
};
const onCompleteForeignDrop = (items: ItemInstance<DemoItem>[]) =>
  removeItemsFromParents(items, (item, newChildren) => {
    item.getItemData().children = newChildren;
  });
const getCssClass = (item: ItemInstance<DemoItem>) =>
  cn("treeitem", {
    focused: item.isFocused(),
    expanded: item.isExpanded(),
    selected: item.isSelected(),
    folder: item.isFolder(),
    drop: item.isDragTarget(),
  });

// story-start
export const VisibleAssistiveText = () => {
  const tree = useTree<DemoItem>({
    initialState: {
      expandedItems: ["fruit"],
      selectedItems: ["banana", "orange"],
    },
    rootItemId: "root",
    getItemName: (item) => item.getItemData().name,
    isItemFolder: (item) => !!item.getItemData().children,
    canReorder: true,
    onDrop: createOnDropHandler((item, newChildren) => {
      data[item.getId()].children = newChildren;
    }),
    onDropForeignDragObject,
    onCompleteForeignDrop,
    createForeignDragObject: (items) => ({
      format: "text/plain",
      data: items.map((item) => item.getId()).join(","),
    }),
    canDropForeignDragObject: (_, target) => target.item.isFolder(),
    indent: 20,
    dataLoader: syncDataLoader,
    features: [
      syncDataLoaderFeature,
      selectionFeature,
      hotkeysCoreFeature,
      dragAndDropFeature,
      keyboardDragAndDropFeature,
    ],
  });

  const dndState = tree.getState().dnd;

  return (
    <>
      <div {...tree.getContainerProps()} className="tree">
        <p className="description">
          The text below is the default description rendered by{" "}
          {"<AssistiveTreeDescription />"}, which explains the drag-process to
          assistive software. It is normally hidden to the visuals of the
          webpage, but forced to be visible in this sample for demonstration
          purposes:
        </p>
        <AssistiveTreeDescription
          tree={tree}
          className="visible-assistive-text description"
        />
        {tree.getItems().map((item) => (
          <button
            key={item.getId()}
            {...item.getProps()}
            style={{ paddingLeft: `${item.getItemMeta().level * 20}px` }}
          >
            <div className={getCssClass(item)}>{item.getItemName()}</div>
          </button>
        ))}
        <div style={tree.getDragLineStyle()} className="dragline" />
      </div>

      <div className="actionbar">
        <div
          className="foreign-dragsource"
          draggable
          onDragStart={(e) => {
            e.dataTransfer.setData("text/plain", "hello world");
          }}
        >
          Drag me into the tree!
        </div>
        <div
          className="foreign-dropzone"
          onDrop={(e) => {
            alert(JSON.stringify(e.dataTransfer.getData("text/plain")));
            console.log(e.dataTransfer.getData("text/plain"));
          }}
          onDragOver={(e) => e.preventDefault()}
        >
          Drop items here!
        </div>
      </div>

      <div className="actionbar">
        <button
          className="actionbtn"
          onClick={() => {
            const dataTransfer = new DataTransfer();
            dataTransfer.setData("text/plain", "hello world");
            tree.startKeyboardDragOnForeignObject(dataTransfer);
            tree.updateDomFocus();
          }}
        >
          Initiate keyboard drag on a foreign object
        </button>

        {dndState && (
          <button
            className="actionbtn"
            onClick={async () => {
              if (dndState?.draggedItems) {
                await onCompleteForeignDrop(dndState.draggedItems);
                alert(dndState.draggedItems.map((item) => item.getItemName()));
              }
              tree.stopKeyboardDrag();
            }}
          >
            Accept dragged items from tree
          </button>
        )}
      </div>
    </>
  );
};
```

## Setup

The Keyboard Drag and Drop feature mostly builds upon the normal [Drag and Drop](https://headless-tree.lukasbach.com/llm/features/dnd.md) feature, integrating
it mostly just means to add it as feature to the tree configuration and it will provide the necessary functionality
for keyboard-controlled drag-and-drop.

Note that this feature needs both the `dragAndDropFeature` and the `hotkeysCoreFeature` to work properly.

```
features: [
  syncDataLoaderFeature,
  selectionFeature,
  hotkeysCoreFeature,
  dragAndDropFeature,
  keyboardDragAndDropFeature,
],
```

One important aspect of the Keyboard Drag and Drop feature is the assistive text, which is used to inform screen readers
about the drag-and-drop process.

The `@headless-tree/react` package exposes a AssistiveTreeDescription
react component, that will automatically render the english assistive text for you in a visually hidden way. It
is recommended to put this component inside the tree root container, or otherwise reference it with aria-tags
in the tree.

```jsx
<div {...tree.getContainerProps()} className="tree">
  <AssistiveTreeDescription tree={tree} />
  {/* ...tree items... */}
</div>
````

Alternatively, you can build this text yourself by using the data available in the tree state that you can retrieve
with `tree.getState()`. You can use treeState.assistiveDndState
of type AssistiveDndState to determine in which state the drag-and-drop process is,
and treeState.dnd for details about what is being dragged and where the user
is dropping it.

```ts
enum AssistiveDndState {
  None,
  Started,
  Dragging,
  Completed,
  Aborted,
}
```

## Dragging items out of the tree with keyboard

Selecting items inside the tree, and then dragging them out of the tree into a third-party component
is a core-functionality of the normal DnD Feature. You can find out more about that in the
[Guide on dragging in and out of trees](https://headless-tree.lukasbach.com/llm/dnd/foreign-dnd.md).

Since there is no actual drag-event happening in keyboard-bound situations, you need to implement
custom logic to accept a keyboard-controlled drag initiated in a Headless Tree instance.

You can detect whether the user is dragging items via keyboard by inspecting `tree.getState().assistiveDndState`,
if it is either `AssistiveDndState.Started` or `AssistiveDndState.Dragging`. In the logic that accepts such
a drag, you can get information about the items that are being dragged by inspecting `tree.getState().dnd?.draggedItems`.
You can then handle the dragged items (e.g. by removing them from the tree with `removeItemsFromParents()`)
and complete the keyboard drag with `tree.stopKeyboardDrag()`.

```jsx
<button
    onClick={async () => {
        const draggedItems = tree.getState().dnd.draggedItems;
        if (draggedItems) {
            await onCompleteForeignDrop(draggedItems);
            alert(draggedItems.map((item) => item.getItemName()));
        }
        tree.stopKeyboardDrag();
    }}
>
    Accept dragged items from tree
</button>
```

!Keyboard Drag tree items out of tree demo

## Dragging foreign objects inside the tree with keyboard

Similarly, moving data from outside the tree into it via a drag event
is also a core-functionality of the normal DnD Feature. You can find out more about that in the
[Guide on dragging in and out of trees](https://headless-tree.lukasbach.com/llm/dnd/foreign-dnd.md).

Again, this functionality works mostly out-of-the-box with headless tree, but the initiation of a keyboard-bound
drag event that can be accepted by Headless Tree needs to be done externally. If the user triggers something
that you consider a keyboard-bound drag event within the boundaries of your application, you can inform Headless
Tree that this is happening by calling `tree.startKeyboardDragOnForeignObject(dataTransfer);` with a custom
`dataTranfer` object, that you handle the same way as with normal foreign drag events. You can create this object
manually.

```jsx

<button
  onClick={() => {
    const dataTransfer = new DataTransfer();
    dataTransfer.setData("text/plain", "hello world");
    tree.startKeyboardDragOnForeignObject(dataTransfer);
  }}
>
  Initiate keyboard drag on a foreign object
</button>
```

!Keyboard Drag foreign object inside of tree demo