Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
b0673f7
feat: 🎸 add posibility to resize columns
mir4a Apr 24, 2024
bb5db52
wip: testing initial columns width option
mir4a Apr 24, 2024
d9bc1bd
wip: resize workaround
mir4a Apr 26, 2024
bbda4ec
add throttle to onDrag
mir4a Apr 26, 2024
7faf986
naming convention
mir4a Apr 26, 2024
4a855c9
fixed resize callbacks
mir4a Apr 29, 2024
a92caa8
cleanup and type fix
mir4a Apr 29, 2024
cf528cf
cleanup
mir4a Apr 29, 2024
380913c
run formatter
mir4a Apr 29, 2024
4004446
audit fix for docs
mir4a Apr 29, 2024
e627b4f
fix: 🐛 add resizable only if callback is provided
mir4a Apr 29, 2024
47bd71d
fix check for callback
mir4a Apr 29, 2024
6aa72ee
docs: ✏️ add new props
mir4a Apr 29, 2024
eaace62
chore: 🤖 bump 4.12.0-alpha.1
mir4a Apr 29, 2024
9ade20e
chore: 🤖 up docusaurus deps
mir4a Apr 29, 2024
d1e9e4e
temp publishing under my org
mir4a Apr 29, 2024
74df72e
temp update repo url
mir4a Apr 29, 2024
9aba369
fix: 🐛 onDragEnd callback
mir4a Apr 29, 2024
c2fe251
chore: 🤖 bump version
mir4a Apr 29, 2024
74c62f3
fix: 🐛 prevent column collapse
mir4a Apr 29, 2024
94ef7bb
bump
mir4a Apr 29, 2024
d944018
revert package.json
mir4a Apr 29, 2024
680a6db
feat: 🎸 save resized column widths as a map by col id
mir4a Apr 30, 2024
38e5687
feat: 🎸 save resized column widths as a map by col id
mir4a Apr 30, 2024
9f12c98
bump
mir4a Apr 30, 2024
cb59f1c
fix: 🐛 adjust debounce timeout for height calc
mir4a Apr 30, 2024
1a36022
bump
mir4a Apr 30, 2024
453d865
fix: 🐛 horizontal scroll due to fractional width values
mir4a May 2, 2024
3c659bf
bump
mir4a May 2, 2024
141a980
fix: 🐛 horizontal scroll appear
mir4a May 3, 2024
a3c3724
bump
mir4a May 3, 2024
263d0cd
workaround with hiding Y scroll while resizing
mir4a May 4, 2024
71e459a
bump
mir4a May 4, 2024
595005b
reduce jumping via debouncing
mir4a May 4, 2024
77358f0
bump
mir4a May 4, 2024
22652bc
calculate scrollbar size and use it for table height calculation
mir4a May 4, 2024
6c2d31d
bump
mir4a May 4, 2024
0e1bffa
while resize adjust the rest columns to fit available width
mir4a May 5, 2024
cb5eed9
bump
mir4a May 5, 2024
f39a1c1
check document and window to prevent error on SSR
mir4a May 6, 2024
7946102
bump
mir4a May 6, 2024
a1877b3
debug adjusted height
mir4a May 18, 2024
9e99a88
bump
mir4a May 18, 2024
6c7f901
bump and rebuild
mir4a May 18, 2024
b48ee48
Merge branch 'resize-columns' into feat/resize-columns
mir4a May 19, 2024
af14638
cleanup
mir4a May 19, 2024
5348cf1
use color var for for column resize handler
mir4a May 19, 2024
ede1a05
package-lock package name fix
mir4a May 19, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
702 changes: 413 additions & 289 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "react-datasheet-grid",
"version": "4.11.4",
"version": "4.12.0",
"description": "An Excel-like React component to create beautiful spreadsheets.",
"main": "dist/index.js",
"types": "dist/index.d.ts",
Expand Down
101 changes: 98 additions & 3 deletions src/components/Cell.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,15 @@
import React, { FC } from 'react'
import React, { FC, useLayoutEffect, useRef, useState } from 'react'
import cx from 'classnames'
import { useResizeHandle } from '../hooks/useResizeHandler'
import { throttle } from 'throttle-debounce'
import { useColumnWidthsContext } from '../hooks/useColumnsWidthContext'

