Skip to content

Commit

Permalink
181846570 adjustable row height (#1700)
Browse files Browse the repository at this point in the history
* Adds cell-span styling wo user can add long text

* Fixes text styling when row height is greater than the default height

* Adds rowHeight as a property of the models

* Adds a row divider that can be used as a handle for changing row height

* limits the queryselector for the row element to the containing collection

* Styles overflow text in a grid cell

* Row height changes saves and restores

* chore: code review tweaks

* chore: fix cypress tests

---------

Co-authored-by: Kirk Swenson <[email protected]>
  • Loading branch information
eireland and kswenson authored Dec 24, 2024
1 parent 52f4562 commit 274d1a7
Show file tree
Hide file tree
Showing 13 changed files with 188 additions and 37 deletions.
2 changes: 1 addition & 1 deletion v3/cypress/support/elements/table-tile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const TableTileElements = {
[data-testid=codap-index-content-button]`)
},
openIndexMenuForRow(rowNum: number, collectionIndex = 1) {
this.getIndexCellInRow(rowNum, collectionIndex).click("top")
this.getIndexCellInRow(rowNum, collectionIndex).click()
},
getIndexMenu() {
return cy.get("[data-testid=index-menu-list]")
Expand Down
6 changes: 5 additions & 1 deletion v3/src/components/case-table/attribute-value-cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,12 @@ import { useDataSet } from "../../hooks/use-data-set"
import { symParent } from "../../models/data/data-set-types"
import { symDom, TRenderCellProps } from "./case-table-types"
import { renderAttributeValue } from "../case-tile-common/attribute-format-utils"
import { useCollectionTableModel } from "./use-collection-table-model"

export function AttributeValueCell({ column, row }: TRenderCellProps) {
const { data, metadata } = useDataSet()
const collectionTableModel = useCollectionTableModel()
const rowHeight = collectionTableModel?.rowHeight
const strValue = (data?.getStrValue(row.__id__, column.key) ?? "").trim()
const numValue = data?.getNumeric(row.__id__, column.key)
// if this is the first React render after performance rendering, add a
Expand All @@ -16,7 +19,8 @@ export function AttributeValueCell({ column, row }: TRenderCellProps) {
const isParentCollapsed = row[symParent] ? metadata?.isCollapsed(row[symParent]) : false
const { value, content } = isParentCollapsed
? { value: "", content: null }
: renderAttributeValue(strValue, numValue, false, data?.attrFromID(column.key), key)
: renderAttributeValue(strValue, numValue, false, data?.attrFromID(column.key),
key, rowHeight)
return (
<Tooltip label={value} h="20px" fontSize="12px" color="white" data-testid="case-table-data-tip"
openDelay={1000} placement="bottom" bottom="10px" left="15px">
Expand Down
11 changes: 10 additions & 1 deletion v3/src/components/case-table/case-table-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ export const CaseTableModel = TileContentModel
type: types.optional(types.literal(kCaseTableTileType), kCaseTableTileType),
// key is attribute id; value is width
columnWidths: types.map(types.number),
// key is collection id, value is row height for collection
rowHeights: types.map(types.number),
// Only used for serialization; volatile property used during run time
horizontalScrollOffset: 0
})
Expand All @@ -36,6 +38,9 @@ export const CaseTableModel = TileContentModel
columnWidth(attrId: string) {
return self.columnWidths.get(attrId)
},
getRowHeightForCollection(collectionId: string) {
return self.rowHeights.get(collectionId)
}
}))
.views(self => {
const collectionTableModels = new Map<string, CollectionTableModel>()
Expand All @@ -44,7 +49,8 @@ export const CaseTableModel = TileContentModel
getCollectionTableModel(collectionId: string) {
let collectionTableModel = collectionTableModels.get(collectionId)
if (!collectionTableModel) {
collectionTableModel = new CollectionTableModel(collectionId)
const rowHeight = self.getRowHeightForCollection(collectionId)
collectionTableModel = new CollectionTableModel(collectionId, rowHeight)
collectionTableModels.set(collectionId, collectionTableModel)
}
return collectionTableModel
Expand All @@ -63,6 +69,9 @@ export const CaseTableModel = TileContentModel
setColumnWidths(columnWidths: Map<string, number>) {
self.columnWidths.replace(columnWidths)
},
setRowHeightForCollection(collectionId: string, height: number) {
self.rowHeights.set(collectionId, height)
},
updateAfterSharedModelChanges(sharedModel?: ISharedModel) {
// TODO
},
Expand Down
3 changes: 0 additions & 3 deletions v3/src/components/case-table/case-table-shared.scss
Original file line number Diff line number Diff line change
@@ -1,11 +1,8 @@

$header-row-height: 30;
$header-row-height-px: #{$header-row-height}px;
$body-row-height: 18;
$body-row-height-px: #{$body-row-height}px;

// https://mattferderer.com/use-sass-variables-in-typescript-and-javascript
:export {
headerRowHeight: $header-row-height;
bodyRowHeight: $body-row-height;
}
7 changes: 6 additions & 1 deletion v3/src/components/case-table/case-table-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
CalculatedColumn, CellClickArgs, CellKeyDownArgs, CellSelectArgs, ColSpanArgs, Column, RenderCellProps,
RenderEditCellProps, Renderers, RenderHeaderCellProps, RenderRowProps, RowsChangeData
} from "react-data-grid"
import { DEBUG_CASE_IDS } from "../../lib/debug"
import { IGroupedCase, symFirstChild } from "../../models/data/data-set-types"

export const kCaseTableIdBase = "case-table"
Expand Down Expand Up @@ -55,7 +56,7 @@ export type OnScrollRowRangeIntoViewFn = (collectionId: string, rowIndices: numb
export const kInputRowKey = "__input__"

export const kDropzoneWidth = 30
export const kIndexColumnWidth = 52
export const kIndexColumnWidth = DEBUG_CASE_IDS ? 150 : 52
export const kDefaultColumnWidth = 80
export const kMinAutoColumnWidth = 50
export const kMaxAutoColumnWidth = 600
Expand All @@ -67,3 +68,7 @@ export const kCaseTableHeaderFont = `bold ${kCaseTableFontSize} ${kCaseTableFont
export const kCaseTableBodyFont = `${kCaseTableFontSize} ${kCaseTableFontFamily}`

export const kCaseTableDefaultWidth = 580
export const kDefaultRowHeaderHeight = 30
export const kDefaultRowHeight = 18
// used for row resizing
export const kSnapToLineHeight = 14
33 changes: 31 additions & 2 deletions v3/src/components/case-table/case-table.scss
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,14 @@ $table-body-font-size: 8pt;
}
}
}

