Skip to content

Commit

Permalink
Merge pull request #9 from jagaapple/feature/add-kanban-story
Browse files Browse the repository at this point in the history
# New Features
- Add support for horizontal direction
- Add `isLonely` Prop to Item component
- Add `draggingCursorStyle` Prop to List component
- Add a sample Kanban story


# Changes and Fixes
- Modify styles to support for collapsing margins
- Fix to call `onDragEnd` callback function in disabled items
- Rename `isDisabled` Prop of Item to `isLocked`
- Fix to stack when a group has other elements
- Change DOM node structures and delete `className` Prop from Item
- Fix to clear a dragging node if a cursor leaves from items


# Refactors
- Rename some stories
- Modify JS Doc
  • Loading branch information
jagaapple committed Mar 19, 2020
2 parents 56d235c + 9faa718 commit a12cadd
Show file tree
Hide file tree
Showing 57 changed files with 1,554 additions and 644 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@
"clean": "rm -rf ./lib ./coverage",
"storybook": "start-storybook --port 5000",
"build-storybook": "build-storybook --config-dir ./.storybook --output-dir ./.out",
"chromatic": "CHROMATIC_APP_CODE=kmmyjbiwtyq chromatic"
"chromatic": "CHROMATIC_APP_CODE=kmmyjbiwtyq chromatic --auto-accept-changes"
},
"config": {},
"dependencies": {
Expand Down
4 changes: 2 additions & 2 deletions src/groups/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,5 @@ import { ItemIdentifier } from "../shared";
export const Context = React.createContext<{
identifier: ItemIdentifier | undefined;
ancestorIdentifiers: ItemIdentifier[];
hasNoItems: boolean;
}>({ identifier: undefined, ancestorIdentifiers: [], hasNoItems: false });
childIdentifiersRef: React.MutableRefObject<Set<ItemIdentifier>>;
}>({ identifier: undefined, ancestorIdentifiers: [], childIdentifiersRef: { current: new Set() } });
165 changes: 120 additions & 45 deletions src/item.component.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {
clearBodyStyle,
clearGhostElementStyle,
getDropLinePositionItemIndex,
getPlaceholderElementStyle,
getStackedGroupElementStyle,
initializeGhostElementStyle,
moveGhostElement,
setBodyStyle,
Expand All @@ -30,8 +32,12 @@ type Props<T extends ItemIdentifier> = {
* Stacking and popping will be allowed. Grandchild items will not be affected.
* @default false
*/
isDisabled?: boolean;
className?: string;
isLocked?: boolean;
/**
* Whether it is impossible to put items on both sides of this item.
* @default false
*/
isLonely?: boolean;
children?: React.ReactNode;
};

Expand All @@ -41,17 +47,37 @@ export const Item = <T extends ItemIdentifier>(props: Props<T>) => {

const ancestorIdentifiers = [...groupContext.ancestorIdentifiers, props.identifier];
const isGroup = props.isGroup ?? false;
const isDisabled = (listContext.isDisabled || props.isDisabled) ?? false;
const hasNoItems =
isGroup &&
React.useMemo(() => React.Children.toArray(props.children).filter((child: any) => child.type === Item).length === 0, [
props.children,
]);
const isLocked = (listContext.isDisabled || props.isLocked) ?? false;
const isLonley = props.isLonely ?? false;

// Registers an identifier to the group context.
const childIdentifiersRef = React.useRef<Set<ItemIdentifier>>(new Set());
React.useEffect(() => {
groupContext.childIdentifiersRef.current.add(props.identifier);

return () => {
groupContext.childIdentifiersRef.current.delete(props.identifier);
};
}, []);

// Clears timers.
const clearingDraggingNodeTimeoutIdRef = React.useRef<number>();
React.useEffect(
() => () => {
window.clearTimeout(clearingDraggingNodeTimeoutIdRef.current);
},
[],
);

const onDragStart = React.useCallback(
(element: HTMLElement) => {
setBodyStyle(document.body);
initializeGhostElementStyle(element, listContext.ghostWrapperElementRef.current ?? undefined);
setBodyStyle(document.body, listContext.draggingCursorStyle);
initializeGhostElementStyle(
element,
listContext.ghostWrapperElementRef.current ?? undefined,
listContext.itemSpacing,
listContext.direction,
);

// Sets contexts to values.
const nodeMeta = getNodeMeta(
Expand All @@ -72,7 +98,17 @@ export const Item = <T extends ItemIdentifier>(props: Props<T>) => {
isGroup: nodeMeta.isGroup,
});
},
[listContext.onDragStart, groupContext.identifier, props.identifier, props.index, ancestorIdentifiers, isGroup],
[
listContext.itemSpacing,
listContext.direction,
listContext.onDragStart,
listContext.draggingCursorStyle,
groupContext.identifier,
props.identifier,
props.index,
ancestorIdentifiers,
isGroup,
],
);
const onDragEnd = React.useCallback(() => {
clearBodyStyle(document.body);
Expand Down Expand Up @@ -104,7 +140,7 @@ export const Item = <T extends ItemIdentifier>(props: Props<T>) => {
const isNeededInitialization =
draggingNodeMeta == undefined ||
props.identifier === draggingNodeMeta.identifier ||
checkIsAncestorItem(draggingNodeMeta.identifier, true, ancestorIdentifiers);
checkIsAncestorItem(draggingNodeMeta.identifier, ancestorIdentifiers);
if (isNeededInitialization) {
listContext.setIsVisibleDropLineElement(false);
listContext.hoveredNodeMetaRef.current = undefined;
Expand Down Expand Up @@ -148,13 +184,19 @@ export const Item = <T extends ItemIdentifier>(props: Props<T>) => {
);
const onMoveForItems = React.useCallback(
(draggingNodeMeta: NodeMeta<T>, hoveredNodeMeta: NodeMeta<T>, absoluteXY: [number, number]) => {
if (isLonley) {
listContext.setIsVisibleDropLineElement(false);
listContext.destinationMetaRef.current = undefined;

return;
}
if (draggingNodeMeta.index !== hoveredNodeMeta.index) listContext.setIsVisibleDropLineElement(true);

const dropLineElement = listContext.dropLineElementRef.current ?? undefined;
setDropLineElementStyle(dropLineElement, listContext.itemSpacing, absoluteXY, hoveredNodeMeta);
setDropLineElementStyle(dropLineElement, absoluteXY, hoveredNodeMeta, listContext.direction);

// Calculates the next index.
const dropLineDirection = getDropLineDirectionFromXY(absoluteXY, hoveredNodeMeta);
const dropLineDirection = getDropLineDirectionFromXY(absoluteXY, hoveredNodeMeta, listContext.direction);
const nextIndex = getDropLinePositionItemIndex(
dropLineDirection,
draggingNodeMeta.index,
Expand All @@ -181,7 +223,15 @@ export const Item = <T extends ItemIdentifier>(props: Props<T>) => {
listContext.setStackedGroupIdentifier(undefined);
listContext.destinationMetaRef.current = { groupIdentifier: groupContext.identifier, index: nextIndex };
},
[listContext.itemSpacing, listContext.onStackGroup, groupContext.identifier, props.identifier, props.index, isGroup],
[
listContext.direction,
listContext.onStackGroup,
groupContext.identifier,
props.identifier,
props.index,
isGroup,
isLonley,
],
);
const onMove = React.useCallback(
(absoluteXY: [number, number]) => {
Expand All @@ -190,14 +240,33 @@ export const Item = <T extends ItemIdentifier>(props: Props<T>) => {
const hoveredNodeMeta = listContext.hoveredNodeMetaRef.current;
if (hoveredNodeMeta == undefined) return;

if (hasNoItems && checkIsInStackableArea(absoluteXY, hoveredNodeMeta, listContext.stackableAreaThreshold)) {
const hasNoItems = childIdentifiersRef.current.size === 0;
if (
isGroup &&
hasNoItems &&
checkIsInStackableArea(absoluteXY, hoveredNodeMeta, listContext.stackableAreaThreshold, listContext.direction)
) {
onMoveForStackableGroup(hoveredNodeMeta);
} else {
onMoveForItems(draggingNodeMeta, hoveredNodeMeta, absoluteXY);
}
},
[listContext.draggingNodeMeta, hasNoItems, onMoveForStackableGroup, onMoveForItems],
[listContext.draggingNodeMeta, listContext.direction, onMoveForStackableGroup, onMoveForItems, isGroup],
);
const onLeave = React.useCallback(() => {
if (listContext.draggingNodeMeta == undefined) return;

// Clears a dragging node after 50ms in order to prevent setting and clearing at the same time.
window.clearTimeout(clearingDraggingNodeTimeoutIdRef.current);
clearingDraggingNodeTimeoutIdRef.current = window.setTimeout(() => {
if (listContext.hoveredNodeMetaRef.current?.identifier != props.identifier) return;

listContext.setIsVisibleDropLineElement(false);
listContext.setStackedGroupIdentifier(undefined);
listContext.hoveredNodeMetaRef.current = undefined;
listContext.destinationMetaRef.current = undefined;
}, 50);
}, [listContext.draggingNodeMeta, props.identifier]);

const binder = useGesture({
onHover: ({ event }) => {
Expand All @@ -213,14 +282,16 @@ export const Item = <T extends ItemIdentifier>(props: Props<T>) => {
if (listContext.draggingNodeMeta == undefined) return;

// Skips if this item is an ancestor group of the dragging item.
const hasItems = childIdentifiersRef.current.size > 0;
const hoveredNodeAncestors = listContext.hoveredNodeMetaRef.current?.ancestorIdentifiers ?? [];
if (checkIsAncestorItem(props.identifier, !hasNoItems, hoveredNodeAncestors)) return;
if (hasItems && checkIsAncestorItem(props.identifier, hoveredNodeAncestors)) return;
if (props.identifier === listContext.draggingNodeMeta.identifier) return;
// Skips if the dragging item is an ancestor group of this item.
if (checkIsAncestorItem(listContext.draggingNodeMeta.identifier, true, ancestorIdentifiers)) return;
if (checkIsAncestorItem(listContext.draggingNodeMeta.identifier, ancestorIdentifiers)) return;

onMove(xy);
},
onPointerLeave: onLeave,
});
const draggableBinder = useGesture({
onDragStart: (state: any) => {
Expand All @@ -231,61 +302,65 @@ export const Item = <T extends ItemIdentifier>(props: Props<T>) => {
event.persist();
event.stopPropagation();

if (isDisabled) return;
if (isLocked) return;

onDragStart(element);
},
onDrag: ({ down, movement }) => {
if (isLocked) return;
if (!down) return;

moveGhostElement(listContext.ghostWrapperElementRef.current ?? undefined, movement);
},
onDragEnd,
onDragEnd: () => {
if (isLocked) return;

onDragEnd();
},
});

const contentElement = React.useMemo((): JSX.Element => {
const draggingNodeMeta = listContext.draggingNodeMeta;
const isDragging = draggingNodeMeta != undefined && props.identifier === draggingNodeMeta.identifier;
const placeholderRenderer = listContext.renderPlaceholder;
const stackedGroupRenderer = listContext.renderStackedGroup;
const { renderPlaceholder, renderStackedGroup, itemSpacing, direction } = listContext;

const style: React.CSSProperties = {
boxSizing: "border-box",
position: "static",
margin: `${listContext.itemSpacing}px 0`,
};
const rendererMeta: Omit<PlaceholderRendererMeta<any>, "isGroup"> | StackedGroupRendererMeta<any> = {
identifier: props.identifier,
groupIdentifier: groupContext.identifier,
index: props.index,
};
if (listContext.stackedGroupIdentifier === props.identifier && stackedGroupRenderer != undefined) {
const hoveredNodeMeta = listContext.hoveredNodeMetaRef.current;

return stackedGroupRenderer(
{ binder, style: { ...style, width: hoveredNodeMeta?.width, height: hoveredNodeMeta?.height } },
rendererMeta,
);
let children = props.children;
if (isDragging && renderPlaceholder != undefined) {
const style = getPlaceholderElementStyle(draggingNodeMeta, itemSpacing, direction);
children = renderPlaceholder({ binder, style }, { ...rendererMeta, isGroup });
}
if (isDragging && placeholderRenderer != undefined) {
return placeholderRenderer(
{ binder, style: { ...style, width: draggingNodeMeta?.width, height: draggingNodeMeta?.height } },
{ ...rendererMeta, isGroup },
);
if (listContext.stackedGroupIdentifier === props.identifier && renderStackedGroup != undefined) {
const style = getStackedGroupElementStyle(listContext.hoveredNodeMetaRef.current, itemSpacing, direction);
children = renderStackedGroup({ binder, style }, rendererMeta);
}

const padding: [string, string] = ["0", "0"];
if (direction === "vertical") padding[0] = `${itemSpacing / 2}px`;
if (direction === "horizontal") padding[1] = `${itemSpacing / 2}px`;

return (
<div className={props.className} style={style} {...binder()} {...draggableBinder()}>
{props.children}
<div
style={{ boxSizing: "border-box", position: "static", padding: padding.join(" ") }}
{...binder()}
{...draggableBinder()}
>
{children}
</div>
);
}, [
listContext.draggingNodeMeta,
listContext.itemSpacing,
listContext.stackedGroupIdentifier,
listContext.renderPlaceholder,
listContext.renderStackedGroup,
listContext.stackedGroupIdentifier,
listContext.itemSpacing,
listContext.direction,
groupContext.identifier,
props.className,
props.identifier,
props.children,
props.index,
Expand All @@ -296,7 +371,7 @@ export const Item = <T extends ItemIdentifier>(props: Props<T>) => {
if (!isGroup) return contentElement;

return (
<GroupContext.Provider value={{ identifier: props.identifier, ancestorIdentifiers, hasNoItems }}>
<GroupContext.Provider value={{ identifier: props.identifier, ancestorIdentifiers, childIdentifiersRef }}>
{contentElement}
</GroupContext.Provider>
);
Expand Down
8 changes: 7 additions & 1 deletion src/item/body.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
export const setBodyStyle = (bodyElement: HTMLElement) => {
export const setBodyStyle = (bodyElement: HTMLElement, draggingCusrsorStyle: string | undefined) => {
// Disables to select elements in entire page.
bodyElement.style.userSelect = "none";

// Applies a cursor style when dragging.
if (draggingCusrsorStyle != undefined) bodyElement.style.cursor = draggingCusrsorStyle;
};

export const clearBodyStyle = (bodyElement: HTMLElement) => {
// Enables to select elements in entire page.
bodyElement.style.removeProperty("user-select");

// Resets a cursor style when dragging.
bodyElement.style.removeProperty("cursor");
};
12 changes: 8 additions & 4 deletions src/item/drop-lines.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
import { DropLineDirection, getDropLinePosition, ItemIdentifier, NodeMeta } from "../shared";
import { Direction, DropLineDirection, getDropLinePosition, ItemIdentifier, NodeMeta } from "../shared";

export const setDropLineElementStyle = <T extends ItemIdentifier>(
dropLineElement: HTMLElement | undefined,
itemSpacing: number,
absoluteXY: [number, number],
nodeMeta: NodeMeta<T>,
direction: Direction,
) => {
if (dropLineElement == undefined) return;

const dropLinePosition = getDropLinePosition(absoluteXY, nodeMeta, itemSpacing);
const dropLinePosition = getDropLinePosition(absoluteXY, nodeMeta, direction);
dropLineElement.style.top = `${dropLinePosition.top}px`;
dropLineElement.style.left = `${dropLinePosition.left}px`;
dropLineElement.style.width = `${nodeMeta.width}px`;

if (direction === "vertical") dropLineElement.style.width = `${nodeMeta.width}px`;
if (direction === "horizontal") dropLineElement.style.height = `${nodeMeta.height}px`;
};

export const getDropLinePositionItemIndex = <T extends ItemIdentifier>(
Expand All @@ -24,6 +26,8 @@ export const getDropLinePositionItemIndex = <T extends ItemIdentifier>(
let nextIndex = draggingItemIndex;
if (dropLineDirection === "TOP") nextIndex = hoveredItemIndex;
if (dropLineDirection === "BOTTOM") nextIndex = hoveredItemIndex + 1;
if (dropLineDirection === "LEFT") nextIndex = hoveredItemIndex;
if (dropLineDirection === "RIGHT") nextIndex = hoveredItemIndex + 1;

const isInSameGroup = draggingItemGroupIdentifier === hoveredItemGroupIdentifier;
if (isInSameGroup && draggingItemIndex < nextIndex) nextIndex -= 1;
Expand Down
22 changes: 17 additions & 5 deletions src/item/ghosts.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,23 @@
export const initializeGhostElementStyle = (itemElement: HTMLElement, ghostWrapperElement: HTMLElement | undefined) => {
import { Direction } from "../shared";

export const initializeGhostElementStyle = (
itemElement: HTMLElement,
ghostWrapperElement: HTMLElement | undefined,
itemSpacing: number,
direction: Direction,
) => {
if (ghostWrapperElement == undefined) return;

const elementRect = itemElement.getBoundingClientRect();
ghostWrapperElement.style.top = `${elementRect.top}px`;
ghostWrapperElement.style.left = `${elementRect.left}px`;
ghostWrapperElement.style.width = `${elementRect.width}px`;
ghostWrapperElement.style.height = `${elementRect.height}px`;
const top = direction === "vertical" ? elementRect.top + itemSpacing / 2 : elementRect.top;
const left = direction === "horizontal" ? elementRect.left + itemSpacing / 2 : elementRect.left;
const width = direction === "horizontal" ? elementRect.width - itemSpacing : elementRect.width;
const height = direction === "vertical" ? elementRect.height - itemSpacing : elementRect.height;

ghostWrapperElement.style.top = `${top}px`;
ghostWrapperElement.style.left = `${left}px`;
ghostWrapperElement.style.width = `${width}px`;
ghostWrapperElement.style.height = `${height}px`;
};

export const moveGhostElement = (ghostWrapperElement: HTMLElement | undefined, movementXY: [number, number]) => {
Expand Down
1 change: 1 addition & 0 deletions src/item/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ export { setBodyStyle, clearBodyStyle } from "./body";
export { setDropLineElementStyle, getDropLinePositionItemIndex } from "./drop-lines";
export { initializeGhostElementStyle, moveGhostElement, clearGhostElementStyle } from "./ghosts";
export { checkIsAncestorItem } from "./move-events";
export { getPlaceholderElementStyle, getStackedGroupElementStyle } from "./renderers";
Loading

0 comments on commit a12cadd

Please sign in to comment.