Skip to content

Commit

Permalink
Merge pull request #5605 from matuzalemsteles/issue-5591
Browse files Browse the repository at this point in the history
feat(@clayui/core): improves TreeView's `onItemHover` and `onItemMove` API
  • Loading branch information
matuzalemsteles authored Jun 28, 2023
2 parents 37ecb6b + dfa8e9e commit 6c75675
Show file tree
Hide file tree
Showing 5 changed files with 186 additions and 67 deletions.
4 changes: 2 additions & 2 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,8 @@ module.exports = {
'./packages/clay-core/src/tree-view/': {
branches: 56,
functions: 65,
lines: 70,
statements: 69,
lines: 67,
statements: 67,
},
'./packages/clay-data-provider/src/': {
branches: 69,
Expand Down
206 changes: 155 additions & 51 deletions packages/clay-core/src/tree-view/DragAndDrop.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ import React, {
import {createPortal} from 'react-dom';

import {LiveAnnouncer} from '../live-announcer';
import {useTreeViewContext} from './context';
import {MoveItemIndex, useTreeViewContext} from './context';
import {createImmutableTree} from './useTree';

import type {AnnouncerAPI} from '../live-announcer';

Expand All @@ -33,6 +34,16 @@ export type DragAndDropMessages = {
insertBefore: string;
};

export type Value = {
[propName: string]: any;
indexes: Array<number>;
itemRef: React.RefObject<HTMLDivElement>;
key: React.Key;
nextKey?: React.Key;
parentItemRef: React.RefObject<HTMLDivElement>;
prevKey?: React.Key;
};

type ContextProps = {
mode: 'keyboard' | 'mouse' | null;
position: 'bottom' | 'middle' | 'top' | null;
Expand All @@ -59,10 +70,13 @@ type State = Pick<

const DnDContext = React.createContext<ContextProps>({} as ContextProps);

type Props = {
rootRef: React.RefObject<HTMLUListElement>;
type Props<T> = {
children: React.ReactNode;
messages?: DragAndDropMessages;
nestedKey: string;
onItemHover?: (item: T, parentItem: T, index: MoveItemIndex) => boolean;
onItemMove?: (item: T, parentItem: T, index: MoveItemIndex) => boolean;
rootRef: React.RefObject<HTMLUListElement>;
};

function getFocusableTree(rootRef: React.RefObject<HTMLUListElement>) {
Expand Down Expand Up @@ -139,12 +153,15 @@ const defaultMessages: DragAndDropMessages = {
insertBefore: 'Insert on top of the',
};

export const DragAndDropProvider = ({
export function DragAndDropProvider<T>({
children,
messages = defaultMessages,
nestedKey,
onItemMove,
onItemHover,
rootRef,
}: Props) => {
const {dragAndDrop, layout, reorder} = useTreeViewContext();
}: Props<T>) {
const {dragAndDrop, items, layout, reorder} = useTreeViewContext();

const announcerRef = useRef<AnnouncerAPI>(null);

Expand Down Expand Up @@ -209,12 +226,47 @@ export const DragAndDropProvider = ({
[]
);

const onCancel = useCallback(() => {
announcerRef.current?.announce(messages.dropCanceled);
setState((state) => ({
currentDrag: null,
currentTarget: null,
lastItem: state.currentDrag,
mode: null,
position: null,
status: 'canceled',
}));
}, []);

const onDrop = useCallback(() => {
const {currentDrag, currentTarget, position} = state;
const dropItem = layout.layoutKeys.current.get(currentTarget!);
const dragItem = layout.layoutKeys.current.get(currentDrag!);
const dropLayoutItem = layout.layoutKeys.current.get(currentTarget!);
const dragLayoutItem = layout.layoutKeys.current.get(currentDrag!);

reorder(dragItem!.loc, getNewItemPath(dropItem!.loc, position!));
const indexes = getNewItemPath(dropLayoutItem!.loc, position!);

if (onItemMove) {
const tree = createImmutableTree(items as any, nestedKey!);

const dragNode = tree.nodeByPath(dragLayoutItem!.loc);

const isMoved = onItemMove(
dragNode.item as Record<any, any>,
tree.nodeByPath(indexes).parent as Record<any, any>,
{
next: indexes[indexes.length - 1],
previous: dragNode.index,
}
);

if (!isMoved) {
onCancel();

return;
}
}

reorder(dragLayoutItem!.loc, indexes);
setState({
currentDrag: null,
currentTarget: null,
Expand All @@ -224,19 +276,7 @@ export const DragAndDropProvider = ({
status: 'complete',
});
announcerRef.current?.announce(messages.dropComplete);
}, [state]);

const onCancel = useCallback(() => {
announcerRef.current?.announce(messages.dropCanceled);
setState((state) => ({
currentDrag: null,
currentTarget: null,
lastItem: state.currentDrag,
mode: null,
position: null,
status: 'canceled',
}));
}, []);
}, [state, onCancel]);

useEffect(() => {
if (state.lastItem && state.status) {
Expand Down Expand Up @@ -273,6 +313,8 @@ export const DragAndDropProvider = ({

useEffect(() => {
if (state.mode === 'keyboard') {
const denylist = new Set<React.Key>();

const onKeyDown = (event: KeyboardEvent) => {
switch (event.key) {
case Keys.Esc:
Expand All @@ -293,58 +335,120 @@ export const DragAndDropProvider = ({
event.preventDefault();
event.stopPropagation();

const items = getFocusableTree(rootRef);
const position = items.findIndex((element) =>
const focusableItems = getFocusableTree(rootRef).filter(
(item) => {
if (item.getAttribute('data-dnd-dropping')) {
return true;
}

const [type, key] = item
.getAttribute('data-id')!
.split(',');

return !denylist.has(
type === 'number' ? Number(key) : key
);
}
);
const position = focusableItems.findIndex((element) =>
element.getAttribute('data-dnd-dropping')
);

const item =
items[
focusableItems[
event.key === Keys.Up
? position - 1
: position + 1
];

if (
const newState: State = {
...state,
};

if (denylist.has(newState.currentTarget!)) {
const [type, key] = item
.getAttribute('data-id')!
.split(',');

newState.position =
event.key === Keys.Up ? 'top' : 'bottom';
newState.currentTarget =
type === 'number' ? Number(key) : key;
} else if (
(event.key === Keys.Up &&
state.position === 'bottom') ||
(event.key === Keys.Down &&
state.position === 'top')
) {
setState((state) => ({
...state,
position: 'middle',
}));
newState.position = 'middle';
} else if (
event.key === Keys.Down &&
state.position === 'middle'
) {
setState((state) => ({
...state,
position: 'bottom',
}));
newState.position = 'bottom';
} else {
if (!item) {
setState((state) => ({
...state,
position: position === 0 ? 'top' : 'bottom',
}));

return;
newState.position =
position === 0 ? 'top' : 'bottom';
} else {
const [type, key] = item
.getAttribute('data-id')!
.split(',');

newState.position =
event.key === Keys.Up ? 'bottom' : 'middle';
newState.currentTarget =
type === 'number' ? Number(key) : key;
}
}

const [type, key] = item
.getAttribute('data-id')!
.split(',');
if (onItemHover) {
const dropLayoutItem =
layout.layoutKeys.current.get(
newState.currentTarget!
);
const dragLayoutItem =
layout.layoutKeys.current.get(
newState.currentDrag!
);
const tree = createImmutableTree(
items as any,
nestedKey!
);
const indexes = getNewItemPath(
dropLayoutItem!.loc,
newState.position!
);

const dragNode = tree.nodeByPath(
dragLayoutItem!.loc
);

const isHovered = onItemHover(
dragNode.item as Record<any, any>,
tree.nodeByPath(indexes).parent as Record<
any,
any
>,
{
next: indexes[indexes.length - 1],
previous: dragNode.index,
}
);

if (!isHovered) {
// Removes the item from the list so that the next function
// call looks for the next element.
denylist.add(newState.currentTarget!);

// Try moving to the next item.
onKeyDown(event);

setState((state) => ({
...state,
currentTarget:
type === 'number' ? Number(key) : key,
position:
event.key === Keys.Up ? 'bottom' : 'middle',
}));
return;
}
}

setState(newState);
break;
}
default:
Expand Down Expand Up @@ -436,7 +540,7 @@ export const DragAndDropProvider = ({
)}
</DnDContext.Provider>
);
};
}

export const TARGET_POSITION = {
BOTTOM: 'bottom',
Expand Down
10 changes: 8 additions & 2 deletions packages/clay-core/src/tree-view/TreeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ interface ITreeViewProps<T>
* The callback is called whenever there is an item dragging over
* another item.
*/
onItemHover?: (item: T, parentItem: T, index: MoveItemIndex) => void;
onItemHover?: (item: T, parentItem: T, index: MoveItemIndex) => boolean;

/**
* Callback is called when an item is about to be moved elsewhere in the tree.
Expand Down Expand Up @@ -228,7 +228,13 @@ export function TreeView<T>({
>
<DndProvider backend={HTML5Backend} context={dragAndDropContext}>
<TreeViewContext.Provider value={context}>
<DragAndDropProvider messages={messages} rootRef={rootRef}>
<DragAndDropProvider<T>
messages={messages}
nestedKey={nestedKey}
onItemHover={onItemHover}
onItemMove={onItemMove}
rootRef={rootRef}
>
<FocusWithinProvider
containerRef={rootRef}
focusableElements={focusableElements}
Expand Down
2 changes: 1 addition & 1 deletion packages/clay-core/src/tree-view/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export interface ITreeViewContext<T> extends ITreeState<T> {
expanderIcons?: Icons;
nestedKey?: string;
onItemMove?: (item: T, parentItem: T, index: MoveItemIndex) => boolean;
onItemHover?: (item: T, parentItem: T, index: MoveItemIndex) => void;
onItemHover?: (item: T, parentItem: T, index: MoveItemIndex) => boolean;
onLoadMore?: OnLoadMore<T>;
onSelect?: (item: T) => void;
onRenameItem?: (item: T) => Promise<any>;
Expand Down
Loading

0 comments on commit 6c75675

Please sign in to comment.