.codap-index-cell-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
width: 100%;
position: absolute;
}
.rdg-cell {
// RDG.beta.17 moved all CSS inside @layers, which decreased their priority
// so that some of Chakra's CSS takes precedence, including the gridlines.
Expand Down Expand Up @@ -323,6 +330,19 @@ $table-body-font-size: 8pt;
background-color: var(--rdg-row-selected-background-color);
}

&.codap-data-cell.multi-line {
white-space: normal;
overflow-wrap: break-word;

.cell-span, .rdg-text-editor {
// https://css-tricks.com/almanac/properties/l/line-clamp/
line-height: 14.5px;
overflow: hidden;
text-overflow: ellipsis;
display: -webkit-box;
-webkit-box-orient: vertical;
}
}
&.codap-data-cell.rowId-__input__ {
cursor: text;
}
Expand All @@ -334,8 +354,10 @@ $table-body-font-size: 8pt;
height: 14px;
}
}

.cell-span {
width: 100%;
height: 100%;

&.numeric-format {
width: 100%;
Expand Down Expand Up @@ -393,6 +415,13 @@ $table-body-font-size: 8pt;
opacity: 30%;
}
}

.codap-row-divider {
height: 9px;
position: absolute;
cursor: row-resize;
z-index: 1;
}
}

