Skip to content

Commit

Permalink
feat: Drag & Drop fast follow for card style rows (#991)
Browse files Browse the repository at this point in the history
Card style rows don't work with padding as spacing, and we can't use
margin and still get drop events. So we need a containing element for
padding and drag & drop events.
  • Loading branch information
bsholmes authored Jan 16, 2024
1 parent 7dde91a commit 70040be
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 50 deletions.
45 changes: 45 additions & 0 deletions src/components/Table/GridTable.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2020,3 +2020,48 @@ export function DraggableNestedRows() {
/>
);
}

export function DraggableCardRows() {
const dragColumn = dragHandleColumn<Row>({});
const nameColumn: GridColumn<Row> = {
header: "Name",
data: ({ name }) => ({ content: <div>{name}</div>, sortValue: name }),
};

const actionColumn: GridColumn<Row> = {
header: "Action",
data: () => <div>Actions</div>,
clientSideSort: false,
};

let rowArray: GridDataRow<Row>[] = new Array(26).fill(0);
rowArray = rowArray.map((elem, idx) => ({
kind: "data",
id: "" + (idx + 1),
order: idx + 1,
data: { name: "" + (idx + 1), value: idx + 1 },
draggable: true,
}));

const [rows, setRows] = useState<GridDataRow<Row>[]>([simpleHeader, ...rowArray]);

// also works with as="table" and as="virtual"
return (
<GridTable
columns={[dragColumn, nameColumn, actionColumn]}
onRowDrop={(draggedRow, droppedRow, indexOffset) => {
const tempRows = [...rows];
// remove dragged row
const draggedRowIndex = tempRows.findIndex((r) => r.id === draggedRow.id);
const reorderRow = tempRows.splice(draggedRowIndex, 1)[0];

const droppedRowIndex = tempRows.findIndex((r) => r.id === droppedRow.id);

// insert it at the dropped row index
setRows([...insertAtIndex(tempRows, reorderRow, droppedRowIndex + indexOffset)]);
}}
rows={[...rows]}
style={cardStyle}
/>
);
}
9 changes: 7 additions & 2 deletions src/components/Table/GridTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -352,7 +352,11 @@ export function GridTable<R extends Kinded, X extends Only<GridTableXss, X> = an
evt.preventDefault();

// set flags for css spacer
recursiveSetDraggedOver(rows, DraggedOver.None);
// don't set none for the row we are entering
recursiveSetDraggedOver(
rows.filter((r) => r.id !== row.id),
DraggedOver.None,
);

