From e3d89d9c4266de507f749e42e79357ffe160ca8d Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 14 Dec 2023 14:30:44 -0800 Subject: [PATCH 01/22] rough draft drag & drop --- src/components/Table/GridTable.stories.tsx | 109 +++++++++++++++++++++ src/components/Table/GridTable.tsx | 39 +++++++- src/components/Table/components/Row.tsx | 17 +++- 3 files changed, 163 insertions(+), 2 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index dcf020e31..bcab336af 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -43,6 +43,11 @@ export default { type Data = { name: string | undefined; value: number | undefined }; type Row = SimpleHeaderAndData; +// SimpleHeaderAndData & { order: number, kind: "data" | "header" | "spacer" }; +type OrderedRow = + | { kind: "header"; order: number } + | { kind: "spacer"; order: number } + | { kind: "data"; data: T; id: string; order: number }; export function ClientSideSorting() { const nameColumn: GridColumn = { @@ -1822,3 +1827,107 @@ export function Headers() { ); } + +export function DraggableRows() { + const nameColumn: GridColumn> = { + header: "Name", + data: ({ name }) => ({ content:
{name}
, sortValue: name }), + spacer: '', + }; + const valueColumn: GridColumn> = { id: "value", header: "Value", data: ({ value }) => value, spacer: '' }; + const actionColumn: GridColumn> = { header: "Action", data: () =>
Actions
, spacer: '', clientSideSort: false }; + const spacerRow: GridDataRow> = { kind: "spacer", id: "spacer", data: {name: "", value: -1}, order: -1 }; + + let rowArray: GridDataRow>[] = 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 + })); + // console.log('rowArray', rowArray); + const [rows, setRows] = useState>[]>([ + {...simpleHeader, order: 0}, + ...rowArray + ]); + + // console.log('rows', rows); + + return ( + { + evt.dataTransfer.effectAllowed = "move"; + evt.dataTransfer.dropEffect = "move"; + evt.dataTransfer.setData("text/plain", JSON.stringify({row: data.row})); + }}} + onRowDragEnd={{ data: (row, data, evt) => { + evt.dataTransfer.clearData(); + // console.log("onRowDragEnd", evt); + let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + if (spacerIndex > 0) { + rows.splice(spacerIndex, 1); + setRows([...rows]); + } + }}} + onRowDrop={{ data: (row, data, evt) => { + evt.dataTransfer.clearData(); + // console.log("onRowDrop", evt); + // console.log("row", row); + // console.log('data', evt.dataTransfer.getData("text/plain")); + try { + let draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; + + // make sure spacer is removed + let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + const spacerOrder = rows[spacerIndex].order; + if (spacerIndex > 0) { + rows.splice(spacerIndex, 1); + } + + // get rows in order (index matches order) + let orderedRows = rows.sort((a,b) => a.order - b.order); + + // remove dragged row + let draggedRow = orderedRows.splice(draggedRowData.order, 1)[0]; + + // change the dragging row's order to ceil(spacer's order) + draggedRow.order = Math.ceil(spacerOrder); + + // insert it at the index + orderedRows = [...orderedRows.slice(0, draggedRow.order), draggedRow, ...orderedRows.slice(draggedRow.order, orderedRows.length)]; + // set row order to index + orderedRows.forEach((r, idx) => r.order = idx); + setRows([...orderedRows]); + } catch {} + + }}} + onRowDragEnter={{ data: (row, data, evt) => { + let dir = evt.clientY > (evt.currentTarget.offsetTop + (0.5 * evt.currentTarget.offsetHeight)) ? 1 : -1; + + // add a spacer above the row being dragged over + let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + if (spacerIndex === -1) { + setRows([...rows, {...spacerRow, order: data.row.order + (dir * 0.1)}]); + } else { + rows[spacerIndex].order = data.row.order + (dir * 0.1); + setRows([...rows]); + } + }}} + onRowDragOver={{ data: (row, data, evt) => { + // console.log("onRowDragOver", evt); + evt.preventDefault(); + + let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + if(spacerIndex > 0) { + let dir = evt.clientY > (evt.currentTarget.offsetTop + (0.5 * evt.currentTarget.offsetHeight)) ? 1 : -1; + + rows[spacerIndex].order = data.row.order + (dir * 0.1); + setRows([...rows]); + } + }}} + rows={[...rows].sort((a,b) => a.order - b.order)} + /> + ); +} diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index e8eac1f14..67faa821c 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -90,6 +90,13 @@ export type OnRowSelect = { : (data: undefined, isSelected: boolean, opts: { row: GridRowKind; api: GridTableApi }) => void; }; +export type OnRowDragEvent = { + [K in R["kind"]]?: + DiscriminateUnion extends { data: infer D } + ? (data: D, opts: { row: GridDataRow; api: GridTableApi }, event: React.DragEvent) => void + : (data: undefined, opts: { row: GridDataRow; api: GridTableApi }, event: React.DragEvent) => void; +}; + export interface GridTableProps { id?: string; /** @@ -165,6 +172,15 @@ export interface GridTableProps { infiniteScroll?: InfiniteScroll; /** Callback for when a row is selected or unselected. */ onRowSelect?: OnRowSelect; + + /** Drag & drop Callbacks. */ + onRowDragStart?: OnRowDragEvent; + onRowDrag?: OnRowDragEvent; + onRowDragEnd?: OnRowDragEvent; + onRowDrop?: OnRowDragEvent; + onRowDragEnter?: OnRowDragEvent; + onRowDragOver?: OnRowDragEvent; + onRowDragLeave?: OnRowDragEvent; } /** @@ -209,6 +225,13 @@ export function GridTable = an visibleColumnsStorageKey, infiniteScroll, onRowSelect, + onRowDragStart, + onRowDrag, + onRowDragEnd, + onRowDrop, + onRowDragEnter, + onRowDragOver, + onRowDragLeave, } = props; const columnsWithIds = useMemo(() => assignDefaultColumnIds(_columns), [_columns]); @@ -216,7 +239,7 @@ export function GridTable = an // We only use this in as=virtual mode, but keep this here for rowLookup to use const virtuosoRef = useRef(null); // Use this ref to watch for changes in the GridTable's container and resize columns accordingly. - const resizeRef = useRef(null); + const resizeRef = useRef< HTMLDivElement>(null); const api = useMemo>( () => { @@ -293,9 +316,23 @@ export function GridTable = an // Get the flat list or rows from the header down... visibleRows.forEach((rs) => { + const dragEventHandler = (callback: OnRowDragEvent | undefined) => (evt: React.DragEvent) => { + if(rs.row.draggable && callback) { + let fn = callback[rs.row.kind]; + fn && fn(rs.row.data as any, { row: rs.row, api: rs.api}, evt); + } + }; + const row = ( { cellHighlight: boolean; omitRowHover: boolean; hasExpandableHeader: boolean; + onDragStart?: (event: React.DragEvent) => void; + onDrag?: (event: React.DragEvent) => void; + onDragEnd?: (event: React.DragEvent) => void; + onDrop?: (event: React.DragEvent) => void; + onDragEnter?: (event: React.DragEvent) => void; + onDragOver?: (event: React.DragEvent) => void; + onDragLeave?: (event: React.DragEvent) => void; } // We extract Row to its own mini-component primarily so we can React.memo'ize it. @@ -104,7 +111,13 @@ function RowImpl(props: RowProps): ReactElement { let expandColumnHidden = false; return ( - + {isKeptGroupRow ? ( ) : ( @@ -360,4 +373,6 @@ export type GridDataRow = { selectable?: false; /** Whether this row should infer its selected state based on its children's selected state */ inferSelectedState?: false; + /** Whether this row is draggable, usually to allow drag & drop reordering of rows */ + draggable?: true; } & IfAny>; From 797ab1fd141c7a05357ca133147fc6f2b6acc64f Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 14 Dec 2023 15:23:10 -0800 Subject: [PATCH 02/22] cleanup, make spacer droppable --- src/components/Table/GridTable.stories.tsx | 180 ++++++++++++--------- 1 file changed, 101 insertions(+), 79 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index bcab336af..97e1852b5 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -17,6 +17,7 @@ import { GridCellAlignment, GridColumn, GridDataRow, + GridTableApi, GridRowLookup, GridTable, Icon, @@ -43,10 +44,9 @@ export default { type Data = { name: string | undefined; value: number | undefined }; type Row = SimpleHeaderAndData; -// SimpleHeaderAndData & { order: number, kind: "data" | "header" | "spacer" }; type OrderedRow = | { kind: "header"; order: number } - | { kind: "spacer"; order: number } + | { kind: "spacer"; data: T; order: number } | { kind: "data"; data: T; id: string; order: number }; export function ClientSideSorting() { @@ -1828,15 +1828,18 @@ export function Headers() { ); } +/** + * Shows how drag & drop reordering can be implemented with GridTable drag events + */ export function DraggableRows() { const nameColumn: GridColumn> = { header: "Name", data: ({ name }) => ({ content:
{name}
, sortValue: name }), - spacer: '', + spacer: ' ', }; - const valueColumn: GridColumn> = { id: "value", header: "Value", data: ({ value }) => value, spacer: '' }; - const actionColumn: GridColumn> = { header: "Action", data: () =>
Actions
, spacer: '', clientSideSort: false }; - const spacerRow: GridDataRow> = { kind: "spacer", id: "spacer", data: {name: "", value: -1}, order: -1 }; + const orderColumn: GridColumn> = { id: "order", header: "Order", data: (data, { row }) => row.order, spacer: ' ' }; + const actionColumn: GridColumn> = { header: "Action", data: () =>
Actions
, spacer: ' ', clientSideSort: false }; + const spacerRow: GridDataRow> = { kind: "spacer", id: "spacer", data: {name: "", value: -1}, order: -1, draggable: true }; let rowArray: GridDataRow>[] = new Array(26).fill(0);; rowArray = rowArray.map((elem, idx) => ({ @@ -1846,87 +1849,106 @@ export function DraggableRows() { data: { name: "" + (idx + 1), value: (idx + 1) }, draggable: true })); - // console.log('rowArray', rowArray); + const [rows, setRows] = useState>[]>([ {...simpleHeader, order: 0}, ...rowArray ]); - // console.log('rows', rows); + type DataType = { row: GridDataRow>, api: GridTableApi> }; + type EventType = React.DragEvent; + + const onDragStart = (row: Data, data: DataType, evt: EventType) => { + // evt.preventDefault(); + evt.dataTransfer.effectAllowed = "move"; + evt.dataTransfer.dropEffect = "move"; + evt.dataTransfer.setData("text/plain", JSON.stringify({row: data.row})); + }; + + const onDragEnd = (row: Data, data: DataType, evt: EventType) => { + evt.preventDefault(); + evt.dataTransfer.clearData(); + // console.log("onRowDragEnd", evt); + let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + if (spacerIndex > 0) { + rows.splice(spacerIndex, 1); + setRows([...rows]); + } + }; + + const onDrop = (row: Data, data: DataType, evt: EventType) => { + evt.preventDefault(); + evt.dataTransfer.clearData(); + + try { + let draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; + + // make sure spacer is removed + let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + const spacerOrder = rows[spacerIndex].order; + if (spacerIndex > 0) { + rows.splice(spacerIndex, 1); + } + + // get rows in order (index matches order) + let orderedRows = rows.sort((a,b) => a.order - b.order); + + // remove dragged row + let draggedRow = orderedRows.splice(draggedRowData.order, 1)[0]; + + // change the dragging row's order to ceil(spacer's order) + draggedRow.order = Math.ceil(spacerOrder); + + // insert it at the index + orderedRows = [...orderedRows.slice(0, draggedRow.order), draggedRow, ...orderedRows.slice(draggedRow.order, orderedRows.length)]; + // set row order to index + orderedRows.forEach((r, idx) => r.order = idx); + setRows([...orderedRows]); + } catch {} + }; + + const onDragEnter = (row: Data, data: DataType, evt: EventType) => { + evt.preventDefault(); + if(data.row.id === spacerRow.id) { + return; + } + + let dir = evt.clientY > (evt.currentTarget.offsetTop + (0.5 * evt.currentTarget.clientHeight)) ? 1 : -1; + + // add a spacer above the row being dragged over + let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + if (spacerIndex === -1) { + setRows([...rows, {...spacerRow, order: data.row.order + (dir * 0.1)}]); + } else { + rows[spacerIndex].order = data.row.order + (dir * 0.1); + setRows([...rows]); + } + }; + + const onDragOver = (row: Data, data: DataType, evt: EventType) => { + evt.preventDefault(); + + if(data.row.id === spacerRow.id) { + return; + } + + let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + if(spacerIndex > 0) { + let dir = evt.clientY > (evt.currentTarget.offsetTop + (0.5 * evt.currentTarget.clientHeight)) ? 1 : -1; + + rows[spacerIndex].order = data.row.order + (dir * 0.1); + setRows([...rows]); + } + }; return ( { - evt.dataTransfer.effectAllowed = "move"; - evt.dataTransfer.dropEffect = "move"; - evt.dataTransfer.setData("text/plain", JSON.stringify({row: data.row})); - }}} - onRowDragEnd={{ data: (row, data, evt) => { - evt.dataTransfer.clearData(); - // console.log("onRowDragEnd", evt); - let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); - if (spacerIndex > 0) { - rows.splice(spacerIndex, 1); - setRows([...rows]); - } - }}} - onRowDrop={{ data: (row, data, evt) => { - evt.dataTransfer.clearData(); - // console.log("onRowDrop", evt); - // console.log("row", row); - // console.log('data', evt.dataTransfer.getData("text/plain")); - try { - let draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; - - // make sure spacer is removed - let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); - const spacerOrder = rows[spacerIndex].order; - if (spacerIndex > 0) { - rows.splice(spacerIndex, 1); - } - - // get rows in order (index matches order) - let orderedRows = rows.sort((a,b) => a.order - b.order); - - // remove dragged row - let draggedRow = orderedRows.splice(draggedRowData.order, 1)[0]; - - // change the dragging row's order to ceil(spacer's order) - draggedRow.order = Math.ceil(spacerOrder); - - // insert it at the index - orderedRows = [...orderedRows.slice(0, draggedRow.order), draggedRow, ...orderedRows.slice(draggedRow.order, orderedRows.length)]; - // set row order to index - orderedRows.forEach((r, idx) => r.order = idx); - setRows([...orderedRows]); - } catch {} - - }}} - onRowDragEnter={{ data: (row, data, evt) => { - let dir = evt.clientY > (evt.currentTarget.offsetTop + (0.5 * evt.currentTarget.offsetHeight)) ? 1 : -1; - - // add a spacer above the row being dragged over - let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); - if (spacerIndex === -1) { - setRows([...rows, {...spacerRow, order: data.row.order + (dir * 0.1)}]); - } else { - rows[spacerIndex].order = data.row.order + (dir * 0.1); - setRows([...rows]); - } - }}} - onRowDragOver={{ data: (row, data, evt) => { - // console.log("onRowDragOver", evt); - evt.preventDefault(); - - let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); - if(spacerIndex > 0) { - let dir = evt.clientY > (evt.currentTarget.offsetTop + (0.5 * evt.currentTarget.offsetHeight)) ? 1 : -1; - - rows[spacerIndex].order = data.row.order + (dir * 0.1); - setRows([...rows]); - } - }}} + columns={[nameColumn, orderColumn, actionColumn]} + onRowDragStart={{ data: onDragStart, spacer: onDragStart }} + onRowDragEnd={{ data: onDragEnd, spacer: onDragEnd }} + onRowDrop={{ data: onDrop, spacer: onDrop }} + onRowDragEnter={{ data: onDragEnter, spacer: onDragEnter }} + onRowDragOver={{ data: onDragOver, spacer: onDragOver }} rows={[...rows].sort((a,b) => a.order - b.order)} /> ); From 493afc149fd947b93ba17ba69b36769acf7dfb6b Mon Sep 17 00:00:00 2001 From: bsholmes Date: Fri, 15 Dec 2023 09:15:00 -0800 Subject: [PATCH 03/22] linting --- src/components/Table/GridTable.stories.tsx | 87 +++++++++++++--------- src/components/Table/GridTable.tsx | 30 +++++--- src/components/Table/components/Row.tsx | 8 +- 3 files changed, 72 insertions(+), 53 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index d8064bf66..a7b53079b 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -44,7 +44,7 @@ export default { type Data = { name: string | undefined; value: number | undefined }; type Row = SimpleHeaderAndData; -type OrderedRow = +type OrderedRow = | { kind: "header"; order: number } | { kind: "spacer"; data: T; order: number } | { kind: "data"; data: T; id: string; order: number }; @@ -1862,41 +1862,54 @@ export function DraggableRows() { const nameColumn: GridColumn> = { header: "Name", data: ({ name }) => ({ content:
{name}
, sortValue: name }), - spacer: ' ', + spacer: " ", + }; + const orderColumn: GridColumn> = { + id: "order", + header: "Order", + data: (data, { row }) => row.order, + spacer: " ", + }; + const actionColumn: GridColumn> = { + header: "Action", + data: () =>
Actions
, + spacer: " ", + clientSideSort: false, + }; + const spacerRow: GridDataRow> = { + kind: "spacer", + id: "spacer", + data: { name: "", value: -1 }, + order: -1, + draggable: true, }; - const orderColumn: GridColumn> = { id: "order", header: "Order", data: (data, { row }) => row.order, spacer: ' ' }; - const actionColumn: GridColumn> = { header: "Action", data: () =>
Actions
, spacer: ' ', clientSideSort: false }; - const spacerRow: GridDataRow> = { kind: "spacer", id: "spacer", data: {name: "", value: -1}, order: -1, draggable: true }; - let rowArray: GridDataRow>[] = new Array(26).fill(0);; + let rowArray: GridDataRow>[] = 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 + order: idx + 1, + data: { name: "" + (idx + 1), value: idx + 1 }, + draggable: true, })); - const [rows, setRows] = useState>[]>([ - {...simpleHeader, order: 0}, - ...rowArray - ]); + const [rows, setRows] = useState>[]>([{ ...simpleHeader, order: 0 }, ...rowArray]); - type DataType = { row: GridDataRow>, api: GridTableApi> }; + type DataType = { row: GridDataRow>; api: GridTableApi> }; type EventType = React.DragEvent; const onDragStart = (row: Data, data: DataType, evt: EventType) => { // evt.preventDefault(); evt.dataTransfer.effectAllowed = "move"; evt.dataTransfer.dropEffect = "move"; - evt.dataTransfer.setData("text/plain", JSON.stringify({row: data.row})); + evt.dataTransfer.setData("text/plain", JSON.stringify({ row: data.row })); }; const onDragEnd = (row: Data, data: DataType, evt: EventType) => { evt.preventDefault(); evt.dataTransfer.clearData(); // console.log("onRowDragEnd", evt); - let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); if (spacerIndex > 0) { rows.splice(spacerIndex, 1); setRows([...rows]); @@ -1908,46 +1921,50 @@ export function DraggableRows() { evt.dataTransfer.clearData(); try { - let draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; + const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; // make sure spacer is removed - let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); const spacerOrder = rows[spacerIndex].order; if (spacerIndex > 0) { rows.splice(spacerIndex, 1); } - + // get rows in order (index matches order) - let orderedRows = rows.sort((a,b) => a.order - b.order); + let orderedRows = rows.sort((a, b) => a.order - b.order); // remove dragged row - let draggedRow = orderedRows.splice(draggedRowData.order, 1)[0]; + const draggedRow = orderedRows.splice(draggedRowData.order, 1)[0]; // change the dragging row's order to ceil(spacer's order) draggedRow.order = Math.ceil(spacerOrder); // insert it at the index - orderedRows = [...orderedRows.slice(0, draggedRow.order), draggedRow, ...orderedRows.slice(draggedRow.order, orderedRows.length)]; + orderedRows = [ + ...orderedRows.slice(0, draggedRow.order), + draggedRow, + ...orderedRows.slice(draggedRow.order, orderedRows.length), + ]; // set row order to index - orderedRows.forEach((r, idx) => r.order = idx); + orderedRows.forEach((r, idx) => (r.order = idx)); setRows([...orderedRows]); } catch {} }; const onDragEnter = (row: Data, data: DataType, evt: EventType) => { evt.preventDefault(); - if(data.row.id === spacerRow.id) { + if (data.row.id === spacerRow.id) { return; } - let dir = evt.clientY > (evt.currentTarget.offsetTop + (0.5 * evt.currentTarget.clientHeight)) ? 1 : -1; + const dir = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : -1; // add a spacer above the row being dragged over - let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); + const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); if (spacerIndex === -1) { - setRows([...rows, {...spacerRow, order: data.row.order + (dir * 0.1)}]); + setRows([...rows, { ...spacerRow, order: data.row.order + dir * 0.1 }]); } else { - rows[spacerIndex].order = data.row.order + (dir * 0.1); + rows[spacerIndex].order = data.row.order + dir * 0.1; setRows([...rows]); } }; @@ -1955,15 +1972,15 @@ export function DraggableRows() { const onDragOver = (row: Data, data: DataType, evt: EventType) => { evt.preventDefault(); - if(data.row.id === spacerRow.id) { + if (data.row.id === spacerRow.id) { return; } - - let spacerIndex = rows.findIndex(r => r.id === spacerRow.id); - if(spacerIndex > 0) { - let dir = evt.clientY > (evt.currentTarget.offsetTop + (0.5 * evt.currentTarget.clientHeight)) ? 1 : -1; - rows[spacerIndex].order = data.row.order + (dir * 0.1); + const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); + if (spacerIndex > 0) { + const dir = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : -1; + + rows[spacerIndex].order = data.row.order + dir * 0.1; setRows([...rows]); } }; @@ -1976,7 +1993,7 @@ export function DraggableRows() { onRowDrop={{ data: onDrop, spacer: onDrop }} onRowDragEnter={{ data: onDragEnter, spacer: onDragEnter }} onRowDragOver={{ data: onDragOver, spacer: onDragOver }} - rows={[...rows].sort((a,b) => a.order - b.order)} + rows={[...rows].sort((a, b) => a.order - b.order)} /> ); } diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index 1a946980a..b05c17b67 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -91,10 +91,17 @@ export type OnRowSelect = { }; export type OnRowDragEvent = { - [K in R["kind"]]?: - DiscriminateUnion extends { data: infer D } - ? (data: D, opts: { row: GridDataRow; api: GridTableApi }, event: React.DragEvent) => void - : (data: undefined, opts: { row: GridDataRow; api: GridTableApi }, event: React.DragEvent) => void; + [K in R["kind"]]?: DiscriminateUnion extends { data: infer D } + ? ( + data: D, + opts: { row: GridDataRow; api: GridTableApi }, + event: React.DragEvent, + ) => void + : ( + data: undefined, + opts: { row: GridDataRow; api: GridTableApi }, + event: React.DragEvent, + ) => void; }; export interface GridTableProps { @@ -239,7 +246,7 @@ export function GridTable = an // We only use this in as=virtual mode, but keep this here for rowLookup to use const virtuosoRef = useRef(null); // Use this ref to watch for changes in the GridTable's container and resize columns accordingly. - const resizeRef = useRef< HTMLDivElement>(null); + const resizeRef = useRef(null); const api = useMemo>( () => { @@ -316,12 +323,13 @@ export function GridTable = an // Get the flat list or rows from the header down... visibleRows.forEach((rs) => { - const dragEventHandler = (callback: OnRowDragEvent | undefined) => (evt: React.DragEvent) => { - if(rs.row.draggable && callback) { - let fn = callback[rs.row.kind]; - fn && fn(rs.row.data as any, { row: rs.row, api: rs.api}, evt); - } - }; + const dragEventHandler = + (callback: OnRowDragEvent | undefined) => (evt: React.DragEvent) => { + if (rs.row.draggable && callback) { + const fn = callback[rs.row.kind]; + fn && fn(rs.row.data as any, { row: rs.row, api: rs.api }, evt); + } + }; const row = ( (props: RowProps): ReactElement { let expandColumnHidden = false; return ( - + {isKeptGroupRow ? ( ) : ( From 8f5e404628ecd4bf7329c03447b39786f0b8d9d8 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 21 Dec 2023 10:16:03 -0800 Subject: [PATCH 04/22] WIP event refactor --- src/components/Table/DragOrderedTable.tsx | 128 ++++++++++++++++ src/components/Table/GridTable.stories.tsx | 130 ++++------------ src/components/Table/GridTable.tsx | 163 ++++++++++++++++----- src/components/Table/components/Row.tsx | 2 +- src/components/Table/types.ts | 1 + src/components/Table/utils/utils.tsx | 8 + 6 files changed, 290 insertions(+), 142 deletions(-) create mode 100644 src/components/Table/DragOrderedTable.tsx diff --git a/src/components/Table/DragOrderedTable.tsx b/src/components/Table/DragOrderedTable.tsx new file mode 100644 index 000000000..045827ee2 --- /dev/null +++ b/src/components/Table/DragOrderedTable.tsx @@ -0,0 +1,128 @@ +import React from "react"; +import { Only } from "src/Css"; +import { + GridTableXss, + Ordered, +} from "src/components/Table/types"; +import { GridTable, GridTableProps } from "./GridTable"; +import { GridTableApi } from "./GridTableApi"; +import { GridDataRow } from "./components/Row"; + +// type OrderedData = Ordered & { data: infer D }; +type OrderedDataRow = GridDataRow & { order: number }; +type OrderedTableProps = GridTableProps & { rows: OrderedDataRow[] }; + +//** a specialized version of GridTable that takes ordered rows and implements default methods to drag & drop reorder rows */ +export function DragOrderedTable = any> (props: OrderedTableProps) { + const { rows: initialRows } = props; + type DataType = { row: OrderedDataRow; api: GridTableApi }; + type EventType = React.DragEvent; + const spacerRow: OrderedDataRow = { + kind: "spacer", + id: "spacer", + data: {} as R, + order: -1, + draggable: true, + }; + + const [rows, setRows] = React.useState[]>(initialRows); + + // need these in an object for each kind allowed by the R type + const onDragStart = (row: R, data: DataType, evt: EventType) => { + evt.dataTransfer.effectAllowed = "move"; + evt.dataTransfer.dropEffect = "move"; + evt.dataTransfer.setData("text/plain", JSON.stringify({ row: data.row })); + }; + + const onDragEnd = (row: R, data: DataType, evt: EventType) => { + evt.preventDefault(); + evt.dataTransfer.clearData(); + const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); + if (spacerIndex > 0) { + rows.splice(spacerIndex, 1); + setRows([...rows]); + } + }; + + const onDrop = (row: R, data: DataType, evt: EventType) => { + evt.preventDefault(); + evt.dataTransfer.clearData(); + + try { + const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; + + // make sure spacer is removed + const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); + const spacerOrder = rows[spacerIndex].order; + if (spacerIndex > 0) { + rows.splice(spacerIndex, 1); + } + + // get rows in order (index matches order) + let orderedRows = rows.sort((a, b) => a.order - b.order); + + // remove dragged row + const draggedRow = orderedRows.splice(draggedRowData.order, 1)[0]; + + // change the dragging row's order to ceil(spacer's order) + draggedRow.order = Math.ceil(spacerOrder); + + // insert it at the index + orderedRows = [ + ...orderedRows.slice(0, draggedRow.order), + draggedRow, + ...orderedRows.slice(draggedRow.order, orderedRows.length), + ]; + // set row order to index + orderedRows.forEach((r, idx) => (r.order = idx)); + setRows([...orderedRows]); + } catch {} + }; + + const onDragEnter = (row: R, data: DataType, evt: EventType) => { + evt.preventDefault(); + if (data.row.id === spacerRow.id) { + return; + } + + const dir = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : -1; + + // add a spacer above or below the row being dragged over + // showing where the dragged row will be when dropped + const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); + if (spacerIndex === -1) { + setRows([...rows, { ...spacerRow, order: data.row.order + dir * 0.1 }]); + } else { + rows[spacerIndex].order = data.row.order + dir * 0.1; + setRows([...rows]); + } + }; + + const onDragOver = (row: R, data: DataType, evt: EventType) => { + evt.preventDefault(); + + if (data.row.id === spacerRow.id) { + return; + } + + const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); + if (spacerIndex > 0) { + const dir = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : -1; + + rows[spacerIndex].order = data.row.order + dir * 0.1; + setRows([...rows]); + } + }; + + return ( + a.order - b.order)} + /> + ); +} \ No newline at end of file diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index a7b53079b..2c57a0ece 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -28,6 +28,7 @@ import { simpleHeader, SimpleHeaderAndData, useGridTableApi, + insertAtIndex, } from "src/components/index"; import { Css, Palette } from "src/Css"; import { useComputed } from "src/hooks"; @@ -1876,13 +1877,6 @@ export function DraggableRows() { spacer: " ", clientSideSort: false, }; - const spacerRow: GridDataRow> = { - kind: "spacer", - id: "spacer", - data: { name: "", value: -1 }, - order: -1, - draggable: true, - }; let rowArray: GridDataRow>[] = new Array(26).fill(0); rowArray = rowArray.map((elem, idx) => ({ @@ -1895,105 +1889,37 @@ export function DraggableRows() { const [rows, setRows] = useState>[]>([{ ...simpleHeader, order: 0 }, ...rowArray]); - type DataType = { row: GridDataRow>; api: GridTableApi> }; - type EventType = React.DragEvent; - - const onDragStart = (row: Data, data: DataType, evt: EventType) => { - // evt.preventDefault(); - evt.dataTransfer.effectAllowed = "move"; - evt.dataTransfer.dropEffect = "move"; - evt.dataTransfer.setData("text/plain", JSON.stringify({ row: data.row })); - }; - - const onDragEnd = (row: Data, data: DataType, evt: EventType) => { - evt.preventDefault(); - evt.dataTransfer.clearData(); - // console.log("onRowDragEnd", evt); - const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); - if (spacerIndex > 0) { - rows.splice(spacerIndex, 1); - setRows([...rows]); - } - }; - - const onDrop = (row: Data, data: DataType, evt: EventType) => { - evt.preventDefault(); - evt.dataTransfer.clearData(); - - try { - const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; - - // make sure spacer is removed - const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); - const spacerOrder = rows[spacerIndex].order; - if (spacerIndex > 0) { - rows.splice(spacerIndex, 1); - } - - // get rows in order (index matches order) - let orderedRows = rows.sort((a, b) => a.order - b.order); - - // remove dragged row - const draggedRow = orderedRows.splice(draggedRowData.order, 1)[0]; - - // change the dragging row's order to ceil(spacer's order) - draggedRow.order = Math.ceil(spacerOrder); - - // insert it at the index - orderedRows = [ - ...orderedRows.slice(0, draggedRow.order), - draggedRow, - ...orderedRows.slice(draggedRow.order, orderedRows.length), - ]; - // set row order to index - orderedRows.forEach((r, idx) => (r.order = idx)); - setRows([...orderedRows]); - } catch {} - }; - - const onDragEnter = (row: Data, data: DataType, evt: EventType) => { - evt.preventDefault(); - if (data.row.id === spacerRow.id) { - return; - } - - const dir = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : -1; - - // add a spacer above the row being dragged over - const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); - if (spacerIndex === -1) { - setRows([...rows, { ...spacerRow, order: data.row.order + dir * 0.1 }]); - } else { - rows[spacerIndex].order = data.row.order + dir * 0.1; - setRows([...rows]); - } - }; - - const onDragOver = (row: Data, data: DataType, evt: EventType) => { - evt.preventDefault(); - - if (data.row.id === spacerRow.id) { - return; - } - - const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); - if (spacerIndex > 0) { - const dir = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : -1; - - rows[spacerIndex].order = data.row.order + dir * 0.1; - setRows([...rows]); - } - }; + console.log('rows', rows); + // also works with as="table" and as="virtual" return ( a.order - b.order)} + // onRowDragStart={{ data: onDragStart, spacer: onDragStart }} + // onRowDragEnd={{ data: onDragEnd, spacer: onDragEnd }} + onRowDrop={(draggedRow, droppedRow, indexOffset: number) => { + console.log("on drop"); + + const draggedRowIndex = rows.findIndex((r) => r.id === draggedRow.id); + console.log('draggedRowIndex', draggedRowIndex); + if(draggedRowIndex === -1) { + console.log('draggedRow', draggedRow); + } + // remove dragged row + const reorderRow = rows.splice(draggedRowIndex, 1)[0]; + + console.log('droppedRow.id', droppedRow.id); + const droppedRowIndex = rows.findIndex((r) => r.id === droppedRow.id); + console.log('droppedRowIndex', droppedRowIndex); + + console.log('inserted rows', [...insertAtIndex(rows, reorderRow, droppedRowIndex + indexOffset)]); + + // insert it at the index + setRows([...insertAtIndex(rows, reorderRow, droppedRowIndex + indexOffset)]); + }}//{ data: onDrop, spacer: onDrop }} + // onRowDragEnter={{ data: onDragEnter, spacer: onDragEnter }} + // onRowDragOver={{ data: onDragOver, spacer: onDragOver }} + rows={[...rows]} /> ); } diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index b05c17b67..f651a072c 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -20,7 +20,7 @@ import { import { assignDefaultColumnIds } from "src/components/Table/utils/columns"; import { GridRowLookup } from "src/components/Table/utils/GridRowLookup"; import { TableStateContext } from "src/components/Table/utils/TableState"; -import { EXPANDABLE_HEADER, KEPT_GROUP, zIndices } from "src/components/Table/utils/utils"; +import { EXPANDABLE_HEADER, KEPT_GROUP, insertAtIndex, zIndices } from "src/components/Table/utils/utils"; import { Css, Only } from "src/Css"; import { useComputed } from "src/hooks"; import { useRenderCount } from "src/hooks/useRenderCount"; @@ -90,19 +90,27 @@ export type OnRowSelect = { : (data: undefined, isSelected: boolean, opts: { row: GridRowKind; api: GridTableApi }) => void; }; -export type OnRowDragEvent = { - [K in R["kind"]]?: DiscriminateUnion extends { data: infer D } - ? ( - data: D, - opts: { row: GridDataRow; api: GridTableApi }, - event: React.DragEvent, - ) => void - : ( - data: undefined, - opts: { row: GridDataRow; api: GridTableApi }, - event: React.DragEvent, - ) => void; -}; +/** Per-kind row drag events */ +// export type OnRowDragEvent = { +// [K in R["kind"]]?: DiscriminateUnion extends { data: infer D } +// ? ( +// data: D, +// opts: { row: GridDataRow; api: GridTableApi }, +// event: React.DragEvent, +// ) => void +// : ( +// data: undefined, +// opts: { row: GridDataRow; api: GridTableApi }, +// event: React.DragEvent, +// ) => void; +// }; + +type DragEventType = React.DragEvent; + +export type OnRowDragEvent = ( + draggedRow: GridDataRow, + event: DragEventType, +) => void; export interface GridTableProps { id?: string; @@ -181,13 +189,15 @@ export interface GridTableProps { onRowSelect?: OnRowSelect; /** Drag & drop Callbacks. */ - onRowDragStart?: OnRowDragEvent; - onRowDrag?: OnRowDragEvent; - onRowDragEnd?: OnRowDragEvent; - onRowDrop?: OnRowDragEvent; - onRowDragEnter?: OnRowDragEvent; - onRowDragOver?: OnRowDragEvent; - onRowDragLeave?: OnRowDragEvent; + // onRowDragStart?: OnRowDragEvent; + // onRowDrag?: OnRowDragEvent; + // onRowDragEnd?: OnRowDragEvent; + // onRowDrop?: OnRowDragEvent; + // onRowDragEnter?: OnRowDragEvent; + // onRowDragOver?: OnRowDragEvent; + // onRowDragLeave?: OnRowDragEvent; + + onRowDrop?: (draggedRow: GridDataRow, droppedRow: GridDataRow, indexOffset: number) => void } /** @@ -232,13 +242,13 @@ export function GridTable = an visibleColumnsStorageKey, infiniteScroll, onRowSelect, - onRowDragStart, - onRowDrag, - onRowDragEnd, - onRowDrop, - onRowDragEnter, - onRowDragOver, - onRowDragLeave, + // onRowDragStart, + // onRowDrag, + onRowDrop: droppedCallback, + // onRowDrop, + // onRowDragEnter, + // onRowDragOver, + // onRowDragLeave, } = props; const columnsWithIds = useMemo(() => assignDefaultColumnIds(_columns), [_columns]); @@ -320,27 +330,102 @@ export function GridTable = an const { visibleRows } = tableState; const hasExpandableHeader = visibleRows.some((rs) => rs.row.id === EXPANDABLE_HEADER); + const spacerRow: GridDataRow = { + kind: "spacer", + id: "spacer", + data: {} as R, + draggable: true, + }; // Get the flat list or rows from the header down... visibleRows.forEach((rs) => { + const dragEventHandler = - (callback: OnRowDragEvent | undefined) => (evt: React.DragEvent) => { - if (rs.row.draggable && callback) { - const fn = callback[rs.row.kind]; - fn && fn(rs.row.data as any, { row: rs.row, api: rs.api }, evt); + (callback: OnRowDragEvent | undefined) => (evt: DragEventType) => { + if (rs.row.draggable && droppedCallback && callback) { + callback(rs.row, evt); } }; + const onDragStart = (row: GridDataRow, evt: DragEventType) => { + evt.dataTransfer.effectAllowed = "move"; + evt.dataTransfer.dropEffect = "move"; + evt.dataTransfer.setData("text/plain", JSON.stringify({ row })); + }; + + const onDragEnd = (row: GridDataRow, evt: DragEventType) => { + evt.preventDefault(); + evt.dataTransfer.clearData(); + tableState.deleteRows([spacerRow.id]); + }; + + const onDrop = (row: GridDataRow & {index?: number}, evt: DragEventType) => { + evt.preventDefault(); + evt.dataTransfer.clearData(); + if(droppedCallback) { + try { + const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; + + // make sure spacer is removed + tableState.deleteRows([spacerRow.id]); + + // if row is the spacer we need to get the row at the index it was before we removed it + if (row.id === spacerRow.id && row.index !== undefined) { + row = {...rows[row.index], index: row.index}; + } + + const indexOffset = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : 0; + + droppedCallback(draggedRowData, row, indexOffset); + } catch(e: any) {console.error(e.message, e.stack);} + } + }; + + const onDragEnter = (row: GridDataRow, evt: DragEventType) => { + evt.preventDefault(); + if (row.id === spacerRow.id) { + return; + } + + const indexOffset = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : 0; + + // add a spacer above or below the row being dragged over + // showing where the dragged row will be when dropped + // since we don't have direct access to the TableState rows we're always adding to the + // prop rows which don't have a spacer, so we won't get duplicates + const rowIndex = rows.findIndex((r) => r.id === row.id); + tableState.setRows([...insertAtIndex(rows, { ...spacerRow, index: rowIndex + indexOffset }, rowIndex + indexOffset)]); + }; + + const onDragOver = (row: GridDataRow, evt: DragEventType) => { + evt.preventDefault(); + + // if (row.id === spacerRow.id) { + // return; + // } + + // const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); + // if (spacerIndex > 0) { + // const indexOffset = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : 0; + + // // remove spacer and insert at index + // rows.splice(spacerIndex, 1); + // // important to get index after splice otherwise it could be one-off + // const rowIndex = rows.findIndex((r) => r.id === row.id); + // tableState.setRows([...insertAtIndex(rows, { ...spacerRow }, rowIndex + indexOffset)]); + // } + }; + const row = ( = { /** Whether this row should infer its selected state based on its children's selected state */ inferSelectedState?: false; /** Whether this row is draggable, usually to allow drag & drop reordering of rows */ - draggable?: true; + draggable?: boolean; } & IfAny>; diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index 26b73008a..7721d7a11 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -5,6 +5,7 @@ import { GridRowApi } from "src/components/Table/GridTableApi"; import { Margin, Xss } from "src/Css"; export type Kinded = { kind: string }; +export type Ordered = { order: number } & Kinded; export type GridTableXss = Xss; export type RenderAs = "div" | "table" | "virtual"; export type Direction = "ASC" | "DESC"; diff --git a/src/components/Table/utils/utils.tsx b/src/components/Table/utils/utils.tsx index 1963f7f1a..1670507e3 100644 --- a/src/components/Table/utils/utils.tsx +++ b/src/components/Table/utils/utils.tsx @@ -254,3 +254,11 @@ export function loadArrayOrUndefined(key: string) { const ids = sessionStorage.getItem(key); return ids ? JSON.parse(ids) : undefined; } + +export function insertAtIndex (array: Array, element: T, index: number):Array { + return [ + ...array.slice(0, index), + element, + ...array.slice(index, array.length), + ]; +} From c64504abe2cd31517232faedb149e01bc8759889 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 21 Dec 2023 15:26:06 -0800 Subject: [PATCH 05/22] refactor to a single event callback, using index as order, and using css spacer --- src/components/Table/GridTable.stories.tsx | 46 ++++--------- src/components/Table/GridTable.tsx | 77 ++++------------------ src/components/Table/components/Row.tsx | 4 ++ 3 files changed, 29 insertions(+), 98 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index 2c57a0ece..9aa1d395a 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -17,7 +17,6 @@ import { GridCellAlignment, GridColumn, GridDataRow, - GridTableApi, GridRowLookup, GridTable, Icon, @@ -1860,25 +1859,18 @@ export function Headers() { * Shows how drag & drop reordering can be implemented with GridTable drag events */ export function DraggableRows() { - const nameColumn: GridColumn> = { + const nameColumn: GridColumn = { header: "Name", data: ({ name }) => ({ content:
{name}
, sortValue: name }), - spacer: " ", - }; - const orderColumn: GridColumn> = { - id: "order", - header: "Order", - data: (data, { row }) => row.order, - spacer: " ", }; - const actionColumn: GridColumn> = { + + const actionColumn: GridColumn = { header: "Action", data: () =>
Actions
, - spacer: " ", clientSideSort: false, }; - let rowArray: GridDataRow>[] = new Array(26).fill(0); + let rowArray: GridDataRow[] = new Array(26).fill(0); rowArray = rowArray.map((elem, idx) => ({ kind: "data", id: "" + (idx + 1), @@ -1887,38 +1879,22 @@ export function DraggableRows() { draggable: true, })); - const [rows, setRows] = useState>[]>([{ ...simpleHeader, order: 0 }, ...rowArray]); - - console.log('rows', rows); + const [rows, setRows] = useState[]>([simpleHeader, ...rowArray]); // also works with as="table" and as="virtual" return ( { - console.log("on drop"); - - const draggedRowIndex = rows.findIndex((r) => r.id === draggedRow.id); - console.log('draggedRowIndex', draggedRowIndex); - if(draggedRowIndex === -1) { - console.log('draggedRow', draggedRow); - } + columns={[nameColumn, actionColumn]} + onRowDrop={(draggedRow, droppedRow) => { // remove dragged row + const draggedRowIndex = rows.findIndex((r) => r.id === draggedRow.id); const reorderRow = rows.splice(draggedRowIndex, 1)[0]; - console.log('droppedRow.id', droppedRow.id); const droppedRowIndex = rows.findIndex((r) => r.id === droppedRow.id); - console.log('droppedRowIndex', droppedRowIndex); - - console.log('inserted rows', [...insertAtIndex(rows, reorderRow, droppedRowIndex + indexOffset)]); - // insert it at the index - setRows([...insertAtIndex(rows, reorderRow, droppedRowIndex + indexOffset)]); - }}//{ data: onDrop, spacer: onDrop }} - // onRowDragEnter={{ data: onDragEnter, spacer: onDragEnter }} - // onRowDragOver={{ data: onDragOver, spacer: onDragOver }} + // insert it at the dropped row index + setRows([...insertAtIndex(rows, reorderRow, droppedRowIndex)]); + }} rows={[...rows]} /> ); diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index f651a072c..a71277dd6 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -106,6 +106,7 @@ export type OnRowSelect = { // }; type DragEventType = React.DragEvent; +// type PlusIndex = GridDataRow & Partial<{index?: number}>; export type OnRowDragEvent = ( draggedRow: GridDataRow, @@ -188,16 +189,8 @@ export interface GridTableProps { /** Callback for when a row is selected or unselected. */ onRowSelect?: OnRowSelect; - /** Drag & drop Callbacks. */ - // onRowDragStart?: OnRowDragEvent; - // onRowDrag?: OnRowDragEvent; - // onRowDragEnd?: OnRowDragEvent; - // onRowDrop?: OnRowDragEvent; - // onRowDragEnter?: OnRowDragEvent; - // onRowDragOver?: OnRowDragEvent; - // onRowDragLeave?: OnRowDragEvent; - - onRowDrop?: (draggedRow: GridDataRow, droppedRow: GridDataRow, indexOffset: number) => void + /** Drag & drop Callback. */ + onRowDrop?: (draggedRow: GridDataRow, droppedRow: GridDataRow) => void } /** @@ -242,13 +235,7 @@ export function GridTable = an visibleColumnsStorageKey, infiniteScroll, onRowSelect, - // onRowDragStart, - // onRowDrag, onRowDrop: droppedCallback, - // onRowDrop, - // onRowDragEnter, - // onRowDragOver, - // onRowDragLeave, } = props; const columnsWithIds = useMemo(() => assignDefaultColumnIds(_columns), [_columns]); @@ -330,12 +317,6 @@ export function GridTable = an const { visibleRows } = tableState; const hasExpandableHeader = visibleRows.some((rs) => rs.row.id === EXPANDABLE_HEADER); - const spacerRow: GridDataRow = { - kind: "spacer", - id: "spacer", - data: {} as R, - draggable: true, - }; // Get the flat list or rows from the header down... visibleRows.forEach((rs) => { @@ -343,7 +324,7 @@ export function GridTable = an const dragEventHandler = (callback: OnRowDragEvent | undefined) => (evt: DragEventType) => { if (rs.row.draggable && droppedCallback && callback) { - callback(rs.row, evt); + callback({...rs.row}, evt); } }; @@ -356,76 +337,46 @@ export function GridTable = an const onDragEnd = (row: GridDataRow, evt: DragEventType) => { evt.preventDefault(); evt.dataTransfer.clearData(); - tableState.deleteRows([spacerRow.id]); }; - const onDrop = (row: GridDataRow & {index?: number}, evt: DragEventType) => { + const onDrop = (row: GridDataRow, evt: DragEventType) => { evt.preventDefault(); evt.dataTransfer.clearData(); if(droppedCallback) { + rows.forEach((r) => r.isDraggedOver = false); try { const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; - - // make sure spacer is removed - tableState.deleteRows([spacerRow.id]); - // if row is the spacer we need to get the row at the index it was before we removed it - if (row.id === spacerRow.id && row.index !== undefined) { - row = {...rows[row.index], index: row.index}; - } - - const indexOffset = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : 0; - - droppedCallback(draggedRowData, row, indexOffset); + droppedCallback(draggedRowData, row); } catch(e: any) {console.error(e.message, e.stack);} } }; const onDragEnter = (row: GridDataRow, evt: DragEventType) => { evt.preventDefault(); - if (row.id === spacerRow.id) { - return; - } - const indexOffset = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : 0; - - // add a spacer above or below the row being dragged over - // showing where the dragged row will be when dropped - // since we don't have direct access to the TableState rows we're always adding to the - // prop rows which don't have a spacer, so we won't get duplicates + // set flags for css spacer const rowIndex = rows.findIndex((r) => r.id === row.id); - tableState.setRows([...insertAtIndex(rows, { ...spacerRow, index: rowIndex + indexOffset }, rowIndex + indexOffset)]); + rows.forEach((r) => r.isDraggedOver = false); + rows[rowIndex].isDraggedOver = true; + + // required to re-render + tableState.setRows([...rows]); }; const onDragOver = (row: GridDataRow, evt: DragEventType) => { evt.preventDefault(); - - // if (row.id === spacerRow.id) { - // return; - // } - - // const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); - // if (spacerIndex > 0) { - // const indexOffset = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : 0; - - // // remove spacer and insert at index - // rows.splice(spacerIndex, 1); - // // important to get index after splice otherwise it could be one-off - // const rowIndex = rows.findIndex((r) => r.id === row.id); - // tableState.setRows([...insertAtIndex(rows, { ...spacerRow }, rowIndex + indexOffset)]); - // } + }; const row = ( (props: RowProps): ReactElement { const levelIndent = style.levels && style.levels[level]?.rowIndent; const rowCss = { + ...(Css.add("transition", "padding-top 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 && { @@ -110,6 +111,7 @@ function RowImpl(props: RowProps): ReactElement { [`:hover > .${revealOnRowHoverClass} > *`]: Css.visible.$, }, ...(isLastKeptRow && Css.addIn("&>*", style.keptLastRowCss).$), + ...(rs.row.isDraggedOver && Css.add("paddingTop","50px").$), }; let currentColspan = 1; @@ -381,4 +383,6 @@ export type GridDataRow = { inferSelectedState?: false; /** Whether this row is draggable, usually to allow drag & drop reordering of rows */ draggable?: boolean; + /** */ + isDraggedOver?: boolean; } & IfAny>; From b47255049a1ad89579eb5ded1f2803e65fca0bc8 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 21 Dec 2023 15:26:53 -0800 Subject: [PATCH 06/22] remove unused --- src/components/Table/DragOrderedTable.tsx | 128 ---------------------- 1 file changed, 128 deletions(-) delete mode 100644 src/components/Table/DragOrderedTable.tsx diff --git a/src/components/Table/DragOrderedTable.tsx b/src/components/Table/DragOrderedTable.tsx deleted file mode 100644 index 045827ee2..000000000 --- a/src/components/Table/DragOrderedTable.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React from "react"; -import { Only } from "src/Css"; -import { - GridTableXss, - Ordered, -} from "src/components/Table/types"; -import { GridTable, GridTableProps } from "./GridTable"; -import { GridTableApi } from "./GridTableApi"; -import { GridDataRow } from "./components/Row"; - -// type OrderedData = Ordered & { data: infer D }; -type OrderedDataRow = GridDataRow & { order: number }; -type OrderedTableProps = GridTableProps & { rows: OrderedDataRow[] }; - -//** a specialized version of GridTable that takes ordered rows and implements default methods to drag & drop reorder rows */ -export function DragOrderedTable = any> (props: OrderedTableProps) { - const { rows: initialRows } = props; - type DataType = { row: OrderedDataRow; api: GridTableApi }; - type EventType = React.DragEvent; - const spacerRow: OrderedDataRow = { - kind: "spacer", - id: "spacer", - data: {} as R, - order: -1, - draggable: true, - }; - - const [rows, setRows] = React.useState[]>(initialRows); - - // need these in an object for each kind allowed by the R type - const onDragStart = (row: R, data: DataType, evt: EventType) => { - evt.dataTransfer.effectAllowed = "move"; - evt.dataTransfer.dropEffect = "move"; - evt.dataTransfer.setData("text/plain", JSON.stringify({ row: data.row })); - }; - - const onDragEnd = (row: R, data: DataType, evt: EventType) => { - evt.preventDefault(); - evt.dataTransfer.clearData(); - const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); - if (spacerIndex > 0) { - rows.splice(spacerIndex, 1); - setRows([...rows]); - } - }; - - const onDrop = (row: R, data: DataType, evt: EventType) => { - evt.preventDefault(); - evt.dataTransfer.clearData(); - - try { - const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; - - // make sure spacer is removed - const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); - const spacerOrder = rows[spacerIndex].order; - if (spacerIndex > 0) { - rows.splice(spacerIndex, 1); - } - - // get rows in order (index matches order) - let orderedRows = rows.sort((a, b) => a.order - b.order); - - // remove dragged row - const draggedRow = orderedRows.splice(draggedRowData.order, 1)[0]; - - // change the dragging row's order to ceil(spacer's order) - draggedRow.order = Math.ceil(spacerOrder); - - // insert it at the index - orderedRows = [ - ...orderedRows.slice(0, draggedRow.order), - draggedRow, - ...orderedRows.slice(draggedRow.order, orderedRows.length), - ]; - // set row order to index - orderedRows.forEach((r, idx) => (r.order = idx)); - setRows([...orderedRows]); - } catch {} - }; - - const onDragEnter = (row: R, data: DataType, evt: EventType) => { - evt.preventDefault(); - if (data.row.id === spacerRow.id) { - return; - } - - const dir = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : -1; - - // add a spacer above or below the row being dragged over - // showing where the dragged row will be when dropped - const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); - if (spacerIndex === -1) { - setRows([...rows, { ...spacerRow, order: data.row.order + dir * 0.1 }]); - } else { - rows[spacerIndex].order = data.row.order + dir * 0.1; - setRows([...rows]); - } - }; - - const onDragOver = (row: R, data: DataType, evt: EventType) => { - evt.preventDefault(); - - if (data.row.id === spacerRow.id) { - return; - } - - const spacerIndex = rows.findIndex((r) => r.id === spacerRow.id); - if (spacerIndex > 0) { - const dir = evt.clientY > evt.currentTarget.offsetTop + 0.5 * evt.currentTarget.clientHeight ? 1 : -1; - - rows[spacerIndex].order = data.row.order + dir * 0.1; - setRows([...rows]); - } - }; - - return ( - a.order - b.order)} - /> - ); -} \ No newline at end of file From 6b2423702600696672f2d1b02ce736f2bd2f5857 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 21 Dec 2023 15:30:41 -0800 Subject: [PATCH 07/22] more cleanup and linting --- src/components/Table/GridTable.stories.tsx | 6 +--- src/components/Table/GridTable.tsx | 40 ++++++++++------------ src/components/Table/components/Row.tsx | 6 ++-- src/components/Table/types.ts | 1 - src/components/Table/utils/utils.tsx | 8 ++--- 5 files changed, 24 insertions(+), 37 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index 9aa1d395a..45c3eee3f 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -44,10 +44,6 @@ export default { type Data = { name: string | undefined; value: number | undefined }; type Row = SimpleHeaderAndData; -type OrderedRow = - | { kind: "header"; order: number } - | { kind: "spacer"; data: T; order: number } - | { kind: "data"; data: T; id: string; order: number }; export function ClientSideSorting() { const nameColumn: GridColumn = { @@ -1891,7 +1887,7 @@ export function DraggableRows() { const reorderRow = rows.splice(draggedRowIndex, 1)[0]; const droppedRowIndex = rows.findIndex((r) => r.id === droppedRow.id); - + // insert it at the dropped row index setRows([...insertAtIndex(rows, reorderRow, droppedRowIndex)]); }} diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index a71277dd6..3d2d233b4 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -108,10 +108,7 @@ export type OnRowSelect = { type DragEventType = React.DragEvent; // type PlusIndex = GridDataRow & Partial<{index?: number}>; -export type OnRowDragEvent = ( - draggedRow: GridDataRow, - event: DragEventType, -) => void; +export type OnRowDragEvent = (draggedRow: GridDataRow, event: DragEventType) => void; export interface GridTableProps { id?: string; @@ -190,7 +187,7 @@ export interface GridTableProps { onRowSelect?: OnRowSelect; /** Drag & drop Callback. */ - onRowDrop?: (draggedRow: GridDataRow, droppedRow: GridDataRow) => void + onRowDrop?: (draggedRow: GridDataRow, droppedRow: GridDataRow) => void; } /** @@ -320,53 +317,52 @@ export function GridTable = an // Get the flat list or rows from the header down... visibleRows.forEach((rs) => { - - const dragEventHandler = - (callback: OnRowDragEvent | undefined) => (evt: DragEventType) => { - if (rs.row.draggable && droppedCallback && callback) { - callback({...rs.row}, evt); - } - }; + const dragEventHandler = (callback: OnRowDragEvent | undefined) => (evt: DragEventType) => { + if (rs.row.draggable && droppedCallback && callback) { + callback({ ...rs.row }, evt); + } + }; const onDragStart = (row: GridDataRow, evt: DragEventType) => { evt.dataTransfer.effectAllowed = "move"; evt.dataTransfer.dropEffect = "move"; evt.dataTransfer.setData("text/plain", JSON.stringify({ row })); }; - + const onDragEnd = (row: GridDataRow, evt: DragEventType) => { evt.preventDefault(); evt.dataTransfer.clearData(); }; - + const onDrop = (row: GridDataRow, evt: DragEventType) => { evt.preventDefault(); evt.dataTransfer.clearData(); - if(droppedCallback) { - rows.forEach((r) => r.isDraggedOver = false); + if (droppedCallback) { + rows.forEach((r) => (r.isDraggedOver = false)); try { const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; droppedCallback(draggedRowData, row); - } catch(e: any) {console.error(e.message, e.stack);} + } catch (e: any) { + console.error(e.message, e.stack); + } } }; - + const onDragEnter = (row: GridDataRow, evt: DragEventType) => { evt.preventDefault(); - + // set flags for css spacer const rowIndex = rows.findIndex((r) => r.id === row.id); - rows.forEach((r) => r.isDraggedOver = false); + rows.forEach((r) => (r.isDraggedOver = false)); rows[rowIndex].isDraggedOver = true; // required to re-render tableState.setRows([...rows]); }; - + const onDragOver = (row: GridDataRow, evt: DragEventType) => { evt.preventDefault(); - }; const row = ( diff --git a/src/components/Table/components/Row.tsx b/src/components/Table/components/Row.tsx index 277270faf..bd68d0b70 100644 --- a/src/components/Table/components/Row.tsx +++ b/src/components/Table/components/Row.tsx @@ -88,7 +88,7 @@ function RowImpl(props: RowProps): ReactElement { const levelIndent = style.levels && style.levels[level]?.rowIndent; const rowCss = { - ...(Css.add("transition", "padding-top 0.5s ease-in-out").$), + ...Css.add("transition", "padding-top 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 && { @@ -111,7 +111,7 @@ function RowImpl(props: RowProps): ReactElement { [`:hover > .${revealOnRowHoverClass} > *`]: Css.visible.$, }, ...(isLastKeptRow && Css.addIn("&>*", style.keptLastRowCss).$), - ...(rs.row.isDraggedOver && Css.add("paddingTop","50px").$), + ...(rs.row.isDraggedOver && Css.add("paddingTop", "50px").$), }; let currentColspan = 1; @@ -383,6 +383,6 @@ export type GridDataRow = { inferSelectedState?: false; /** Whether this row is draggable, usually to allow drag & drop reordering of rows */ draggable?: boolean; - /** */ + /** Whether the user is currently dragging something over this row, used to create visual space using css */ isDraggedOver?: boolean; } & IfAny>; diff --git a/src/components/Table/types.ts b/src/components/Table/types.ts index 7721d7a11..26b73008a 100644 --- a/src/components/Table/types.ts +++ b/src/components/Table/types.ts @@ -5,7 +5,6 @@ import { GridRowApi } from "src/components/Table/GridTableApi"; import { Margin, Xss } from "src/Css"; export type Kinded = { kind: string }; -export type Ordered = { order: number } & Kinded; export type GridTableXss = Xss; export type RenderAs = "div" | "table" | "virtual"; export type Direction = "ASC" | "DESC"; diff --git a/src/components/Table/utils/utils.tsx b/src/components/Table/utils/utils.tsx index 1670507e3..22158670f 100644 --- a/src/components/Table/utils/utils.tsx +++ b/src/components/Table/utils/utils.tsx @@ -255,10 +255,6 @@ export function loadArrayOrUndefined(key: string) { return ids ? JSON.parse(ids) : undefined; } -export function insertAtIndex (array: Array, element: T, index: number):Array { - return [ - ...array.slice(0, index), - element, - ...array.slice(index, array.length), - ]; +export function insertAtIndex(array: Array, element: T, index: number): Array { + return [...array.slice(0, index), element, ...array.slice(index, array.length)]; } From b3fdc895f74b603331f8336058234eb775e46a38 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 21 Dec 2023 15:40:23 -0800 Subject: [PATCH 08/22] more cleanup --- src/components/Table/GridTable.tsx | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index 3d2d233b4..67e4693fb 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -90,23 +90,7 @@ export type OnRowSelect = { : (data: undefined, isSelected: boolean, opts: { row: GridRowKind; api: GridTableApi }) => void; }; -/** Per-kind row drag events */ -// export type OnRowDragEvent = { -// [K in R["kind"]]?: DiscriminateUnion extends { data: infer D } -// ? ( -// data: D, -// opts: { row: GridDataRow; api: GridTableApi }, -// event: React.DragEvent, -// ) => void -// : ( -// data: undefined, -// opts: { row: GridDataRow; api: GridTableApi }, -// event: React.DragEvent, -// ) => void; -// }; - type DragEventType = React.DragEvent; -// type PlusIndex = GridDataRow & Partial<{index?: number}>; export type OnRowDragEvent = (draggedRow: GridDataRow, event: DragEventType) => void; From cd5d4ccff72b470606c7fc0ceef762b53291fbf4 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 21 Dec 2023 15:45:46 -0800 Subject: [PATCH 09/22] comment --- src/components/Table/GridTable.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index 67e4693fb..ee0a47059 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -301,6 +301,7 @@ export function GridTable = an // Get the flat list or rows from the header down... visibleRows.forEach((rs) => { + // only pass through events if the row is draggable and the user has provided a callback const dragEventHandler = (callback: OnRowDragEvent | undefined) => (evt: DragEventType) => { if (rs.row.draggable && droppedCallback && callback) { callback({ ...rs.row }, evt); From ff644f3d3b4aa8237ae3dea8cce918377854d654 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 21 Dec 2023 16:11:16 -0800 Subject: [PATCH 10/22] fix memoized render when no callback is defined --- src/components/Table/GridTable.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index ee0a47059..7edca7d69 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -302,10 +302,13 @@ export function GridTable = an // Get the flat list or rows from the header down... visibleRows.forEach((rs) => { // only pass through events if the row is draggable and the user has provided a callback - const dragEventHandler = (callback: OnRowDragEvent | undefined) => (evt: DragEventType) => { - if (rs.row.draggable && droppedCallback && callback) { - callback({ ...rs.row }, evt); - } + const dragEventHandler = (callback: OnRowDragEvent | undefined) => { + return rs.row.draggable && callback ? + (evt: DragEventType) => { + if (rs.row.draggable && droppedCallback && callback) { + callback({ ...rs.row }, evt); + } + } : undefined }; const onDragStart = (row: GridDataRow, evt: DragEventType) => { From 008cc1697cae82f1ab2fdc659656422c827fb43c Mon Sep 17 00:00:00 2001 From: bsholmes Date: Thu, 21 Dec 2023 16:13:47 -0800 Subject: [PATCH 11/22] lint --- src/components/Table/GridTable.tsx | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index 7edca7d69..6c2c7068a 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -303,12 +303,13 @@ export function GridTable = an visibleRows.forEach((rs) => { // only pass through events if the row is draggable and the user has provided a callback const dragEventHandler = (callback: OnRowDragEvent | undefined) => { - return rs.row.draggable && callback ? - (evt: DragEventType) => { - if (rs.row.draggable && droppedCallback && callback) { - callback({ ...rs.row }, evt); - } - } : undefined + return rs.row.draggable && callback + ? (evt: DragEventType) => { + if (rs.row.draggable && droppedCallback && callback) { + callback({ ...rs.row }, evt); + } + } + : undefined; }; const onDragStart = (row: GridDataRow, evt: DragEventType) => { From 35763c1e1596005eb14a3a7557454833e9eb4c74 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Wed, 3 Jan 2024 16:27:55 -0800 Subject: [PATCH 12/22] fix drag events always causing rerenders --- src/components/Table/GridTable.tsx | 131 +++++++++++++---------- src/components/Table/components/Row.tsx | 35 ++++-- src/components/Table/utils/RowState.ts | 3 + src/components/Table/utils/RowStates.ts | 6 ++ src/components/Table/utils/TableState.ts | 5 + 5 files changed, 111 insertions(+), 69 deletions(-) diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index 6c2c7068a..f82a0690f 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -282,6 +282,73 @@ export function GridTable = an const expandedColumnIds: string[] = useComputed(() => tableState.expandedColumnIds, [tableState]); const columnSizes = useSetupColumnSizes(style, columns, resizeTarget ?? resizeRef, expandedColumnIds); + function onDragStart (row: GridDataRow, evt: DragEventType) { + if(!row.draggable || !droppedCallback) { + return; + } + + evt.dataTransfer.effectAllowed = "move"; + evt.dataTransfer.dropEffect = "move"; + evt.dataTransfer.setData("text/plain", JSON.stringify({ row })); + }; + + const onDragEnd = (row: GridDataRow, evt: DragEventType) => { + if(!row.draggable || !droppedCallback) { + return; + } + + evt.preventDefault(); + evt.dataTransfer.clearData(); + rows.forEach((r, idx) => { + tableState.setRowDraggedOver(idx, false); + }); + }; + + const onDrop = (row: GridDataRow, evt: DragEventType) => { + if(!row.draggable || !droppedCallback) { + return; + } + + evt.preventDefault(); + evt.dataTransfer.clearData(); + if (droppedCallback) { + rows.forEach((r, idx) => {r + tableState.setRowDraggedOver(idx, false); + }); + try { + const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; + + droppedCallback(draggedRowData, row); + } catch (e: any) { + console.error(e.message, e.stack); + } + } + }; + + const onDragEnter = (row: GridDataRow, evt: DragEventType) => { + if(!row.draggable || !droppedCallback) { + return; + } + + evt.preventDefault(); + + // set flags for css spacer + const rowIndex = rows.findIndex((r) => r.id === row.id); + rows.forEach((r, idx) => {r + tableState.setRowDraggedOver(idx, false); + }); + + tableState.setRowDraggedOver(rowIndex, true); + }; + + const onDragOver = (row: GridDataRow, evt: DragEventType) => { + if(!row.draggable || !droppedCallback) { + return; + } + + evt.preventDefault(); + }; + // Flatten, hide-if-filtered, hide-if-collapsed, and component-ize the sorted rows. const [tableHeadRows, visibleDataRows, keptSelectedRows, tooManyClientSideRows]: [ ReactElement[], @@ -301,67 +368,15 @@ export function GridTable = an // Get the flat list or rows from the header down... visibleRows.forEach((rs) => { - // only pass through events if the row is draggable and the user has provided a callback - const dragEventHandler = (callback: OnRowDragEvent | undefined) => { - return rs.row.draggable && callback - ? (evt: DragEventType) => { - if (rs.row.draggable && droppedCallback && callback) { - callback({ ...rs.row }, evt); - } - } - : undefined; - }; - - const onDragStart = (row: GridDataRow, evt: DragEventType) => { - evt.dataTransfer.effectAllowed = "move"; - evt.dataTransfer.dropEffect = "move"; - evt.dataTransfer.setData("text/plain", JSON.stringify({ row })); - }; - - const onDragEnd = (row: GridDataRow, evt: DragEventType) => { - evt.preventDefault(); - evt.dataTransfer.clearData(); - }; - - const onDrop = (row: GridDataRow, evt: DragEventType) => { - evt.preventDefault(); - evt.dataTransfer.clearData(); - if (droppedCallback) { - rows.forEach((r) => (r.isDraggedOver = false)); - try { - const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; - - droppedCallback(draggedRowData, row); - } catch (e: any) { - console.error(e.message, e.stack); - } - } - }; - - const onDragEnter = (row: GridDataRow, evt: DragEventType) => { - evt.preventDefault(); - - // set flags for css spacer - const rowIndex = rows.findIndex((r) => r.id === row.id); - rows.forEach((r) => (r.isDraggedOver = false)); - rows[rowIndex].isDraggedOver = true; - - // required to re-render - tableState.setRows([...rows]); - }; - - const onDragOver = (row: GridDataRow, evt: DragEventType) => { - evt.preventDefault(); - }; - + const row = ( { cellHighlight: boolean; omitRowHover: boolean; hasExpandableHeader: boolean; - onDragStart?: (event: React.DragEvent) => void; - onDrag?: (event: React.DragEvent) => void; - onDragEnd?: (event: React.DragEvent) => void; - onDrop?: (event: React.DragEvent) => void; - onDragEnter?: (event: React.DragEvent) => void; - onDragOver?: (event: React.DragEvent) => void; - onDragLeave?: (event: React.DragEvent) => void; + onDragStart?: (row: GridDataRow, event: React.DragEvent) => void; + onDragEnd?: (row: GridDataRow, event: React.DragEvent) => void; + onDrop?: (row: GridDataRow, event: React.DragEvent) => void; + onDragEnter?: (row: GridDataRow, event: React.DragEvent) => void; + onDragOver?: (row: GridDataRow, event: React.DragEvent) => void; + // onDrag?: (row: GridDataRow, event: React.DragEvent) => void; // currently unused + // onDragLeave?: (row: GridDataRow, event: React.DragEvent) => void; // currently unused } // We extract Row to its own mini-component primarily so we can React.memo'ize it. @@ -63,6 +63,11 @@ function RowImpl(props: RowProps): ReactElement { cellHighlight, omitRowHover, hasExpandableHeader, + onDragStart, + onDragEnd, + onDrop, + onDragEnter, + onDragOver, ...others } = props; @@ -111,7 +116,7 @@ function RowImpl(props: RowProps): ReactElement { [`:hover > .${revealOnRowHoverClass} > *`]: Css.visible.$, }, ...(isLastKeptRow && Css.addIn("&>*", style.keptLastRowCss).$), - ...(rs.row.isDraggedOver && Css.add("paddingTop", "50px").$), + ...(rs.isDraggedOver && Css.add("paddingTop", "50px").$), }; let currentColspan = 1; @@ -122,7 +127,17 @@ function RowImpl(props: RowProps): ReactElement { let expandColumnHidden = false; return ( - + onDragStart?.(row, evt)} + onDragEnd={(evt) => onDragEnd?.(row, evt)} + onDrop={(evt) => onDrop?.(row, evt)} + onDragEnter={(evt) => onDragEnter?.(row, evt)} + onDragOver={(evt) => onDragOver?.(row, evt)} + > {isKeptGroupRow ? ( ) : ( @@ -383,6 +398,4 @@ export type GridDataRow = { inferSelectedState?: false; /** Whether this row is draggable, usually to allow drag & drop reordering of rows */ draggable?: boolean; - /** Whether the user is currently dragging something over this row, used to create visual space using css */ - isDraggedOver?: boolean; } & IfAny>; diff --git a/src/components/Table/utils/RowState.ts b/src/components/Table/utils/RowState.ts index bd93060bf..2849421ca 100644 --- a/src/components/Table/utils/RowState.ts +++ b/src/components/Table/utils/RowState.ts @@ -23,6 +23,8 @@ export class RowState { selected = false; /** Whether we are collapsed. */ collapsed = false; + /** Whether we are dragged over. */ + isDraggedOver = false; /** * Whether our `row` had been in `props.rows`, but then removed _while being * selected_, i.e. potentially by server-side filters. @@ -59,6 +61,7 @@ export class RowState { _row: false, _data: observable.ref, isCalculatingDirectMatch: false, + isDraggedOver: observable.ref, // allows the table to re-render only this row when the dragged over state changes } as any, { name: `RowState@${row.id}` }, ); diff --git a/src/components/Table/utils/RowStates.ts b/src/components/Table/utils/RowStates.ts index e6de90ac1..85d8f375e 100644 --- a/src/components/Table/utils/RowStates.ts +++ b/src/components/Table/utils/RowStates.ts @@ -177,6 +177,12 @@ export class RowStates { rs.children = []; return rs; } + + setRowDraggedOver(id: string, draggedOver: boolean): void { + const rs = this.get(id); + // this allows a single-row re-render + rs.isDraggedOver = draggedOver; + } } const missingHeader = { kind: "header" as const, id: "header", data: "MISSING" }; diff --git a/src/components/Table/utils/TableState.ts b/src/components/Table/utils/TableState.ts index f43db8fb5..7c9c8b29e 100644 --- a/src/components/Table/utils/TableState.ts +++ b/src/components/Table/utils/TableState.ts @@ -252,6 +252,11 @@ export class TableState { this.rows = this.rows.filter((row) => !ids.includes(row.id)); this.rowStates.delete(ids); } + + setRowDraggedOver(index: number, draggedOver: boolean): void { + // if we do rowStates.setRows here all of the rows will re-render + this.rowStates.setRowDraggedOver(this.rows[index].id, draggedOver); + } } /** Provides a context for rows to access their table's `TableState`. */ From 3b2ac9cf0aa3e93a6d5fa47e5485bbeeb41d6213 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Fri, 5 Jan 2024 13:43:00 -0800 Subject: [PATCH 13/22] drag handle, index offset --- src/components/Table/GridTable.stories.tsx | 136 ++++++++++++++++++--- src/components/Table/GridTable.tsx | 45 +++++-- src/components/Table/components/Row.tsx | 53 +++++--- src/components/Table/utils/RowState.ts | 8 +- src/components/Table/utils/RowStates.ts | 4 +- src/components/Table/utils/TableState.ts | 8 +- 6 files changed, 204 insertions(+), 50 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index 45c3eee3f..7ef90eb90 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -827,7 +827,7 @@ export function CustomEmptyCell() { ); } -function makeNestedRows(repeat: number = 1): GridDataRow[] { +function makeNestedRows(repeat: number = 1, draggable: boolean = false): GridDataRow[] { let parentId = 0; return zeroTo(repeat).flatMap((i) => { // Make three unique parent ids for this iteration @@ -838,36 +838,37 @@ function makeNestedRows(repeat: number = 1): GridDataRow[] { const rows: GridDataRow[] = [ // 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; }); @@ -1881,17 +1882,114 @@ export function DraggableRows() { return ( { + onRowDrop={(draggedRow, droppedRow, indexOffset) => { + const tempRows = [...rows]; // remove dragged row - const draggedRowIndex = rows.findIndex((r) => r.id === draggedRow.id); - const reorderRow = rows.splice(draggedRowIndex, 1)[0]; - - const droppedRowIndex = rows.findIndex((r) => r.id === droppedRow.id); - - // insert it at the dropped row index - setRows([...insertAtIndex(rows, reorderRow, droppedRowIndex)]); + // 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( + () => { + const nameCol = column({ header: "Name", data: ({ name }) => name }); + const priceCol = numericColumn({ + header: "Price", + data: ({ priceInCents }) => , + }); + const actionCol = actionColumn({ header: "Action", data: () => }); + return ( + + 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[] = [simpleHeader, ...draggableRows]; +export function DraggableNestedRows() { + const nameColumn: GridColumn = { + header: () => "Name", + parent: (row) => ({ + content:
{row.name}
, + value: row.name, + }), + child: (row) => ({ + content:
{row.name}
, + value: row.name, + }), + grandChild: (row) => ({ + content:
{row.name}
, + value: row.name, + }), + add: () => "Add", + }; + return ( + (), nameColumn]} + {...{ rows: draggableRowsWithHeader }} + sorting={{ on: "client", initial: ["c1", "ASC"] }} + onRowDrop={(draggedRow, droppedRow) => {}} + /> + ); +} diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index f82a0690f..ae5cbad8d 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -26,6 +26,7 @@ import { useComputed } from "src/hooks"; import { useRenderCount } from "src/hooks/useRenderCount"; import { isPromise } from "src/utils"; import { GridDataRow, Row } from "./components/Row"; +import { DraggedOver } from "./utils/RowState"; let runningInJest = false; @@ -90,7 +91,7 @@ export type OnRowSelect = { : (data: undefined, isSelected: boolean, opts: { row: GridRowKind; api: GridTableApi }) => void; }; -type DragEventType = React.DragEvent; +type DragEventType = React.DragEvent; export type OnRowDragEvent = (draggedRow: GridDataRow, event: DragEventType) => void; @@ -171,7 +172,7 @@ export interface GridTableProps { onRowSelect?: OnRowSelect; /** Drag & drop Callback. */ - onRowDrop?: (draggedRow: GridDataRow, droppedRow: GridDataRow) => void; + onRowDrop?: (draggedRow: GridDataRow, droppedRow: GridDataRow, indexOffset: number) => void; } /** @@ -282,6 +283,16 @@ export function GridTable = an const expandedColumnIds: string[] = useComputed(() => tableState.expandedColumnIds, [tableState]); const columnSizes = useSetupColumnSizes(style, columns, resizeTarget ?? resizeRef, expandedColumnIds); + 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')); + + return clientY > rect.top + pt + ((rect.height - pb) / 2); + } + function onDragStart (row: GridDataRow, evt: DragEventType) { if(!row.draggable || !droppedCallback) { return; @@ -299,8 +310,8 @@ export function GridTable = an evt.preventDefault(); evt.dataTransfer.clearData(); - rows.forEach((r, idx) => { - tableState.setRowDraggedOver(idx, false); + rows.forEach((r) => { + tableState.setRowDraggedOver(r.id, DraggedOver.None); }); }; @@ -312,13 +323,19 @@ export function GridTable = an evt.preventDefault(); evt.dataTransfer.clearData(); if (droppedCallback) { - rows.forEach((r, idx) => {r - tableState.setRowDraggedOver(idx, false); + rows.forEach((r) => {r + tableState.setRowDraggedOver(r.id, DraggedOver.None); }); try { const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; - droppedCallback(draggedRowData, row); + if(draggedRowData.id === row.id) { + return; + } + + const isBelow = isCursorBelowMidpoint(evt.currentTarget, evt.clientY); + + droppedCallback(draggedRowData, row, isBelow ? 1 : 0); } catch (e: any) { console.error(e.message, e.stack); } @@ -333,12 +350,14 @@ export function GridTable = an evt.preventDefault(); // set flags for css spacer - const rowIndex = rows.findIndex((r) => r.id === row.id); - rows.forEach((r, idx) => {r - tableState.setRowDraggedOver(idx, false); + rows.forEach((r) => {r + tableState.setRowDraggedOver(r.id, DraggedOver.None); }); - tableState.setRowDraggedOver(rowIndex, true); + // determine above or below + const isBelow = isCursorBelowMidpoint(evt.currentTarget, evt.clientY); + + tableState.setRowDraggedOver(row.id, isBelow ? DraggedOver.Below : DraggedOver.Above); }; const onDragOver = (row: GridDataRow, evt: DragEventType) => { @@ -347,6 +366,10 @@ export function GridTable = an } evt.preventDefault(); + + const isBelow = isCursorBelowMidpoint(evt.currentTarget, evt.clientY); + + tableState.setRowDraggedOver(row.id, isBelow ? DraggedOver.Below : DraggedOver.Above); }; // Flatten, hide-if-filtered, hide-if-collapsed, and component-ize the sorted rows. diff --git a/src/components/Table/components/Row.tsx b/src/components/Table/components/Row.tsx index 1ead7e8e7..b9551054e 100644 --- a/src/components/Table/components/Row.tsx +++ b/src/components/Table/components/Row.tsx @@ -1,5 +1,5 @@ import { observer } from "mobx-react"; -import { ReactElement, useContext } from "react"; +import { ReactElement, useContext, useRef } from "react"; import { defaultRenderFn, headerRenderFn, @@ -10,7 +10,7 @@ import { import { KeptGroupRow } from "src/components/Table/components/KeptGroupRow"; import { GridStyle, RowStyles } from "src/components/Table/TableStyles"; import { DiscriminateUnion, IfAny, Kinded, Pin, RenderAs } from "src/components/Table/types"; -import { RowState } from "src/components/Table/utils/RowState"; +import { DraggedOver, RowState } from "src/components/Table/utils/RowState"; import { ensureClientSideSortValueIsSortable } from "src/components/Table/utils/sortRows"; import { TableStateContext } from "src/components/Table/utils/TableState"; import { @@ -29,8 +29,10 @@ import { zIndices, } from "src/components/Table/utils/utils"; import { Css, Palette } from "src/Css"; +import { useBreakpoint } from "src/hooks"; import { AnyObject } from "src/types"; import { isFunction } from "src/utils"; +import { Icon } from "src"; interface RowProps { as: RenderAs; @@ -42,13 +44,13 @@ interface RowProps { cellHighlight: boolean; omitRowHover: boolean; hasExpandableHeader: boolean; - onDragStart?: (row: GridDataRow, event: React.DragEvent) => void; - onDragEnd?: (row: GridDataRow, event: React.DragEvent) => void; - onDrop?: (row: GridDataRow, event: React.DragEvent) => void; - onDragEnter?: (row: GridDataRow, event: React.DragEvent) => void; - onDragOver?: (row: GridDataRow, event: React.DragEvent) => void; - // onDrag?: (row: GridDataRow, event: React.DragEvent) => void; // currently unused - // onDragLeave?: (row: GridDataRow, event: React.DragEvent) => void; // currently unused + onDragStart?: (row: GridDataRow, event: React.DragEvent) => void; + onDragEnd?: (row: GridDataRow, event: React.DragEvent) => void; + onDrop?: (row: GridDataRow, event: React.DragEvent) => void; + onDragEnter?: (row: GridDataRow, event: React.DragEvent) => void; + onDragOver?: (row: GridDataRow, event: React.DragEvent) => void; + // onDrag?: (row: GridDataRow, event: React.DragEvent) => void; // currently unused + // onDragLeave?: (row: GridDataRow, event: React.DragEvent) => void; // currently unused } // We extract Row to its own mini-component primarily so we can React.memo'ize it. @@ -72,6 +74,7 @@ function RowImpl(props: RowProps): ReactElement { } = props; const { tableState } = useContext(TableStateContext); + const { sm } = useBreakpoint(); // We're wrapped in observer, so can access these without useComputeds const { api, visibleColumns: columns } = tableState; const { row, api: rowApi, isActive, isKept: isKeptRow, isLastKeptRow, level } = rs; @@ -93,7 +96,8 @@ function RowImpl(props: RowProps): ReactElement { const levelIndent = style.levels && style.levels[level]?.rowIndent; const rowCss = { - ...Css.add("transition", "padding-top 0.5s ease-in-out").$, + // ...Css.add("transition", "padding-top 0.5s ease-in-out").$, + ...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 && { @@ -116,7 +120,8 @@ function RowImpl(props: RowProps): ReactElement { [`:hover > .${revealOnRowHoverClass} > *`]: Css.visible.$, }, ...(isLastKeptRow && Css.addIn("&>*", style.keptLastRowCss).$), - ...(rs.isDraggedOver && Css.add("paddingTop", "50px").$), + ...(rs.isDraggedOver === DraggedOver.Above && Css.add("paddingTop", "50px").$), + ...(rs.isDraggedOver === DraggedOver.Below && Css.add("paddingBottom", "50px").$), }; let currentColspan = 1; @@ -126,18 +131,38 @@ function RowImpl(props: RowProps): ReactElement { let minStickyLeftOffset = 0; let expandColumnHidden = false; + // used to render the whole row when dragging with the handle + const ref = useRef(null); + return ( onDragStart?.(row, evt)} - onDragEnd={(evt) => onDragEnd?.(row, evt)} + // 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 && ( +
{ + // 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.$} + > + +
+ )} {isKeptGroupRow ? ( ) : ( diff --git a/src/components/Table/utils/RowState.ts b/src/components/Table/utils/RowState.ts index 2849421ca..65b71747f 100644 --- a/src/components/Table/utils/RowState.ts +++ b/src/components/Table/utils/RowState.ts @@ -6,6 +6,12 @@ import { RowStates } from "src/components/Table/utils/RowStates"; import { SelectedState } from "src/components/Table/utils/TableState"; import { applyRowFn, HEADER, KEPT_GROUP, matchesFilter, reservedRowKinds } from "src/components/Table/utils/utils"; +export enum DraggedOver { + None, + Above, // In this case this means higher on the screen which means a lower y value and a lower row index + Below, // In this case this means lower on the screen which means a higher y value and a higher row index +}; + /** * A reactive/observable state of each GridDataRow's current behavior. * @@ -24,7 +30,7 @@ export class RowState { /** Whether we are collapsed. */ collapsed = false; /** Whether we are dragged over. */ - isDraggedOver = false; + isDraggedOver: DraggedOver = DraggedOver.None; /** * Whether our `row` had been in `props.rows`, but then removed _while being * selected_, i.e. potentially by server-side filters. diff --git a/src/components/Table/utils/RowStates.ts b/src/components/Table/utils/RowStates.ts index 85d8f375e..5bd8fe129 100644 --- a/src/components/Table/utils/RowStates.ts +++ b/src/components/Table/utils/RowStates.ts @@ -1,7 +1,7 @@ import { ObservableMap } from "mobx"; import { Kinded } from "src"; import { GridDataRow } from "src/components/Table/components/Row"; -import { RowState } from "src/components/Table/utils/RowState"; +import { DraggedOver, RowState } from "src/components/Table/utils/RowState"; import { RowStorage } from "src/components/Table/utils/RowStorage"; import { TableState } from "src/components/Table/utils/TableState"; import { HEADER, KEPT_GROUP, reservedRowKinds } from "src/components/Table/utils/utils"; @@ -178,7 +178,7 @@ export class RowStates { return rs; } - setRowDraggedOver(id: string, draggedOver: boolean): void { + setRowDraggedOver(id: string, draggedOver: DraggedOver): void { const rs = this.get(id); // this allows a single-row re-render rs.isDraggedOver = draggedOver; diff --git a/src/components/Table/utils/TableState.ts b/src/components/Table/utils/TableState.ts index 7c9c8b29e..daea6d9bb 100644 --- a/src/components/Table/utils/TableState.ts +++ b/src/components/Table/utils/TableState.ts @@ -6,7 +6,7 @@ import { GridSortConfig } from "src/components/Table/GridTable"; import { GridTableApi } from "src/components/Table/GridTableApi"; import { Direction, GridColumnWithId } from "src/components/Table/types"; import { ColumnStates } from "src/components/Table/utils/ColumnStates"; -import { RowState } from "src/components/Table/utils/RowState"; +import { DraggedOver, RowState } from "src/components/Table/utils/RowState"; import { RowStates } from "src/components/Table/utils/RowStates"; import { ASC, DESC, HEADER, KEPT_GROUP, reservedRowKinds } from "src/components/Table/utils/utils"; @@ -253,9 +253,11 @@ export class TableState { this.rowStates.delete(ids); } - setRowDraggedOver(index: number, draggedOver: boolean): void { + setRowDraggedOver(id: string, draggedOver: DraggedOver): void { // if we do rowStates.setRows here all of the rows will re-render - this.rowStates.setRowDraggedOver(this.rows[index].id, draggedOver); + + // this.rows[index] can be undefined here? + this.rowStates.setRowDraggedOver(id, draggedOver); } } From 7a8fbb424889b4494e63d5fa015903bb57dea7b8 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Fri, 5 Jan 2024 13:44:00 -0800 Subject: [PATCH 14/22] test for draggable re-renders --- src/components/Table/GridTable.test.tsx | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/components/Table/GridTable.test.tsx b/src/components/Table/GridTable.test.tsx index f4bea1c19..7a3adca64 100644 --- a/src/components/Table/GridTable.test.tsx +++ b/src/components/Table/GridTable.test.tsx @@ -42,6 +42,11 @@ const rows: GridDataRow[] = [ { kind: "data", id: "1", data: { name: "foo", value: 1 } }, { kind: "data", id: "2", data: { name: "bar", value: 2 } }, ]; +const draggableRows: GridDataRow[] = [ + simpleHeader, + { kind: "data", id: "1", data: { name: "foo", value: 1 }, draggable: true }, + { kind: "data", id: "2", data: { name: "bar", value: 2 }, draggable: true }, +]; // Make a `NestedRow` ADT for a table with a header + 3 levels of nesting type TotalsRow = { kind: "totals"; id: string; data: undefined }; @@ -2462,6 +2467,33 @@ describe("GridTable", () => { expect(row(r, 2).getAttribute("data-render")).toEqual("1"); }); + it("memoizes draggable rows based on the data attribute", async () => { + const [header, row1, row2] = draggableRows; + const columns = [nameColumn]; + function onDropRow () {}; + // Given a table is initially rendered with 2 rows + const r = await render( + , + ); + // When we render with new rows but unchanged data values + r.rerender( + , + ); + // Then neither row was re-rendered + expect(row(r, 1).getAttribute("data-render")).toEqual("1"); + expect(row(r, 2).getAttribute("data-render")).toEqual("1"); + }); + it("reacts to setting activeRowId", async () => { const activeRowIdRowStyles: RowStyles = { data: { From 50483766c0d0305439d683a9da9d010fcddba55d Mon Sep 17 00:00:00 2001 From: bsholmes Date: Mon, 8 Jan 2024 15:11:19 -0800 Subject: [PATCH 15/22] handle children gracefully while dragging --- src/components/Table/GridTable.tsx | 59 +++++++++++++++++------- src/components/Table/components/Row.tsx | 6 +-- src/components/Table/utils/RowStates.ts | 20 +++++++- src/components/Table/utils/TableState.ts | 7 +-- 4 files changed, 65 insertions(+), 27 deletions(-) diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index ae5cbad8d..7bc44385c 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -245,6 +245,13 @@ export function GridTable = an [props.api], ); + const [draggedRow, _setDraggedRow] = useState | undefined>(undefined); + const draggedRowRef = useRef(draggedRow); + const setDraggedRow = (row: GridDataRow | undefined) => { + draggedRowRef.current = row; + _setDraggedRow(row); + }; + const style = resolveStyles(maybeStyle); const { tableState } = api; @@ -293,14 +300,24 @@ export function GridTable = an return clientY > rect.top + pt + ((rect.height - pb) / 2); } + // allows us to unset children and grandchildren, etc. + function recursiveSetDraggedOver(rows: GridDataRow[], draggedOver: DraggedOver) { + rows.forEach((r) => { + tableState.maybeSetRowDraggedOver(r.id, draggedOver); + if(r.children) { + recursiveSetDraggedOver(r.children, draggedOver); + } + }); + } + function onDragStart (row: GridDataRow, evt: DragEventType) { if(!row.draggable || !droppedCallback) { return; } evt.dataTransfer.effectAllowed = "move"; - evt.dataTransfer.dropEffect = "move"; evt.dataTransfer.setData("text/plain", JSON.stringify({ row })); + setDraggedRow(row); }; const onDragEnd = (row: GridDataRow, evt: DragEventType) => { @@ -310,9 +327,7 @@ export function GridTable = an evt.preventDefault(); evt.dataTransfer.clearData(); - rows.forEach((r) => { - tableState.setRowDraggedOver(r.id, DraggedOver.None); - }); + recursiveSetDraggedOver(rows, DraggedOver.None); }; const onDrop = (row: GridDataRow, evt: DragEventType) => { @@ -323,9 +338,8 @@ export function GridTable = an evt.preventDefault(); evt.dataTransfer.clearData(); if (droppedCallback) { - rows.forEach((r) => {r - tableState.setRowDraggedOver(r.id, DraggedOver.None); - }); + recursiveSetDraggedOver(rows, DraggedOver.None); + try { const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; @@ -349,15 +363,21 @@ export function GridTable = an evt.preventDefault(); + evt.cancelable + // set flags for css spacer - rows.forEach((r) => {r - tableState.setRowDraggedOver(r.id, DraggedOver.None); - }); + recursiveSetDraggedOver(rows, DraggedOver.None); - // determine above or below - const isBelow = isCursorBelowMidpoint(evt.currentTarget, evt.clientY); + if(draggedRowRef.current) { + + if(draggedRowRef.current.id === row.id) { + return; + } - tableState.setRowDraggedOver(row.id, isBelow ? DraggedOver.Below : DraggedOver.Above); + // determine above or below + const isBelow = isCursorBelowMidpoint(evt.currentTarget, evt.clientY); + tableState.maybeSetRowDraggedOver(row.id, isBelow ? DraggedOver.Below : DraggedOver.Above, draggedRowRef.current); + } }; const onDragOver = (row: GridDataRow, evt: DragEventType) => { @@ -367,9 +387,15 @@ export function GridTable = an evt.preventDefault(); - const isBelow = isCursorBelowMidpoint(evt.currentTarget, evt.clientY); - - tableState.setRowDraggedOver(row.id, isBelow ? DraggedOver.Below : DraggedOver.Above); + if(draggedRowRef.current) { + if(draggedRowRef.current.id === row.id) { + return; + } + + // continuously determine above or below + const isBelow = isCursorBelowMidpoint(evt.currentTarget, evt.clientY); + tableState.maybeSetRowDraggedOver(row.id, isBelow ? DraggedOver.Below : DraggedOver.Above, draggedRowRef.current); + } }; // Flatten, hide-if-filtered, hide-if-collapsed, and component-ize the sorted rows. @@ -391,7 +417,6 @@ export function GridTable = an // Get the flat list or rows from the header down... visibleRows.forEach((rs) => { - const row = ( (props: RowProps): ReactElement { } = props; const { tableState } = useContext(TableStateContext); - const { sm } = useBreakpoint(); // We're wrapped in observer, so can access these without useComputeds const { api, visibleColumns: columns } = tableState; const { row, api: rowApi, isActive, isKept: isKeptRow, isLastKeptRow, level } = rs; @@ -120,8 +118,8 @@ function RowImpl(props: RowProps): ReactElement { [`:hover > .${revealOnRowHoverClass} > *`]: Css.visible.$, }, ...(isLastKeptRow && Css.addIn("&>*", style.keptLastRowCss).$), - ...(rs.isDraggedOver === DraggedOver.Above && Css.add("paddingTop", "50px").$), - ...(rs.isDraggedOver === DraggedOver.Below && Css.add("paddingBottom", "50px").$), + ...(rs.isDraggedOver === DraggedOver.Above && Css.add("paddingTop", "35px").$), + ...(rs.isDraggedOver === DraggedOver.Below && Css.add("paddingBottom", "35px").$), }; let currentColspan = 1; diff --git a/src/components/Table/utils/RowStates.ts b/src/components/Table/utils/RowStates.ts index 5bd8fe129..e33118706 100644 --- a/src/components/Table/utils/RowStates.ts +++ b/src/components/Table/utils/RowStates.ts @@ -178,8 +178,26 @@ export class RowStates { return rs; } - setRowDraggedOver(id: string, draggedOver: DraggedOver): void { + maybeSetRowDraggedOver(id: string, draggedOver: DraggedOver, requireSameParentRow: GridDataRow | undefined = undefined): void { const rs = this.get(id); + + if(requireSameParentRow) { + const requireParentRowState = this.get(requireSameParentRow.id); + if(requireParentRowState.parent?.row?.id !== rs.parent?.row?.id) return; + } + + // if this is an expanded parent and draggedOver is Below then we want to set this on this rows bottom-most child + if(!rs.collapsed && rs.children && rs.children?.length > 0 && draggedOver === DraggedOver.Below) { + let rowState = rs; + // recursively find the bottom-most child + while(rowState.children && rowState.children?.length > 0) { + rowState = rowState.children[rowState.children.length - 1]; + } + + rowState.isDraggedOver = draggedOver; + return; + } + // this allows a single-row re-render rs.isDraggedOver = draggedOver; } diff --git a/src/components/Table/utils/TableState.ts b/src/components/Table/utils/TableState.ts index daea6d9bb..f6e078288 100644 --- a/src/components/Table/utils/TableState.ts +++ b/src/components/Table/utils/TableState.ts @@ -253,11 +253,8 @@ export class TableState { this.rowStates.delete(ids); } - setRowDraggedOver(id: string, draggedOver: DraggedOver): void { - // if we do rowStates.setRows here all of the rows will re-render - - // this.rows[index] can be undefined here? - this.rowStates.setRowDraggedOver(id, draggedOver); + maybeSetRowDraggedOver(id: string, draggedOver: DraggedOver, requireSameParentRow: GridDataRow | undefined = undefined): void { + this.rowStates.maybeSetRowDraggedOver(id, draggedOver, requireSameParentRow); } } From abc6e72c5e387415661f26c23575eae27135dac9 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Mon, 8 Jan 2024 15:32:57 -0800 Subject: [PATCH 16/22] cleanup --- src/components/Table/GridTable.stories.tsx | 11 ++++--- src/components/Table/GridTable.test.tsx | 27 ---------------- src/components/Table/GridTable.tsx | 37 ++++++++++------------ src/components/Table/components/Row.tsx | 3 +- src/components/Table/utils/RowState.ts | 2 +- src/components/Table/utils/RowStates.ts | 16 ++++++---- src/components/Table/utils/TableState.ts | 6 +++- 7 files changed, 41 insertions(+), 61 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index 7ef90eb90..df810742d 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -847,14 +847,16 @@ function makeNestedRows(repeat: number = 1, draggable: boolean = false): GridDat kind: "grandChild", id: `${p1}c1g1`, data: { name: `grandchild ${prefix}p1c1g1` + " foo".repeat(20) }, - draggable + draggable, }, { kind: "grandChild", id: `${p1}c1g2`, data: { name: `grandchild ${prefix}p1c1g2` }, draggable }, ], }, { ...{ kind: "child", id: `${p1}c2`, data: { name: `child ${prefix}p1c2` }, draggable }, - children: [{ kind: "grandChild", id: `${p1}c2g1`, data: { name: `grandchild ${prefix}p1c2g1` }, 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` }, draggable }, @@ -1901,13 +1903,12 @@ export function DraggableRows() { // console.log("ondrop: " + draggedRow.id + " -> " + droppedRow.id); - if(draggedRowIndex === -1 || droppedRowIndex === -1) { - console.error ("draggedRowIndex or droppedRowIndex is -1"); + 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); diff --git a/src/components/Table/GridTable.test.tsx b/src/components/Table/GridTable.test.tsx index 7a3adca64..17290ec67 100644 --- a/src/components/Table/GridTable.test.tsx +++ b/src/components/Table/GridTable.test.tsx @@ -2467,33 +2467,6 @@ describe("GridTable", () => { expect(row(r, 2).getAttribute("data-render")).toEqual("1"); }); - it("memoizes draggable rows based on the data attribute", async () => { - const [header, row1, row2] = draggableRows; - const columns = [nameColumn]; - function onDropRow () {}; - // Given a table is initially rendered with 2 rows - const r = await render( - , - ); - // When we render with new rows but unchanged data values - r.rerender( - , - ); - // Then neither row was re-rendered - expect(row(r, 1).getAttribute("data-render")).toEqual("1"); - expect(row(r, 2).getAttribute("data-render")).toEqual("1"); - }); - it("reacts to setting activeRowId", async () => { const activeRowIdRowStyles: RowStyles = { data: { diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index 7bc44385c..2f1aa4819 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -290,38 +290,38 @@ export function GridTable = an const expandedColumnIds: string[] = useComputed(() => tableState.expandedColumnIds, [tableState]); const columnSizes = useSetupColumnSizes(style, columns, resizeTarget ?? resizeRef, expandedColumnIds); - function isCursorBelowMidpoint (target: HTMLElement, clientY: number) { + 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 = parseInt(style.getPropertyValue("padding-top")) / 2; + const pb = parseInt(style.getPropertyValue("padding-bottom")); - return clientY > rect.top + pt + ((rect.height - pb) / 2); + return clientY > rect.top + pt + (rect.height - pb) / 2; } // allows us to unset children and grandchildren, etc. function recursiveSetDraggedOver(rows: GridDataRow[], draggedOver: DraggedOver) { rows.forEach((r) => { tableState.maybeSetRowDraggedOver(r.id, draggedOver); - if(r.children) { + if (r.children) { recursiveSetDraggedOver(r.children, draggedOver); } }); } - function onDragStart (row: GridDataRow, evt: DragEventType) { - if(!row.draggable || !droppedCallback) { + function onDragStart(row: GridDataRow, evt: DragEventType) { + if (!row.draggable || !droppedCallback) { return; } evt.dataTransfer.effectAllowed = "move"; evt.dataTransfer.setData("text/plain", JSON.stringify({ row })); setDraggedRow(row); - }; + } const onDragEnd = (row: GridDataRow, evt: DragEventType) => { - if(!row.draggable || !droppedCallback) { + if (!row.draggable || !droppedCallback) { return; } @@ -331,7 +331,7 @@ export function GridTable = an }; const onDrop = (row: GridDataRow, evt: DragEventType) => { - if(!row.draggable || !droppedCallback) { + if (!row.draggable || !droppedCallback) { return; } @@ -343,7 +343,7 @@ export function GridTable = an try { const draggedRowData = JSON.parse(evt.dataTransfer.getData("text/plain")).row; - if(draggedRowData.id === row.id) { + if (draggedRowData.id === row.id) { return; } @@ -357,20 +357,17 @@ export function GridTable = an }; const onDragEnter = (row: GridDataRow, evt: DragEventType) => { - if(!row.draggable || !droppedCallback) { + if (!row.draggable || !droppedCallback) { return; } evt.preventDefault(); - evt.cancelable - // set flags for css spacer recursiveSetDraggedOver(rows, DraggedOver.None); - if(draggedRowRef.current) { - - if(draggedRowRef.current.id === row.id) { + if (draggedRowRef.current) { + if (draggedRowRef.current.id === row.id) { return; } @@ -381,14 +378,14 @@ export function GridTable = an }; const onDragOver = (row: GridDataRow, evt: DragEventType) => { - if(!row.draggable || !droppedCallback) { + if (!row.draggable || !droppedCallback) { return; } evt.preventDefault(); - if(draggedRowRef.current) { - if(draggedRowRef.current.id === row.id) { + if (draggedRowRef.current) { + if (draggedRowRef.current.id === row.id) { return; } diff --git a/src/components/Table/components/Row.tsx b/src/components/Table/components/Row.tsx index 4911f9c1c..e1306ba84 100644 --- a/src/components/Table/components/Row.tsx +++ b/src/components/Table/components/Row.tsx @@ -136,7 +136,8 @@ function RowImpl(props: RowProps): ReactElement { onDrop?.(row, evt)} diff --git a/src/components/Table/utils/RowState.ts b/src/components/Table/utils/RowState.ts index 65b71747f..77fc75d8d 100644 --- a/src/components/Table/utils/RowState.ts +++ b/src/components/Table/utils/RowState.ts @@ -10,7 +10,7 @@ export enum DraggedOver { None, Above, // In this case this means higher on the screen which means a lower y value and a lower row index Below, // In this case this means lower on the screen which means a higher y value and a higher row index -}; +} /** * A reactive/observable state of each GridDataRow's current behavior. diff --git a/src/components/Table/utils/RowStates.ts b/src/components/Table/utils/RowStates.ts index e33118706..823029a33 100644 --- a/src/components/Table/utils/RowStates.ts +++ b/src/components/Table/utils/RowStates.ts @@ -178,22 +178,26 @@ export class RowStates { return rs; } - maybeSetRowDraggedOver(id: string, draggedOver: DraggedOver, requireSameParentRow: GridDataRow | undefined = undefined): void { + maybeSetRowDraggedOver( + id: string, + draggedOver: DraggedOver, + requireSameParentRow: GridDataRow | undefined = undefined, + ): void { const rs = this.get(id); - if(requireSameParentRow) { + if (requireSameParentRow) { const requireParentRowState = this.get(requireSameParentRow.id); - if(requireParentRowState.parent?.row?.id !== rs.parent?.row?.id) return; + if (requireParentRowState.parent?.row?.id !== rs.parent?.row?.id) return; } // if this is an expanded parent and draggedOver is Below then we want to set this on this rows bottom-most child - if(!rs.collapsed && rs.children && rs.children?.length > 0 && draggedOver === DraggedOver.Below) { + if (!rs.collapsed && rs.children && rs.children?.length > 0 && draggedOver === DraggedOver.Below) { let rowState = rs; // recursively find the bottom-most child - while(rowState.children && rowState.children?.length > 0) { + while (rowState.children && rowState.children?.length > 0) { rowState = rowState.children[rowState.children.length - 1]; } - + rowState.isDraggedOver = draggedOver; return; } diff --git a/src/components/Table/utils/TableState.ts b/src/components/Table/utils/TableState.ts index f6e078288..565a3e785 100644 --- a/src/components/Table/utils/TableState.ts +++ b/src/components/Table/utils/TableState.ts @@ -253,7 +253,11 @@ export class TableState { this.rowStates.delete(ids); } - maybeSetRowDraggedOver(id: string, draggedOver: DraggedOver, requireSameParentRow: GridDataRow | undefined = undefined): void { + maybeSetRowDraggedOver( + id: string, + draggedOver: DraggedOver, + requireSameParentRow: GridDataRow | undefined = undefined, + ): void { this.rowStates.maybeSetRowDraggedOver(id, draggedOver, requireSameParentRow); } } From fd3b60770ebc8fc5d9ccded93639473677d06094 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Tue, 9 Jan 2024 17:13:40 -0800 Subject: [PATCH 17/22] draghandle column component, child reordering in story, some cleanup --- src/components/Table/GridTable.stories.tsx | 138 ++++++++++++--------- src/components/Table/GridTable.tsx | 12 +- src/components/Table/components/Row.tsx | 23 +++- src/components/Table/utils/RowState.ts | 1 - src/components/Table/utils/columns.tsx | 65 +++++++++- src/components/Table/utils/utils.tsx | 41 +++++- 6 files changed, 205 insertions(+), 75 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index df810742d..473c3d292 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -28,6 +28,8 @@ import { SimpleHeaderAndData, useGridTableApi, insertAtIndex, + dragHandleColumn, + recursivelyGetContainingRow, } from "src/components/index"; import { Css, Palette } from "src/Css"; import { useComputed } from "src/hooks"; @@ -1858,6 +1860,7 @@ export function Headers() { * Shows how drag & drop reordering can be implemented with GridTable drag events */ export function DraggableRows() { + const dragColumn = dragHandleColumn({}); const nameColumn: GridColumn = { header: "Name", data: ({ name }) => ({ content:
{name}
, sortValue: name }), @@ -1883,38 +1886,17 @@ export function DraggableRows() { // also works with as="table" and as="virtual" return ( { 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)]); - } + // insert it at the dropped row index + setRows([...insertAtIndex(tempRows, reorderRow, droppedRowIndex + indexOffset)]); }} rows={[...rows]} /> @@ -1923,43 +1905,57 @@ export function DraggableRows() { export const DraggableWithInputColumns = newStory( () => { + const dragColumn = dragHandleColumn({}); const nameCol = column({ header: "Name", data: ({ name }) => name }); const priceCol = numericColumn({ header: "Price", data: ({ priceInCents }) => , }); const actionCol = actionColumn({ header: "Action", data: () => }); + + const [rows, setRows] = useState[]>([ + 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, + }, + ]); + return ( - 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={() => {}} + columns={[dragColumn, nameCol, priceCol, actionCol]} + rows={rows} + 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)]); + }} /> ); }, @@ -1969,6 +1965,7 @@ export const DraggableWithInputColumns = newStory( const draggableRows = makeNestedRows(1, true); const draggableRowsWithHeader: GridDataRow[] = [simpleHeader, ...draggableRows]; export function DraggableNestedRows() { + const dragColumn = dragHandleColumn({}); const nameColumn: GridColumn = { header: () => "Name", parent: (row) => ({ @@ -1985,12 +1982,41 @@ export function DraggableNestedRows() { }), add: () => "Add", }; + + const [rows, setRows] = useState[]>(draggableRowsWithHeader); + return ( (), nameColumn]} - {...{ rows: draggableRowsWithHeader }} + columns={[dragColumn, collapseColumn(), nameColumn]} + rows={rows} sorting={{ on: "client", initial: ["c1", "ASC"] }} - onRowDrop={(draggedRow, droppedRow) => {}} + onRowDrop={(draggedRow, droppedRow, indexOffset) => { + + const tempRows = [...rows]; + const foundRowContainer = recursivelyGetContainingRow(draggedRow.id, tempRows)!; + if (!foundRowContainer) { + console.error("Could not find row array for row", draggedRow); + return; + } + if (!foundRowContainer.array.some(row => row.id === droppedRow.id)) { + console.error("Could not find dropped row in row array", droppedRow); + return; + } + // remove dragged row + const draggedRowIndex = foundRowContainer.array.findIndex((r) => r.id === draggedRow.id); + const reorderRow = foundRowContainer.array.splice(draggedRowIndex, 1)[0]; + + const droppedRowIndex = foundRowContainer.array.findIndex((r) => r.id === droppedRow.id); + + // we also need the parent row so we can set the newly inserted array + if (foundRowContainer.parent && foundRowContainer.parent?.children) { + foundRowContainer.parent.children = [...insertAtIndex(foundRowContainer.parent?.children, reorderRow, droppedRowIndex + indexOffset)]; + setRows([...tempRows]); + } + else { + setRows([...insertAtIndex(tempRows, reorderRow, droppedRowIndex + indexOffset)]); + } + }} /> ); } diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index 2f1aa4819..6474e125c 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -20,7 +20,7 @@ import { import { assignDefaultColumnIds } from "src/components/Table/utils/columns"; import { GridRowLookup } from "src/components/Table/utils/GridRowLookup"; import { TableStateContext } from "src/components/Table/utils/TableState"; -import { EXPANDABLE_HEADER, KEPT_GROUP, insertAtIndex, zIndices } from "src/components/Table/utils/utils"; +import { EXPANDABLE_HEADER, KEPT_GROUP, isCursorBelowMidpoint, zIndices } from "src/components/Table/utils/utils"; import { Css, Only } from "src/Css"; import { useComputed } from "src/hooks"; import { useRenderCount } from "src/hooks/useRenderCount"; @@ -290,16 +290,6 @@ export function GridTable = an const expandedColumnIds: string[] = useComputed(() => tableState.expandedColumnIds, [tableState]); const columnSizes = useSetupColumnSizes(style, columns, resizeTarget ?? resizeRef, expandedColumnIds); - 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")); - - return clientY > rect.top + pt + (rect.height - pb) / 2; - } - // allows us to unset children and grandchildren, etc. function recursiveSetDraggedOver(rows: GridDataRow[], draggedOver: DraggedOver) { rows.forEach((r) => { diff --git a/src/components/Table/components/Row.tsx b/src/components/Table/components/Row.tsx index e1306ba84..5cc15c81b 100644 --- a/src/components/Table/components/Row.tsx +++ b/src/components/Table/components/Row.tsx @@ -9,7 +9,7 @@ import { } from "src/components/Table/components/cell"; import { KeptGroupRow } from "src/components/Table/components/KeptGroupRow"; import { GridStyle, RowStyles } from "src/components/Table/TableStyles"; -import { DiscriminateUnion, IfAny, Kinded, Pin, RenderAs } from "src/components/Table/types"; +import { DiscriminateUnion, GridColumnWithId, IfAny, Kinded, Pin, RenderAs } from "src/components/Table/types"; import { DraggedOver, RowState } from "src/components/Table/utils/RowState"; import { ensureClientSideSortValueIsSortable } from "src/components/Table/utils/sortRows"; import { TableStateContext } from "src/components/Table/utils/TableState"; @@ -94,7 +94,6 @@ function RowImpl(props: RowProps): ReactElement { const levelIndent = style.levels && style.levels[level]?.rowIndent; const rowCss = { - // ...Css.add("transition", "padding-top 0.5s ease-in-out").$, ...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. @@ -145,7 +144,7 @@ function RowImpl(props: RowProps): ReactElement { onDragOver={(evt) => onDragOver?.(row, evt)} ref={ref} > - {row.draggable && ( + {/* {row.draggable && (
{ @@ -161,7 +160,7 @@ function RowImpl(props: RowProps): ReactElement { >
- )} + )} */} {isKeptGroupRow ? ( ) : ( @@ -222,7 +221,21 @@ function RowImpl(props: RowProps): ReactElement { currentColspan -= 1; return null; } - const maybeContent = applyRowFn(column, row, rowApi, level, isExpanded); + const maybeContent = applyRowFn( + column as GridColumnWithId, + row, + rowApi, + level, + isExpanded, + { + rowRenderRef: ref, + onDragStart, + onDragEnd, + onDrop, + onDragEnter, + onDragOver, + } + ); // Only use the `numExpandedColumns` as the `colspan` when rendering the "Expandable Header" currentColspan = diff --git a/src/components/Table/utils/RowState.ts b/src/components/Table/utils/RowState.ts index 77fc75d8d..0784de30b 100644 --- a/src/components/Table/utils/RowState.ts +++ b/src/components/Table/utils/RowState.ts @@ -67,7 +67,6 @@ export class RowState { _row: false, _data: observable.ref, isCalculatingDirectMatch: false, - isDraggedOver: observable.ref, // allows the table to re-render only this row when the dragged over state changes } as any, { name: `RowState@${row.id}` }, ); diff --git a/src/components/Table/utils/columns.tsx b/src/components/Table/utils/columns.tsx index c0b958912..893f7267e 100644 --- a/src/components/Table/utils/columns.tsx +++ b/src/components/Table/utils/columns.tsx @@ -2,8 +2,10 @@ import { CollapseToggle } from "src/components/Table/components/CollapseToggle"; import { GridDataRow } from "src/components/Table/components/Row"; import { SelectToggle } from "src/components/Table/components/SelectToggle"; import { GridColumn, GridColumnWithId, Kinded, nonKindGridColumnKeys } from "src/components/Table/types"; -import { emptyCell } from "src/components/Table/utils/utils"; +import { DragData, emptyCell } from "src/components/Table/utils/utils"; import { isFunction, newMethodMissingProxy } from "src/utils"; +import { Icon } from "src"; +import { Css, Palette } from "src/Css"; /** Provides default styling for a GridColumn representing a Date. */ export function column(columnDef: GridColumn): GridColumn { @@ -191,3 +193,64 @@ export function assignDefaultColumnIds(columns: GridColumn[ } export const generateColumnId = (columnIndex: number) => `beamColumn_${columnIndex}`; + +export function dragHandleColumn(columnDef?: Partial>): GridColumn { + const base = { + ...nonKindDefaults(), + 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; level: number }) => ({ + // content: 0} />, + // }); + // }) as any; + + return newMethodMissingProxy(base, (key) => { + return (data: any, { row, dragData }: { row: GridDataRow, dragData: DragData }) => { + 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 ? ( +
{ + // 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.$} + > + +
+ ) : undefined, + }); + } + }) as any; +} \ No newline at end of file diff --git a/src/components/Table/utils/utils.tsx b/src/components/Table/utils/utils.tsx index 22158670f..f676c21d4 100644 --- a/src/components/Table/utils/utils.tsx +++ b/src/components/Table/utils/utils.tsx @@ -130,6 +130,15 @@ function isContentEmpty(content: ReactNode): boolean { return emptyValues.includes(content); } +export type DragData = { + rowRenderRef: React.RefObject; + onDragStart?: (row: GridDataRow, event: React.DragEvent) => void; + onDragEnd?: (row: GridDataRow, event: React.DragEvent) => void; + onDrop?: (row: GridDataRow, event: React.DragEvent) => void; + onDragEnter?: (row: GridDataRow, event: React.DragEvent) => void; + onDragOver?: (row: GridDataRow, event: React.DragEvent) => void; +} + /** Return the content for a given column def applied to a given row. */ export function applyRowFn( column: GridColumnWithId, @@ -137,12 +146,13 @@ export function applyRowFn( api: GridRowApi, level: number, expanded: boolean, + dragData?: DragData, ): ReactNode | GridCellContent { // Usually this is a function to apply against the row, but sometimes it's a hard-coded value, i.e. for headers const maybeContent = column[row.kind]; if (typeof maybeContent === "function") { // Auto-destructure data - return (maybeContent as Function)((row as any)["data"], { row: row as any, api, level, expanded }); + return (maybeContent as Function)((row as any)["data"], { row: row as any, api, level, expanded, dragData }); } else { return maybeContent; } @@ -258,3 +268,32 @@ export function loadArrayOrUndefined(key: string) { export function insertAtIndex(array: Array, element: T, index: number): Array { return [...array.slice(0, index), element, ...array.slice(index, array.length)]; } + +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")); + + return clientY > rect.top + pt + (rect.height - pb) / 2; +} + +function recursivelyGetContainingRow (rowId: string, rowArray: GridDataRow[], parent?: GridDataRow): { array: GridDataRow[], parent: GridDataRow | undefined } | undefined { + if (rowArray.some(row => row.id === rowId)) { + return { array: rowArray, parent }; + } + + for (let i = 0; i < rowArray.length; i++) { + if (!rowArray[i].children) { + continue; + } + + const result = recursivelyGetContainingRow(rowId, rowArray[i].children!, rowArray[i]); + if (result) { + return result; + } + } + + return undefined; +} \ No newline at end of file From 98f18d54349b9750cb4471104a8290f2e5139571 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Tue, 9 Jan 2024 17:19:18 -0800 Subject: [PATCH 18/22] lint --- src/components/Table/GridTable.stories.tsx | 16 +++++++-------- src/components/Table/components/Row.tsx | 23 ++++++++-------------- src/components/Table/utils/columns.tsx | 23 ++++++++-------------- src/components/Table/utils/utils.tsx | 12 +++++++---- 4 files changed, 32 insertions(+), 42 deletions(-) diff --git a/src/components/Table/GridTable.stories.tsx b/src/components/Table/GridTable.stories.tsx index 473c3d292..51adefbcb 100644 --- a/src/components/Table/GridTable.stories.tsx +++ b/src/components/Table/GridTable.stories.tsx @@ -1940,7 +1940,7 @@ export const DraggableWithInputColumns = newStory( draggable: true, }, ]); - + return ( columns={[dragColumn, nameCol, priceCol, actionCol]} @@ -1950,9 +1950,9 @@ export const DraggableWithInputColumns = newStory( // 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)]); }} @@ -1991,14 +1991,13 @@ export function DraggableNestedRows() { rows={rows} sorting={{ on: "client", initial: ["c1", "ASC"] }} onRowDrop={(draggedRow, droppedRow, indexOffset) => { - const tempRows = [...rows]; const foundRowContainer = recursivelyGetContainingRow(draggedRow.id, tempRows)!; if (!foundRowContainer) { console.error("Could not find row array for row", draggedRow); return; } - if (!foundRowContainer.array.some(row => row.id === droppedRow.id)) { + if (!foundRowContainer.array.some((row) => row.id === droppedRow.id)) { console.error("Could not find dropped row in row array", droppedRow); return; } @@ -2010,10 +2009,11 @@ export function DraggableNestedRows() { // we also need the parent row so we can set the newly inserted array if (foundRowContainer.parent && foundRowContainer.parent?.children) { - foundRowContainer.parent.children = [...insertAtIndex(foundRowContainer.parent?.children, reorderRow, droppedRowIndex + indexOffset)]; + foundRowContainer.parent.children = [ + ...insertAtIndex(foundRowContainer.parent?.children, reorderRow, droppedRowIndex + indexOffset), + ]; setRows([...tempRows]); - } - else { + } else { setRows([...insertAtIndex(tempRows, reorderRow, droppedRowIndex + indexOffset)]); } }} diff --git a/src/components/Table/components/Row.tsx b/src/components/Table/components/Row.tsx index 5cc15c81b..fbf92a72d 100644 --- a/src/components/Table/components/Row.tsx +++ b/src/components/Table/components/Row.tsx @@ -221,21 +221,14 @@ function RowImpl(props: RowProps): ReactElement { currentColspan -= 1; return null; } - const maybeContent = applyRowFn( - column as GridColumnWithId, - row, - rowApi, - level, - isExpanded, - { - rowRenderRef: ref, - onDragStart, - onDragEnd, - onDrop, - onDragEnter, - onDragOver, - } - ); + const maybeContent = applyRowFn(column as GridColumnWithId, row, rowApi, level, isExpanded, { + rowRenderRef: ref, + onDragStart, + onDragEnd, + onDrop, + onDragEnter, + onDragOver, + }); // Only use the `numExpandedColumns` as the `colspan` when rendering the "Expandable Header" currentColspan = diff --git a/src/components/Table/utils/columns.tsx b/src/components/Table/utils/columns.tsx index 893f7267e..3c96b20c7 100644 --- a/src/components/Table/utils/columns.tsx +++ b/src/components/Table/utils/columns.tsx @@ -217,20 +217,13 @@ export function dragHandleColumn(columnDef?: Partial 0} />, // }); // }) as any; - + return newMethodMissingProxy(base, (key) => { - return (data: any, { row, dragData }: { row: GridDataRow, dragData: DragData }) => { - if (!dragData) return ; - const { - rowRenderRef: ref, - onDragStart, - onDragEnd, - onDrop, - onDragEnter, - onDragOver - } = dragData; + return (data: any, { row, dragData }: { row: GridDataRow; dragData: DragData }) => { + if (!dragData) return; + const { rowRenderRef: ref, onDragStart, onDragEnd, onDrop, onDragEnter, onDragOver } = dragData; - return ({ + return { // how do we get the callbacks and the ref here? // inject them into the row in the Row component? content: row.draggable ? ( @@ -250,7 +243,7 @@ export function dragHandleColumn(columnDef?: Partial ) : undefined, - }); - } + }; + }; }) as any; -} \ No newline at end of file +} diff --git a/src/components/Table/utils/utils.tsx b/src/components/Table/utils/utils.tsx index f676c21d4..c62bf1aa8 100644 --- a/src/components/Table/utils/utils.tsx +++ b/src/components/Table/utils/utils.tsx @@ -137,7 +137,7 @@ export type DragData = { onDrop?: (row: GridDataRow, event: React.DragEvent) => void; onDragEnter?: (row: GridDataRow, event: React.DragEvent) => void; onDragOver?: (row: GridDataRow, event: React.DragEvent) => void; -} +}; /** Return the content for a given column def applied to a given row. */ export function applyRowFn( @@ -279,8 +279,12 @@ export function isCursorBelowMidpoint(target: HTMLElement, clientY: number) { return clientY > rect.top + pt + (rect.height - pb) / 2; } -function recursivelyGetContainingRow (rowId: string, rowArray: GridDataRow[], parent?: GridDataRow): { array: GridDataRow[], parent: GridDataRow | undefined } | undefined { - if (rowArray.some(row => row.id === rowId)) { +function recursivelyGetContainingRow( + rowId: string, + rowArray: GridDataRow[], + parent?: GridDataRow, +): { array: GridDataRow[]; parent: GridDataRow | undefined } | undefined { + if (rowArray.some((row) => row.id === rowId)) { return { array: rowArray, parent }; } @@ -296,4 +300,4 @@ function recursivelyGetContainingRow (rowId: string, rowArray: } return undefined; -} \ No newline at end of file +} From cfc3672a2baaad1570b11d1b733cd5aa2e392186 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Wed, 10 Jan 2024 09:11:23 -0800 Subject: [PATCH 19/22] export --- src/components/Table/utils/utils.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Table/utils/utils.tsx b/src/components/Table/utils/utils.tsx index c62bf1aa8..d42b98ab1 100644 --- a/src/components/Table/utils/utils.tsx +++ b/src/components/Table/utils/utils.tsx @@ -279,7 +279,7 @@ export function isCursorBelowMidpoint(target: HTMLElement, clientY: number) { return clientY > rect.top + pt + (rect.height - pb) / 2; } -function recursivelyGetContainingRow( +export function recursivelyGetContainingRow( rowId: string, rowArray: GridDataRow[], parent?: GridDataRow, From 83efc86f92dbecb78a5eed47a18ffedc8e94050c Mon Sep 17 00:00:00 2001 From: bsholmes Date: Wed, 10 Jan 2024 09:45:51 -0800 Subject: [PATCH 20/22] memoize test for row rerender --- src/components/Table/GridTable.test.tsx | 27 +++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/components/Table/GridTable.test.tsx b/src/components/Table/GridTable.test.tsx index 17290ec67..7a3adca64 100644 --- a/src/components/Table/GridTable.test.tsx +++ b/src/components/Table/GridTable.test.tsx @@ -2467,6 +2467,33 @@ describe("GridTable", () => { expect(row(r, 2).getAttribute("data-render")).toEqual("1"); }); + it("memoizes draggable rows based on the data attribute", async () => { + const [header, row1, row2] = draggableRows; + const columns = [nameColumn]; + function onDropRow () {}; + // Given a table is initially rendered with 2 rows + const r = await render( + , + ); + // When we render with new rows but unchanged data values + r.rerender( + , + ); + // Then neither row was re-rendered + expect(row(r, 1).getAttribute("data-render")).toEqual("1"); + expect(row(r, 2).getAttribute("data-render")).toEqual("1"); + }); + it("reacts to setting activeRowId", async () => { const activeRowIdRowStyles: RowStyles = { data: { From 2f42520ed65c7a2598e50cc9a52acf1f4bb0f77b Mon Sep 17 00:00:00 2001 From: bsholmes Date: Wed, 10 Jan 2024 10:09:21 -0800 Subject: [PATCH 21/22] lint --- src/components/Table/GridTable.test.tsx | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/components/Table/GridTable.test.tsx b/src/components/Table/GridTable.test.tsx index 7a3adca64..921ec15ea 100644 --- a/src/components/Table/GridTable.test.tsx +++ b/src/components/Table/GridTable.test.tsx @@ -2470,25 +2470,11 @@ describe("GridTable", () => { it("memoizes draggable rows based on the data attribute", async () => { const [header, row1, row2] = draggableRows; const columns = [nameColumn]; - function onDropRow () {}; + function onDropRow() {} // Given a table is initially rendered with 2 rows - const r = await render( - , - ); + const r = await render(); // When we render with new rows but unchanged data values - r.rerender( - , - ); + r.rerender(); // Then neither row was re-rendered expect(row(r, 1).getAttribute("data-render")).toEqual("1"); expect(row(r, 2).getAttribute("data-render")).toEqual("1"); From 92c5498ef669018af642309d2841ecff74322a34 Mon Sep 17 00:00:00 2001 From: bsholmes Date: Wed, 10 Jan 2024 11:44:16 -0800 Subject: [PATCH 22/22] fix for Firefox --- src/components/Table/GridTable.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/Table/GridTable.tsx b/src/components/Table/GridTable.tsx index 6474e125c..c104d18d0 100644 --- a/src/components/Table/GridTable.tsx +++ b/src/components/Table/GridTable.tsx @@ -316,7 +316,6 @@ export function GridTable = an } evt.preventDefault(); - evt.dataTransfer.clearData(); recursiveSetDraggedOver(rows, DraggedOver.None); }; @@ -326,7 +325,6 @@ export function GridTable = an } evt.preventDefault(); - evt.dataTransfer.clearData(); if (droppedCallback) { recursiveSetDraggedOver(rows, DraggedOver.None);