.codap-menu-list {
Expand Down
17 changes: 9 additions & 8 deletions v3/src/components/case-table/collection-table-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,8 @@ import { action, computed, makeObservable, observable } from "mobx"
import { symParent } from "../../models/data/data-set-types"
import { getNumericCssVariable } from "../../utilities/css-utils"
import { uniqueId } from "../../utilities/js-utils"
import { IScrollOptions, TRow } from "./case-table-types"
import { IScrollOptions, kDefaultRowHeaderHeight, kDefaultRowHeight, TRow } from "./case-table-types"

const kDefaultRowHeaderHeight = 30
const kDefaultRowHeight = 18
const kDefaultRowCount = 12
const kDefaultGridHeight = kDefaultRowHeaderHeight + (kDefaultRowCount * kDefaultRowHeight)

Expand Down Expand Up @@ -47,8 +45,11 @@ export class CollectionTableModel {
// rows are passed directly to RDG for rendering
@observable.shallow rows: TRow[] = []

constructor(collectionId: string) {
@observable rowHeight

constructor(collectionId: string, rowHeight = kDefaultRowHeight) {
this.collectionId = collectionId
this.rowHeight = rowHeight

makeObservable(this)
}
Expand All @@ -61,10 +62,6 @@ export class CollectionTableModel {
return getNumericCssVariable(this.element, "--rdg-row-header-height") ?? kDefaultRowHeaderHeight
}

get rowHeight() {
return getNumericCssVariable(this.element, "--rdg-row-height") ?? kDefaultRowHeight
}

// visible height of the body of the grid, i.e. the rows excluding the header row
get gridBodyHeight() {
return (this.element?.getBoundingClientRect().height ?? kDefaultGridHeight) - kDefaultRowHeaderHeight
Expand Down Expand Up @@ -232,6 +229,10 @@ export class CollectionTableModel {
this.inputRowIndex = inputRowIndex
}

@action setRowHeight(rowHeight: number) {
this.rowHeight = rowHeight
}

@action syncScrollTopFromEvent(event: React.UIEvent<HTMLDivElement, UIEvent>) {
const { scrollTop } = event.currentTarget
this.lastScrollStep = this.scrollStep
Expand Down
13 changes: 7 additions & 6 deletions v3/src/components/case-table/collection-table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from "react"
import DataGrid, { CellKeyboardEvent, DataGridHandle } from "react-data-grid"
import { kCollectionTableBodyDropZoneBaseId } from "./case-table-drag-drop"
import {
kInputRowKey, OnScrollClosestRowIntoViewFn, OnScrollRowRangeIntoViewFn, OnTableScrollFn,
kDefaultRowHeight, kInputRowKey, OnScrollClosestRowIntoViewFn, OnScrollRowRangeIntoViewFn, OnTableScrollFn,
TCellKeyDownArgs, TRenderers, TRow
} from "./case-table-types"
import { CollectionTableSpacer } from "./collection-table-spacer"
Expand Down Expand Up @@ -75,6 +75,7 @@ export const CollectionTable = observer(function CollectionTable(props: IProps)
const [selectionStartRowIdx, setSelectionStartRowIdx] = useState<number | null>(null)
const initialPointerDownPosition = useRef({ x: 0, y: 0 })
const kPointerMovementThreshold = 3
const rowHeight = collectionTableModel?.rowHeight ?? kDefaultRowHeight

useEffect(function setGridElement() {
const element = gridRef.current?.element
Expand Down Expand Up @@ -279,8 +280,8 @@ export const CollectionTable = observer(function CollectionTable(props: IProps)

const startAutoScroll = useCallback((clientY: number) => {
const grid = gridRef.current?.element
const rowHeight = collectionTableModel?.rowHeight
if (!grid || !rowHeight) return
const rHeight = collectionTableModel?.rowHeight
if (!grid || !rHeight) return

const scrollSpeed = 50

Expand All @@ -291,11 +292,11 @@ export const CollectionTable = observer(function CollectionTable(props: IProps)
if (mouseY.current < top + kScrollMargin) {
grid.scrollTop -= scrollSpeed
const scrolledTop = grid.scrollTop
scrolledToRowIdx = Math.floor(scrolledTop / rowHeight)
scrolledToRowIdx = Math.floor(scrolledTop / rHeight)
} else if (mouseY.current > bottom - kScrollMargin) {
grid.scrollTop += scrollSpeed
const scrolledBottom = grid.scrollTop + grid.clientHeight - 1
scrolledToRowIdx = Math.floor(scrolledBottom / rowHeight)
scrolledToRowIdx = Math.floor(scrolledBottom / rHeight)

}
if (scrolledToRowIdx != null && selectionStartRowIdx != null && scrolledToRowIdx >= 0 &&
Expand Down Expand Up @@ -371,7 +372,7 @@ export const CollectionTable = observer(function CollectionTable(props: IProps)
<CollectionTitle onAddNewAttribute={handleAddNewAttribute} showCount={true} />
<DataGrid ref={gridRef} className="rdg-light" data-testid="collection-table-grid" renderers={renderers}
columns={columns} rows={rows} headerRowHeight={+styles.headerRowHeight} rowKeyGetter={rowKey}
rowHeight={+styles.bodyRowHeight} selectedRows={selectedRows} onSelectedRowsChange={setSelectedRows}
rowHeight={rowHeight} selectedRows={selectedRows} onSelectedRowsChange={setSelectedRows}
columnWidths={columnWidths} onColumnResize={handleColumnResize} onCellClick={handleCellClick}
onCellKeyDown={handleCellKeyDown} onRowsChange={handleRowsChange} onScroll={handleGridScroll}
onSelectedCellChange={handleSelectedCellChange}/>
Expand Down
86 changes: 86 additions & 0 deletions v3/src/components/case-table/row-divider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { clsx } from "clsx"
import { observer } from "mobx-react-lite"
import React, { useEffect, useRef, useState } from "react"
import { createPortal } from "react-dom"
import { useCollectionContext } from "../../hooks/use-collection-context"
import { kDefaultRowHeaderHeight, kDefaultRowHeight, kIndexColumnWidth, kSnapToLineHeight } from "./case-table-types"
import { useCaseTableModel } from "./use-case-table-model"
import { useCollectionTableModel } from "./use-collection-table-model"

const kTableRowDividerHeight = 13
const kTableDividerOffset = Math.floor(kTableRowDividerHeight / 2)
interface IRowDividerProps {
rowId: string
}
export const RowDivider = observer(function RowDivider({ rowId }: IRowDividerProps) {
const collectionTableModel = useCollectionTableModel()
const collectionId = useCollectionContext()
const collectionTable = collectionTableModel?.element
const caseTableModel = useCaseTableModel()
const rows = collectionTableModel?.rows
const isResizing = useRef(false)
const startY = useRef(0)
const getRowHeight = () => collectionTableModel?.rowHeight ?? kDefaultRowHeight
const initialHeight = useRef(getRowHeight())
const rowIdx = rows?.findIndex(row => row.__id__ === rowId)
const [rowElement, setRowElement] = useState<HTMLDivElement | null>(null)
const getRowBottom = () => {
if (rowIdx === 0) return getRowHeight()
else return (rowIdx && collectionTableModel?.getRowBottom(rowIdx)) ?? kDefaultRowHeight
}


useEffect(() => {
(rowIdx != null && collectionTable) &&
setRowElement(collectionTable.querySelector(`[aria-rowindex="${rowIdx + 2}"]`) as HTMLDivElement)
}, [collectionTable, rowIdx])

// allow the user to drag the divider
const handleMouseDown = (e: React.MouseEvent) => {
e.stopPropagation()
isResizing.current = true
startY.current = e.clientY
initialHeight.current = getRowHeight()
document.addEventListener("mousemove", handleMouseMove)
document.addEventListener("mouseup", handleMouseUp)
}

const handleMouseMove = (e: MouseEvent) => {
e.stopPropagation()
if (isResizing.current) {
const deltaY = e.clientY - startY.current
const tempNewHeight = Math.max(kDefaultRowHeight, initialHeight.current + deltaY)
const newHeight = kSnapToLineHeight * Math.round((tempNewHeight - 4) / kSnapToLineHeight) + 4
collectionTableModel?.setRowHeight(newHeight)
}
}

const handleMouseUp = (e: MouseEvent) => {
e.stopPropagation()
isResizing.current = false
caseTableModel?.applyModelChange(() => {
caseTableModel?.setRowHeightForCollection(collectionId, getRowHeight())
},
{
log: "Change row height",
undoStringKey: "DG.Undo.caseTable.changeRowHeight",
redoStringKey: "DG.Redo.caseTable.changeRowHeight"
}
)
document.removeEventListener("mousemove", handleMouseMove)
document.removeEventListener("mouseup", handleMouseUp)
}

const className = clsx("codap-row-divider")
const top = getRowBottom() + kDefaultRowHeaderHeight - kTableDividerOffset
const width = kIndexColumnWidth
return (
rowElement
? createPortal((
<div className={className} onMouseDown={handleMouseDown}
data-testid={`row-divider-${rowIdx}`} style={{top, width}}
/>
), rowElement)
: null
)
})
10 changes: 7 additions & 3 deletions v3/src/components/case-table/use-columns.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,20 +9,23 @@ import { getCollectionAttrs } from "../../models/data/data-set-utils"
import { mstReaction } from "../../utilities/mst-reaction"
import { isCaseEditable } from "../../utilities/plugin-utils"
import { AttributeValueCell } from "./attribute-value-cell"
import { kDefaultColumnWidth, TColumn } from "./case-table-types"
import { kDefaultColumnWidth, kDefaultRowHeight, TColumn } from "./case-table-types"
import CellTextEditor from "./cell-text-editor"
import ColorCellTextEditor from "./color-cell-text-editor"
import { ColumnHeader } from "./column-header"
import { useCollectionTableModel } from "./use-collection-table-model"

interface IUseColumnsProps {
data?: IDataSet
indexColumn?: TColumn
}
export const useColumns = ({ data, indexColumn }: IUseColumnsProps) => {
const caseMetadata = useCaseMetadata()
const collectionTableModel = useCollectionTableModel()
const parentCollection = useParentCollectionContext()
const collectionId = useCollectionContext()
const [columns, setColumns] = useState<TColumn[]>([])
const rowHeight = collectionTableModel?.rowHeight ?? kDefaultRowHeight

useEffect(() => {
// rebuild column definitions when referenced properties change
Expand All @@ -49,7 +52,8 @@ export const useColumns = ({ data, indexColumn }: IUseColumnsProps) => {
resizable: true,
headerCellClass: `codap-column-header`,
renderHeaderCell: ColumnHeader,
cellClass: row => clsx("codap-data-cell", `rowId-${row.__id__}`, {"formula-column": hasFormula}),
cellClass: row => clsx("codap-data-cell", `rowId-${row.__id__}`,
{"formula-column": hasFormula, "multi-line": rowHeight > kDefaultRowHeight}),
renderCell: AttributeValueCell,
editable: row => isCaseEditable(data, row.__id__),
renderEditCell: isEditable
Expand All @@ -64,7 +68,7 @@ export const useColumns = ({ data, indexColumn }: IUseColumnsProps) => {
},
{ name: "useColumns [rebuild columns]", equals: comparer.structural, fireImmediately: true }, data
)
}, [caseMetadata, collectionId, data, indexColumn, parentCollection])
}, [caseMetadata, collectionId, data, indexColumn, parentCollection, rowHeight])

return columns
}
Loading

0 comments on commit 274d1a7

Please sign in to comment.