-
Notifications
You must be signed in to change notification settings - Fork 2
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
feat: [SC-43819] draggable table rows #987
Changes from 19 commits
e3d89d9
797ab1f
19f0a48
493afc1
8f5e404
c64504a
5ce7af3
b472550
6b24237
b3fdc89
cd5d4cc
ff644f3
008cc16
35763c1
3b2ac9c
7a8fbb4
5048376
abc6e72
567637b
fd3b607
98f18d5
cfc3672
83efc86
2f42520
92c5498
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,6 +27,7 @@ import { | |
simpleHeader, | ||
SimpleHeaderAndData, | ||
useGridTableApi, | ||
insertAtIndex, | ||
} from "src/components/index"; | ||
import { Css, Palette } from "src/Css"; | ||
import { useComputed } from "src/hooks"; | ||
|
@@ -826,7 +827,7 @@ export function CustomEmptyCell() { | |
); | ||
} | ||
|
||
function makeNestedRows(repeat: number = 1): GridDataRow<NestedRow>[] { | ||
function makeNestedRows(repeat: number = 1, draggable: boolean = false): GridDataRow<NestedRow>[] { | ||
let parentId = 0; | ||
return zeroTo(repeat).flatMap((i) => { | ||
// Make three unique parent ids for this iteration | ||
|
@@ -837,36 +838,39 @@ function makeNestedRows(repeat: number = 1): GridDataRow<NestedRow>[] { | |
const rows: GridDataRow<NestedRow>[] = [ | ||
// a parent w/ two children, 1st child has 2 grandchild, 2nd child has 1 grandchild | ||
{ | ||
...{ kind: "parent", id: p1, data: { name: `parent ${prefix}1` } }, | ||
...{ kind: "parent", id: p1, data: { name: `parent ${prefix}1` }, draggable }, | ||
children: [ | ||
{ | ||
...{ kind: "child", id: `${p1}c1`, data: { name: `child ${prefix}p1c1` } }, | ||
...{ kind: "child", id: `${p1}c1`, data: { name: `child ${prefix}p1c1` }, draggable }, | ||
children: [ | ||
{ | ||
kind: "grandChild", | ||
id: `${p1}c1g1`, | ||
data: { name: `grandchild ${prefix}p1c1g1` + " foo".repeat(20) }, | ||
draggable, | ||
}, | ||
{ kind: "grandChild", id: `${p1}c1g2`, data: { name: `grandchild ${prefix}p1c1g2` } }, | ||
{ kind: "grandChild", id: `${p1}c1g2`, data: { name: `grandchild ${prefix}p1c1g2` }, draggable }, | ||
], | ||
}, | ||
{ | ||
...{ kind: "child", id: `${p1}c2`, data: { name: `child ${prefix}p1c2` } }, | ||
children: [{ kind: "grandChild", id: `${p1}c2g1`, data: { name: `grandchild ${prefix}p1c2g1` } }], | ||
...{ kind: "child", id: `${p1}c2`, data: { name: `child ${prefix}p1c2` }, draggable }, | ||
children: [ | ||
{ kind: "grandChild", id: `${p1}c2g1`, data: { name: `grandchild ${prefix}p1c2g1` }, draggable }, | ||
], | ||
}, | ||
// Put this "grandchild" in the 2nd level to show heterogeneous levels | ||
{ kind: "grandChild", id: `${p1}g1`, data: { name: `grandchild ${prefix}p1g1` } }, | ||
{ kind: "grandChild", id: `${p1}g1`, data: { name: `grandchild ${prefix}p1g1` }, draggable }, | ||
// Put this "kind" into the 2nd level to show it doesn't have to be a card | ||
{ kind: "add", id: `${p1}add`, pin: "last", data: {} }, | ||
{ kind: "add", id: `${p1}add`, pin: "last", data: {}, draggable }, | ||
], | ||
}, | ||
// a parent with just a child | ||
{ | ||
...{ kind: "parent", id: p2, data: { name: `parent ${prefix}2` } }, | ||
children: [{ kind: "child", id: `${p2}c1`, data: { name: `child ${prefix}p2c1` } }], | ||
...{ kind: "parent", id: p2, data: { name: `parent ${prefix}2` }, draggable }, | ||
children: [{ kind: "child", id: `${p2}c1`, data: { name: `child ${prefix}p2c1` }, draggable }], | ||
}, | ||
// a parent with no children | ||
{ kind: "parent", id: p3, data: { name: `parent ${prefix}3` } }, | ||
{ kind: "parent", id: p3, data: { name: `parent ${prefix}3` }, draggable }, | ||
]; | ||
return rows; | ||
}); | ||
|
@@ -1849,3 +1853,144 @@ export function Headers() { | |
</div> | ||
); | ||
} | ||
|
||
/** | ||
* Shows how drag & drop reordering can be implemented with GridTable drag events | ||
*/ | ||
export function DraggableRows() { | ||
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, | ||
})); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @bsholmes I think one of the things that @bdow noticed in the demo is that the Definitely fine for the POC, but I think our goal for this PR, probably even a requirement? for v1, is that as the rows are dragged up/down, React doesn't re-render them at all, and instead they're just swapped in the DOM or what not. I'm kinda surprised, because at first I thought "oh this Which I was really hoping would be all we'd need to get for the "don't re-render each row" stuff to work, but it sounds like it's not... Is that something you've observed/are thinking about/have any ideas yet as to why There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmm, that is a good call-out. I'm not sure why that would be happening here but I will look into it before I get a final PR ready. |
||
|
||
const [rows, setRows] = useState<GridDataRow<Row>[]>([simpleHeader, ...rowArray]); | ||
|
||
// also works with as="table" and as="virtual" | ||
return ( | ||
<GridTable | ||
columns={[nameColumn, actionColumn]} | ||
onRowDrop={(draggedRow, droppedRow, indexOffset) => { | ||
const tempRows = [...rows]; | ||
// remove dragged row | ||
// console.log("onRowDrop"); | ||
// console.log("rows", rows); | ||
const draggedRowIndex = tempRows.findIndex((r) => r.id === draggedRow.id); | ||
if (draggedRowIndex === -1) { | ||
console.error("draggedRowIndex is -1"); | ||
console.log("draggedRow", draggedRow); | ||
console.log("draggedRowIndex", draggedRowIndex); | ||
return; | ||
} | ||
const reorderRow = tempRows.splice(draggedRowIndex, 1)[0]; | ||
// console.log("reorder row removed"); | ||
|
||
const droppedRowIndex = tempRows.findIndex((r) => r.id === droppedRow.id); | ||
|
||
// console.log("ondrop: " + draggedRow.id + " -> " + droppedRow.id); | ||
|
||
if (draggedRowIndex === -1 || droppedRowIndex === -1) { | ||
console.error("draggedRowIndex or droppedRowIndex is -1"); | ||
console.log("draggedRow", draggedRow); | ||
console.log("draggedRowIndex", draggedRowIndex); | ||
console.log("droppedRow", droppedRow); | ||
console.log("droppedRowIndex", droppedRowIndex); | ||
} else { | ||
// console.log("insert row at index " + droppedRowIndex); | ||
// console.log("indexOffset", indexOffset); | ||
// insert it at the dropped row index | ||
setRows([...insertAtIndex(tempRows, reorderRow, droppedRowIndex + indexOffset)]); | ||
} | ||
}} | ||
rows={[...rows]} | ||
/> | ||
); | ||
} | ||
|
||
export const DraggableWithInputColumns = newStory( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. curious @bsholmes - For these last 2 columns, the dragged item does not remain in the position it is dropped. Is that expected? Am I doing something wrong like dropping things where they are not allowed to be? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I neglected to implement the drop event handler in those stories, should be there now |
||
() => { | ||
const nameCol = column<Row2>({ header: "Name", data: ({ name }) => name }); | ||
const priceCol = numericColumn<Row2>({ | ||
header: "Price", | ||
data: ({ priceInCents }) => <NumberField label="Price" value={priceInCents} onChange={noop} type="cents" />, | ||
}); | ||
const actionCol = actionColumn<Row2>({ header: "Action", data: () => <IconButton icon="check" onClick={noop} /> }); | ||
return ( | ||
<GridTable<Row2> | ||
columns={[nameCol, priceCol, actionCol]} | ||
rows={[ | ||
simpleHeader, | ||
{ | ||
kind: "data", | ||
id: "1", | ||
data: { name: "Foo", role: "Manager", date: "11/29/85", priceInCents: 113_00 }, | ||
draggable: true, | ||
}, | ||
{ | ||
kind: "data", | ||
id: "2", | ||
data: { name: "Bar", role: "VP", date: "01/29/86", priceInCents: 1_524_99 }, | ||
draggable: true, | ||
}, | ||
{ | ||
kind: "data", | ||
id: "3", | ||
data: { name: "Biz", role: "Engineer", date: "11/08/18", priceInCents: 80_65 }, | ||
draggable: true, | ||
}, | ||
{ | ||
kind: "data", | ||
id: "4", | ||
data: { name: "Baz", role: "Contractor", date: "04/21/21", priceInCents: 12_365_00 }, | ||
draggable: true, | ||
}, | ||
]} | ||
onRowDrop={() => {}} | ||
/> | ||
); | ||
}, | ||
{ decorators: [withRouter()] }, | ||
); | ||
|
||
const draggableRows = makeNestedRows(1, true); | ||
const draggableRowsWithHeader: GridDataRow<NestedRow>[] = [simpleHeader, ...draggableRows]; | ||
export function DraggableNestedRows() { | ||
const nameColumn: GridColumn<NestedRow> = { | ||
header: () => "Name", | ||
parent: (row) => ({ | ||
content: <div>{row.name}</div>, | ||
value: row.name, | ||
}), | ||
child: (row) => ({ | ||
content: <div css={Css.ml2.$}>{row.name}</div>, | ||
value: row.name, | ||
}), | ||
grandChild: (row) => ({ | ||
content: <div css={Css.ml4.$}>{row.name}</div>, | ||
value: row.name, | ||
}), | ||
add: () => "Add", | ||
}; | ||
return ( | ||
<GridTable | ||
columns={[collapseColumn<NestedRow>(), nameColumn]} | ||
{...{ rows: draggableRowsWithHeader }} | ||
sorting={{ on: "client", initial: ["c1", "ASC"] }} | ||
onRowDrop={(draggedRow, droppedRow) => {}} | ||
/> | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Praise: I popped this open in storybook and it looks slick!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems like the only thing this doesn't do is scroll the page/table when I get to the top and bottom, but otherwise it seems pretty functionally complete? Very nice! Pretty great that it's just native events too, and not bringing in a new library. Kinda assumed we'd just need that, but this looks good!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It was scrolling for me when dragging to top/bottom, thought there seems to be a small window of where you need to hold the row for it to work 🤔
praise: Looks great @bsholmes
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah the scrolling behavior isn't ideal, and is relying on the
dataTransfer.effectAllowed = "move"
stuff so isn't tweakable. We may need to implement our own scrolling behavior, at least on mobile.