if (draggedRowRef.current) {
if (draggedRowRef.current.id === row.id) {
Expand All @@ -373,7 +377,8 @@ export function GridTable<R extends Kinded, X extends Only<GridTableXss, X> = an
evt.preventDefault();

if (draggedRowRef.current) {
if (draggedRowRef.current.id === row.id) {
if (draggedRowRef.current.id === row.id || !evt.currentTarget) {
tableState.maybeSetRowDraggedOver(row.id, DraggedOver.None, draggedRowRef.current);
return;
}

Expand Down
76 changes: 40 additions & 36 deletions src/components/Table/components/Row.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { observer } from "mobx-react";
import { ReactElement, useContext, useRef } from "react";
import { ReactElement, useContext, useRef, useCallback } from "react";
import {
defaultRenderFn,
headerRenderFn,
Expand Down Expand Up @@ -31,7 +31,7 @@ import {
import { Css, Palette } from "src/Css";
import { AnyObject } from "src/types";
import { isFunction } from "src/utils";
import { Icon } from "src";
import { useDebouncedCallback } from "use-debounce";

interface RowProps<R extends Kinded> {
as: RenderAs;
Expand Down Expand Up @@ -93,8 +93,13 @@ function RowImpl<R extends Kinded, S>(props: RowProps<R>): ReactElement {
const rowStyleCellCss = maybeApplyFunction(row as any, rowStyle?.cellCss);
const levelIndent = style.levels && style.levels[level]?.rowIndent;

const containerCss = {
...Css.add("transition", "padding 0.25s ease-in-out").$,
...(rs.isDraggedOver === DraggedOver.Above && Css.ptPx(25).$),
...(rs.isDraggedOver === DraggedOver.Below && Css.pbPx(25).$),
};

const rowCss = {
...Css.add("transition", "padding 0.5s ease-in-out").$,
...(!reservedRowKinds.includes(row.kind) && style.nonHeaderRowCss),
// Optionally include the row hover styles, by default they should be turned on.
...(showRowHoverColor && {
Expand All @@ -117,8 +122,6 @@ function RowImpl<R extends Kinded, S>(props: RowProps<R>): ReactElement {
[`:hover > .${revealOnRowHoverClass} > *`]: Css.visible.$,
},
...(isLastKeptRow && Css.addIn("&>*", style.keptLastRowCss).$),
...(rs.isDraggedOver === DraggedOver.Above && Css.add("paddingTop", "35px").$),
...(rs.isDraggedOver === DraggedOver.Below && Css.add("paddingBottom", "35px").$),
};

let currentColspan = 1;
Expand All @@ -131,36 +134,16 @@ function RowImpl<R extends Kinded, S>(props: RowProps<R>): ReactElement {
// used to render the whole row when dragging with the handle
const ref = useRef<HTMLTableRowElement>(null);

return (
<RowTag
css={rowCss}
{...others}
data-gridrow
{...getCount(row.id)}
// these events are necessary to get the dragged-over row for the drop event
// and spacer styling
onDrop={(evt) => onDrop?.(row, evt)}
onDragEnter={(evt) => onDragEnter?.(row, evt)}
onDragOver={(evt) => onDragOver?.(row, evt)}
ref={ref}
>
{/* {row.draggable && (
<div
draggable={row.draggable}
onDragStart={(evt) => {
// show the whole row being dragged when dragging with the handle
ref.current && evt.dataTransfer.setDragImage(ref.current, 0, 0);
return onDragStart?.(row, evt);
}}
onDragEnd={(evt) => onDragEnd?.(row, evt)}
onDrop={(evt) => onDrop?.(row, evt)}
onDragEnter={(evt) => onDragEnter?.(row, evt)}
onDragOver={(evt) => onDragOver?.(row, evt)}
css={Css.mh100.ma.$}
>
<Icon icon="drag" />
</div>
)} */}
// debounce drag over callback to avoid excessive re-renders
const dragOverCallback = useCallback(
(row: GridDataRow<R>, evt: React.DragEvent<HTMLElement>) => onDragOver?.(row, evt),
[onDragOver],
);
// when the event is not called, we still need to call preventDefault
const onDragOverDebounced = useDebouncedCallback(dragOverCallback, 100);

const RowContent = () => (
<RowTag css={rowCss} {...others} data-gridrow {...getCount(row.id)}>
{isKeptGroupRow ? (
<KeptGroupRow as={as} style={style} columnSizes={columnSizes} row={row} colSpan={columns.length} />
) : (
Expand Down Expand Up @@ -227,7 +210,7 @@ function RowImpl<R extends Kinded, S>(props: RowProps<R>): ReactElement {
onDragEnd,
onDrop,
onDragEnter,
onDragOver,
onDragOver: onDragOverDebounced,
});

// Only use the `numExpandedColumns` as the `colspan` when rendering the "Expandable Header"
Expand Down Expand Up @@ -372,6 +355,27 @@ function RowImpl<R extends Kinded, S>(props: RowProps<R>): ReactElement {
)}
</RowTag>
);

return row.draggable ? (
<div
css={containerCss}
// these events are necessary to get the dragged-over row for the drop event
// and spacer styling
onDrop={(evt) => onDrop?.(row, evt)}
onDragEnter={(evt) => onDragEnter?.(row, evt)}
onDragOver={(evt) => {
// when the event isn't called due to debounce, we still need to
// call preventDefault for the drop event to fire
evt.preventDefault();
onDragOverDebounced(row, evt);
}}
ref={ref}
>
{RowContent()}
</div>
) : (
<>{RowContent()}</>
);
}

/**
Expand Down
1 change: 1 addition & 0 deletions src/components/Table/utils/RowStates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ export class RowStates<R extends Kinded> {
}

// this allows a single-row re-render
if (rs.isDraggedOver === draggedOver) return;
rs.isDraggedOver = draggedOver;
}
}
Expand Down
10 changes: 0 additions & 10 deletions src/components/Table/utils/columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -200,32 +200,22 @@ export function dragHandleColumn<T extends Kinded>(columnDef?: Partial<GridColum
id: "beamDragHandleColumn",
clientSideSort: false,
align: "center",
// Defining `w: 40px` to accommodate for the `16px` wide checkbox and `12px` of padding on either side.
w: "40px",
wrapAction: false,
isAction: true,
expandColumns: undefined,
// Select Column should not display the select toggle for `expandableHeader` or `totals` row kinds
expandableHeader: emptyCell,
totals: emptyCell,
// Use any of the user's per-row kind methods if they have them.
...columnDef,
};

// return newMethodMissingProxy(base, (key) => {
// return (data: any, { row, level }: { row: GridDataRow<any>; level: number }) => ({
// content: <CollapseToggle row={row} compact={level > 0} />,
// });
// }) as any;

return newMethodMissingProxy(base, (key) => {
return (data: any, { row, dragData }: { row: GridDataRow<T>; dragData: DragData<T> }) => {
if (!dragData) return;
const { rowRenderRef: ref, onDragStart, onDragEnd, onDrop, onDragEnter, onDragOver } = dragData;

return {
// how do we get the callbacks and the ref here?
// inject them into the row in the Row component?
content: row.draggable ? (
<div
draggable={row.draggable}
Expand Down
4 changes: 2 additions & 2 deletions src/components/Table/utils/utils.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -273,8 +273,8 @@ export function isCursorBelowMidpoint(target: HTMLElement, clientY: number) {
const style = window.getComputedStyle(target);
const rect = target.getBoundingClientRect();

const pt = parseInt(style.getPropertyValue("padding-top")) / 2;
const pb = parseInt(style.getPropertyValue("padding-bottom"));
const pt = parseFloat(style.getPropertyValue("padding-top"));
const pb = parseFloat(style.getPropertyValue("padding-bottom"));

return clientY > rect.top + pt + (rect.height - pb) / 2;
}
Expand Down

0 comments on commit 70040be

Please sign in to comment.