export const Cell: FC<{
export const MIN_COLUMN_WIDTH = 40

type CellProps = {
id?: string
index: number
isHeader?: boolean
gutter: boolean
stickyRight: boolean
disabled?: boolean
Expand All @@ -10,7 +18,17 @@ export const Cell: FC<{
children?: any
width: number
left: number
}> = ({
}

export const Cell: FC<CellProps> = ({ isHeader, ...props }) => {
return isHeader ? (
<HeaderCell {...props} resizable={props.index !== 0} />
) : (
<BodyCell {...props} />
)
}

export const BodyCell: FC<CellProps> = ({
children,
gutter,
stickyRight,
Expand Down Expand Up @@ -39,3 +57,80 @@ export const Cell: FC<{
</div>
)
}

export const HeaderCell: FC<CellProps & { resizable?: boolean }> = ({
id,
index,
children,
gutter,
stickyRight,
active,
disabled,
className,
width,
left,
resizable,
}) => {
const {
columnWidths,
columnsMap,
resizedColumnWidths,
onColumnsResize,
resizeCallback,
} = useColumnWidthsContext()

const [prevWidth, setPrevWidth] = useState(width)

const colWidth = useRef(width)

useLayoutEffect(() => {
setPrevWidth(width)
colWidth.current = width
}, [width])

const throttledOnDrag = throttle(50, (dx = 0) => {
// prevent column collapse
if (prevWidth + dx <= MIN_COLUMN_WIDTH) {
return
}

colWidth.current = prevWidth + dx

if (id) {
resizeCallback?.(() => ({
...resizedColumnWidths,
[id]: colWidth.current,
}))
}
})

const ref = useResizeHandle({
onDrag: throttledOnDrag,
onDragEnd: () => {
if (id) {
onColumnsResize?.({ ...resizedColumnWidths, [id]: colWidth.current })
}
},
})
return (
<div
className={cx(
'dsg-cell',
gutter && 'dsg-cell-gutter',
disabled && 'dsg-cell-disabled',
gutter && active && 'dsg-cell-gutter-active',
stickyRight && 'dsg-cell-sticky-right',
className
)}
style={{
width: colWidth.current ?? width,
left: stickyRight ? undefined : left,
}}
>
{children}
{resizable && onColumnsResize && (
<div className="dsg-resize-handle" ref={ref} />
)}
</div>
)
}
215 changes: 139 additions & 76 deletions src/components/DataSheetGrid.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import { getAllTabbableElements } from '../utils/tab'
import { Grid } from './Grid'
import { SelectionRect } from './SelectionRect'
import { useRowHeights } from '../hooks/useRowHeights'
import { ColumnWidthsContext } from '../hooks/useColumnsWidthContext'
import { useScrollbarSize } from '../hooks/useScrollbarSize'

const DEFAULT_DATA: any[] = []
const DEFAULT_COLUMNS: Column<any, any, any>[] = []
Expand Down Expand Up @@ -87,6 +89,8 @@ export const DataSheetGrid = React.memo(
rowClassName,
cellClassName,
onScroll,
initialColumnWidths,
onColumnsResize,
}: DataSheetGridProps<T>,
ref: React.ForwardedRef<DataSheetGridRef>
): JSX.Element => {
Expand All @@ -98,9 +102,27 @@ export const DataSheetGrid = React.memo(
const outerRef = useRef<HTMLDivElement>(null)
const beforeTabIndexRef = useRef<HTMLDivElement>(null)
const afterTabIndexRef = useRef<HTMLDivElement>(null)

// Default value is 1 for the border
const [heightDiff, setHeightDiff] = useDebounceState(1, 100)
const [heightDiff, setHeightDiff] = useDebounceState(20, 100)
const [resizedColumnWidths, setResizedColumnWidths] = useState<
Record<string, number>
>(initialColumnWidths ?? {})
const [resizing, setResizing] = useDebounceState<boolean>(false, 50)
// FIXME: this needs to be calculated on the first render somehow
const [horizontalScroll, setHorizontalScroll] = useState<boolean>(false)
const scrollbarSize = useScrollbarSize()

const resizeCallback = (
val: React.SetStateAction<Record<string, number>>
) => {
setResizing(true)
setResizedColumnWidths(val)
}

const resizeEndCallback = (val: Record<string, number>) => {
setResizing(false)
onColumnsResize?.(val)
}

const { getRowSize, totalSize, getRowIndex } = useRowHeights({
value: data,
Expand All @@ -115,21 +137,48 @@ export const DataSheetGrid = React.memo(

// Width and height of the scrollable area
const { width, height } = useResizeDetector({
skipOnMount: true,
targetRef: outerRef,
refreshMode: 'throttle',
refreshRate: 100,
})

const { width: innerWidth } = useResizeDetector({
skipOnMount: true,
targetRef: innerRef,
refreshMode: 'throttle',
refreshRate: 100,
})

const adjustedDisplayHeight = horizontalScroll
? displayHeight + scrollbarSize
: displayHeight

useEffect(() => {
if (innerWidth && width && width < innerWidth) {
setHorizontalScroll(true)
} else {
setHorizontalScroll(false)
}
}, [innerWidth, setHorizontalScroll, width])

setHeightDiff(height ? displayHeight - height : 0)

const edges = useEdges(outerRef, width, height)

useEffect(() => {
if (initialColumnWidths) {
setResizedColumnWidths(initialColumnWidths)
}
}, [initialColumnWidths])

const {
fullWidth,
totalWidth: contentWidth,
columnsMap,
columnWidths,
columnRights,
} = useColumnWidths(columns, width)
} = useColumnWidths(columns, width, resizedColumnWidths)

// x,y coordinates of the right click
const [contextMenu, setContextMenu] = useState<{
Expand Down Expand Up @@ -1766,84 +1815,98 @@ export const DataSheetGrid = React.memo(
])

return (
<div className={className} style={style}>
<div
ref={beforeTabIndexRef}
tabIndex={rawColumns.length && data.length ? 0 : undefined}
onFocus={(e) => {
e.target.blur()
setActiveCell({ col: 0, row: 0 })
}}
/>
<Grid
columns={columns}
outerRef={outerRef}
columnWidths={columnWidths}
hasStickyRightColumn={hasStickyRightColumn}
displayHeight={displayHeight}
data={data}
fullWidth={fullWidth}
headerRowHeight={headerRowHeight}
activeCell={activeCell}
innerRef={innerRef}
rowHeight={getRowSize}
rowKey={rowKey}
selection={selection}
rowClassName={rowClassName}
editing={editing}
getContextMenuItems={getContextMenuItems}
setRowData={setRowData}
deleteRows={deleteRows}
insertRowAfter={insertRowAfter}
duplicateRows={duplicateRows}
stopEditing={stopEditing}
cellClassName={cellClassName}
onScroll={onScroll}
>
<SelectionRect
columnRights={columnRights}
<ColumnWidthsContext.Provider
value={{
columnWidths,
initialColumnWidths,
columnsMap,
onColumnsResize: resizeEndCallback,
resizeCallback: resizeCallback,
resizedColumnWidths,
}}
>
<div className={className} style={style}>
<div
ref={beforeTabIndexRef}
tabIndex={rawColumns.length && data.length ? 0 : undefined}
onFocus={(e) => {
e.target.blur()
setActiveCell({ col: 0, row: 0 })
}}
/>
<Grid
columns={columns}
outerRef={outerRef}
columnWidths={columnWidths}
activeCell={activeCell}
selection={selection}
hasStickyRightColumn={hasStickyRightColumn}
displayHeight={adjustedDisplayHeight}
data={data}
fullWidth={fullWidth}
headerRowHeight={headerRowHeight}
activeCell={activeCell}
innerRef={innerRef}
rowHeight={getRowSize}
hasStickyRightColumn={hasStickyRightColumn}
dataLength={data.length}
viewHeight={height}
viewWidth={width}
contentWidth={fullWidth ? undefined : contentWidth}
edges={edges}
rowKey={rowKey}
selection={selection}
rowClassName={rowClassName}
editing={editing}
isCellDisabled={isCellDisabled}
expandSelection={expandSelection}
/>
</Grid>
<div
ref={afterTabIndexRef}
tabIndex={rawColumns.length && data.length ? 0 : undefined}
onFocus={(e) => {
e.target.blur()
setActiveCell({
col: columns.length - (hasStickyRightColumn ? 3 : 2),
row: data.length - 1,
})
}}
/>
{!lockRows && AddRowsComponent && (
<AddRowsComponent
addRows={(count) => insertRowAfter(data.length - 1, count)}
/>
)}
{contextMenu && contextMenuItems.length > 0 && (
<ContextMenuComponent
clientX={contextMenu.x}
clientY={contextMenu.y}
cursorIndex={contextMenu.cursorIndex}
items={contextMenuItems}
close={() => setContextMenu(null)}
getContextMenuItems={getContextMenuItems}
setRowData={setRowData}
deleteRows={deleteRows}
insertRowAfter={insertRowAfter}
duplicateRows={duplicateRows}
stopEditing={stopEditing}
cellClassName={cellClassName}
onScroll={onScroll}
style={{
overflowY: resizing ? 'hidden' : 'auto',
}}
>
<SelectionRect
columnRights={columnRights}
columnWidths={columnWidths}
activeCell={activeCell}
selection={selection}
headerRowHeight={headerRowHeight}
rowHeight={getRowSize}
hasStickyRightColumn={hasStickyRightColumn}
dataLength={data.length}
viewHeight={height}
viewWidth={width}
contentWidth={fullWidth ? undefined : contentWidth}
edges={edges}
editing={editing}
isCellDisabled={isCellDisabled}
expandSelection={expandSelection}
/>
</Grid>
<div
ref={afterTabIndexRef}
tabIndex={rawColumns.length && data.length ? 0 : undefined}
onFocus={(e) => {
e.target.blur()
setActiveCell({
col: columns.length - (hasStickyRightColumn ? 3 : 2),
row: data.length - 1,
})
}}
/>
)}
</div>
{!lockRows && AddRowsComponent && (
<AddRowsComponent
addRows={(count) => insertRowAfter(data.length - 1, count)}
/>
)}
{contextMenu && contextMenuItems.length > 0 && (
<ContextMenuComponent
clientX={contextMenu.x}
clientY={contextMenu.y}
cursorIndex={contextMenu.cursorIndex}
items={contextMenuItems}
close={() => setContextMenu(null)}
/>
)}
</div>
</ColumnWidthsContext.Provider>
)
}
)
Expand Down
Loading