Merge pull request #10695 from Budibase/grid-feedback

Grid enhancements from user feedback
This commit is contained in:
Andrew Kingston 2023-05-26 09:36:04 +01:00 committed by GitHub
commit 3a672b3660
15 changed files with 142 additions and 38 deletions

View File

@ -37,6 +37,9 @@
.boolean-cell { .boolean-cell {
padding: 2px var(--cell-padding); padding: 2px var(--cell-padding);
pointer-events: none; pointer-events: none;
flex: 1 1 auto;
display: flex;
justify-content: center;
} }
.boolean-cell.editable { .boolean-cell.editable {
pointer-events: all; pointer-events: all;

View File

@ -11,6 +11,7 @@
export let selected export let selected
export let rowFocused export let rowFocused
export let rowIdx export let rowIdx
export let topRow = false
export let focused export let focused
export let selectedUser export let selectedUser
export let column export let column
@ -68,6 +69,7 @@
{highlighted} {highlighted}
{selected} {selected}
{rowIdx} {rowIdx}
{topRow}
{focused} {focused}
{selectedUser} {selectedUser}
{readonly} {readonly}

View File

@ -6,6 +6,7 @@
export let selectedUser = null export let selectedUser = null
export let error = null export let error = null
export let rowIdx export let rowIdx
export let topRow = false
export let defaultHeight = false export let defaultHeight = false
export let center = false export let center = false
export let readonly = false export let readonly = false
@ -31,13 +32,14 @@
class:readonly class:readonly
class:default-height={defaultHeight} class:default-height={defaultHeight}
class:selected-other={selectedUser != null} class:selected-other={selectedUser != null}
class:alt={rowIdx % 2 === 1}
class:top={topRow}
on:focus on:focus
on:mousedown on:mousedown
on:mouseup on:mouseup
on:click on:click
on:contextmenu on:contextmenu
{style} {style}
data-row={rowIdx}
> >
{#if error} {#if error}
<div class="label"> <div class="label">
@ -70,6 +72,9 @@
width: 0; width: 0;
--cell-color: transparent; --cell-color: transparent;
} }
.cell.alt {
--cell-background: var(--cell-background-alt);
}
.cell.default-height { .cell.default-height {
height: var(--default-row-height); height: var(--default-row-height);
} }
@ -98,8 +103,8 @@
.cell.selected-other:not(.focused):after { .cell.selected-other:not(.focused):after {
border-radius: 0 2px 2px 2px; border-radius: 0 2px 2px 2px;
} }
.cell[data-row="0"].error:after, .cell.top.error:after,
.cell[data-row="0"].selected-other:not(.focused):after { .cell.top.selected-other:not(.focused):after {
border-radius: 2px 2px 2px 0; border-radius: 2px 2px 2px 0;
} }
@ -152,7 +157,7 @@
overflow: hidden; overflow: hidden;
user-select: none; user-select: none;
} }
.cell[data-row="0"] .label { .cell.top .label {
bottom: auto; bottom: auto;
top: 100%; top: 100%;
border-radius: 0 2px 2px 2px; border-radius: 0 2px 2px 2px;

View File

@ -21,16 +21,7 @@
svelteDispatch("select") svelteDispatch("select")
const id = row?._id const id = row?._id
if (id) { if (id) {
selectedRows.update(state => { selectedRows.actions.toggleRow(id)
let newState = {
...state,
[id]: !state[id],
}
if (!newState[id]) {
delete newState[id]
}
return newState
})
} }
} }
@ -47,6 +38,7 @@
highlighted={rowFocused || rowHovered} highlighted={rowFocused || rowHovered}
selected={rowSelected} selected={rowSelected}
{defaultHeight} {defaultHeight}
rowIdx={row?.__idx}
> >
<div class="gutter"> <div class="gutter">
{#if $$slots.default} {#if $$slots.default}

View File

@ -196,7 +196,11 @@
<MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}> <MenuItem disabled={!canMoveRight} icon="ChevronRight" on:click={moveRight}>
Move right Move right
</MenuItem> </MenuItem>
<MenuItem icon="VisibilityOff" on:click={hideColumn}>Hide column</MenuItem> <MenuItem
disabled={idx === "sticky"}
icon="VisibilityOff"
on:click={hideColumn}>Hide column</MenuItem
>
</Menu> </Menu>
</Popover> </Popover>

View File

@ -41,6 +41,7 @@
export let allowExpandRows = true export let allowExpandRows = true
export let allowEditRows = true export let allowEditRows = true
export let allowDeleteRows = true export let allowDeleteRows = true
export let stripeRows = false
// Unique identifier for DOM nodes inside this instance // Unique identifier for DOM nodes inside this instance
const rand = Math.random() const rand = Math.random()
@ -55,6 +56,7 @@
allowExpandRows, allowExpandRows,
allowEditRows, allowEditRows,
allowDeleteRows, allowDeleteRows,
stripeRows,
}) })
// Build up context // Build up context
@ -90,6 +92,7 @@
allowExpandRows, allowExpandRows,
allowEditRows, allowEditRows,
allowDeleteRows, allowDeleteRows,
stripeRows,
}) })
// Set context for children to consume // Set context for children to consume
@ -107,6 +110,7 @@
id="grid-{rand}" id="grid-{rand}"
class:is-resizing={$isResizing} class:is-resizing={$isResizing}
class:is-reordering={$isReordering} class:is-reordering={$isReordering}
class:stripe={$config.stripeRows}
style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};" style="--row-height:{$rowHeight}px; --default-row-height:{DefaultRowHeight}px; --gutter-width:{GutterWidth}px; --max-cell-render-height:{MaxCellRenderHeight}px; --max-cell-render-width-overflow:{MaxCellRenderWidthOverflow}px; --content-lines:{$contentLines};"
> >
<div class="controls"> <div class="controls">
@ -169,6 +173,7 @@
/* Variables */ /* Variables */
--cell-background: var(--spectrum-global-color-gray-50); --cell-background: var(--spectrum-global-color-gray-50);
--cell-background-hover: var(--spectrum-global-color-gray-100); --cell-background-hover: var(--spectrum-global-color-gray-100);
--cell-background-alt: var(--cell-background);
--cell-padding: 8px; --cell-padding: 8px;
--cell-spacing: 4px; --cell-spacing: 4px;
--cell-border: 1px solid var(--spectrum-global-color-gray-200); --cell-border: 1px solid var(--spectrum-global-color-gray-200);
@ -185,6 +190,9 @@
.grid.is-reordering :global(*) { .grid.is-reordering :global(*) {
cursor: grabbing !important; cursor: grabbing !important;
} }
.grid.stripe {
--cell-background-alt: var(--spectrum-global-color-gray-75);
}
.grid-data-outer, .grid-data-outer,
.grid-data-inner { .grid-data-inner {

View File

@ -36,7 +36,11 @@
<div bind:this={body} class="grid-body"> <div bind:this={body} class="grid-body">
<GridScrollWrapper scrollHorizontally scrollVertically wheelInteractive> <GridScrollWrapper scrollHorizontally scrollVertically wheelInteractive>
{#each $renderedRows as row, idx} {#each $renderedRows as row, idx}
<GridRow {row} {idx} invertY={idx >= $rowVerticalInversionIndex} /> <GridRow
{row}
top={idx === 0}
invertY={idx >= $rowVerticalInversionIndex}
/>
{/each} {/each}
{#if $config.allowAddRows && $renderedColumns.length} {#if $config.allowAddRows && $renderedColumns.length}
<div <div

View File

@ -3,7 +3,7 @@
import DataCell from "../cells/DataCell.svelte" import DataCell from "../cells/DataCell.svelte"
export let row export let row
export let idx export let top = false
export let invertY = false export let invertY = false
const { const {
@ -41,7 +41,8 @@
invertX={columnIdx >= $columnHorizontalInversionIndex} invertX={columnIdx >= $columnHorizontalInversionIndex}
highlighted={rowHovered || rowFocused || reorderSource === column.name} highlighted={rowHovered || rowFocused || reorderSource === column.name}
selected={rowSelected} selected={rowSelected}
rowIdx={idx} rowIdx={row.__idx}
topRow={top}
focused={$focusedCellId === cellId} focused={$focusedCellId === cellId}
selectedUser={$selectedCellMap[cellId]} selectedUser={$selectedCellMap[cellId]}
width={column.width} width={column.width}

View File

@ -61,7 +61,7 @@
border-right: var(--cell-border); border-right: var(--cell-border);
border-bottom: var(--cell-border); border-bottom: var(--cell-border);
background: var(--spectrum-global-color-gray-100); background: var(--spectrum-global-color-gray-100);
z-index: 20; z-index: 1;
} }
.add:hover { .add:hover {
background: var(--spectrum-global-color-gray-200); background: var(--spectrum-global-color-gray-200);

View File

@ -38,7 +38,7 @@
padding: 2px 6px; padding: 2px 6px;
font-size: 12px; font-size: 12px;
font-weight: 600; font-weight: 600;
background-color: var(--spectrum-global-color-gray-200); background-color: var(--spectrum-global-color-gray-300);
color: var(--spectrum-global-color-gray-700); color: var(--spectrum-global-color-gray-700);
border-radius: 4px; border-radius: 4px;
text-align: center; text-align: center;

View File

@ -167,7 +167,7 @@
focused={$focusedCellId === cellId} focused={$focusedCellId === cellId}
width={$stickyColumn.width} width={$stickyColumn.width}
{updateValue} {updateValue}
rowIdx={0} topRow={offset === 0}
{invertY} {invertY}
> >
{#if $stickyColumn?.schema?.autocolumn} {#if $stickyColumn?.schema?.autocolumn}
@ -193,7 +193,7 @@
row={newRow} row={newRow}
focused={$focusedCellId === cellId} focused={$focusedCellId === cellId}
width={column.width} width={column.width}
rowIdx={0} topRow={offset === 0}
invertX={columnIdx >= $columnHorizontalInversionIndex} invertX={columnIdx >= $columnHorizontalInversionIndex}
{invertY} {invertY}
> >
@ -219,7 +219,7 @@
<Button size="M" secondary newStyles on:click={clear}> <Button size="M" secondary newStyles on:click={clear}>
<div class="button-with-keys"> <div class="button-with-keys">
Cancel Cancel
<KeyboardShortcut overlay keybind="Esc" /> <KeyboardShortcut keybind="Esc" />
</div> </div>
</Button> </Button>
</div> </div>

View File

@ -82,7 +82,8 @@
{rowFocused} {rowFocused}
selected={rowSelected} selected={rowSelected}
highlighted={rowHovered || rowFocused} highlighted={rowHovered || rowFocused}
rowIdx={idx} rowIdx={row.__idx}
topRow={idx === 0}
focused={$focusedCellId === cellId} focused={$focusedCellId === cellId}
selectedUser={$selectedCellMap[cellId]} selectedUser={$selectedCellMap[cellId]}
width={$stickyColumn.width} width={$stickyColumn.width}

View File

@ -224,10 +224,7 @@
if (!id || id === NewRowID) { if (!id || id === NewRowID) {
return return
} }
selectedRows.update(state => { selectedRows.actions.toggleRow(id)
state[id] = !state[id]
return state
})
} }
onMount(() => { onMount(() => {

View File

@ -4,9 +4,10 @@ const reorderInitialState = {
sourceColumn: null, sourceColumn: null,
targetColumn: null, targetColumn: null,
breakpoints: [], breakpoints: [],
initialMouseX: null,
scrollLeft: 0,
gridLeft: 0, gridLeft: 0,
width: 0,
latestX: 0,
increment: 0,
} }
export const createStores = () => { export const createStores = () => {
@ -23,14 +24,24 @@ export const createStores = () => {
} }
export const deriveStores = context => { export const deriveStores = context => {
const { reorder, columns, visibleColumns, scroll, bounds, stickyColumn, ui } = const {
context reorder,
columns,
visibleColumns,
scroll,
bounds,
stickyColumn,
ui,
maxScrollLeft,
} = context
let autoScrollInterval
let isAutoScrolling
// Callback when dragging on a colum header and starting reordering // Callback when dragging on a colum header and starting reordering
const startReordering = (column, e) => { const startReordering = (column, e) => {
const $visibleColumns = get(visibleColumns) const $visibleColumns = get(visibleColumns)
const $bounds = get(bounds) const $bounds = get(bounds)
const $scroll = get(scroll)
const $stickyColumn = get(stickyColumn) const $stickyColumn = get(stickyColumn)
ui.actions.blur() ui.actions.blur()
@ -51,9 +62,8 @@ export const deriveStores = context => {
sourceColumn: column, sourceColumn: column,
targetColumn: null, targetColumn: null,
breakpoints, breakpoints,
initialMouseX: e.clientX,
scrollLeft: $scroll.left,
gridLeft: $bounds.left, gridLeft: $bounds.left,
width: $bounds.width,
}) })
// Add listeners to handle mouse movement // Add listeners to handle mouse movement
@ -66,12 +76,44 @@ export const deriveStores = context => {
// Callback when moving the mouse when reordering columns // Callback when moving the mouse when reordering columns
const onReorderMouseMove = e => { const onReorderMouseMove = e => {
// Immediately handle the current position
const x = e.clientX
reorder.update(state => ({
...state,
latestX: x,
}))
considerReorderPosition()
// Check if we need to start auto-scrolling
const $reorder = get(reorder) const $reorder = get(reorder)
const proximityCutoff = 140
const speedFactor = 8
const rightProximity = Math.max(0, $reorder.gridLeft + $reorder.width - x)
const leftProximity = Math.max(0, x - $reorder.gridLeft)
if (rightProximity < proximityCutoff) {
const weight = proximityCutoff - rightProximity
const increment = (weight / proximityCutoff) * speedFactor
reorder.update(state => ({ ...state, increment }))
startAutoScroll()
} else if (leftProximity < proximityCutoff) {
const weight = -1 * (proximityCutoff - leftProximity)
const increment = (weight / proximityCutoff) * speedFactor
reorder.update(state => ({ ...state, increment }))
startAutoScroll()
} else {
stopAutoScroll()
}
}
// Actual logic to consider the current position and determine the new order
const considerReorderPosition = () => {
const $reorder = get(reorder)
const $scroll = get(scroll)
// Compute the closest breakpoint to the current position // Compute the closest breakpoint to the current position
let targetColumn let targetColumn
let minDistance = Number.MAX_SAFE_INTEGER let minDistance = Number.MAX_SAFE_INTEGER
const mouseX = e.clientX - $reorder.gridLeft + $reorder.scrollLeft const mouseX = $reorder.latestX - $reorder.gridLeft + $scroll.left
$reorder.breakpoints.forEach(point => { $reorder.breakpoints.forEach(point => {
const distance = Math.abs(point.x - mouseX) const distance = Math.abs(point.x - mouseX)
if (distance < minDistance) { if (distance < minDistance) {
@ -79,7 +121,6 @@ export const deriveStores = context => {
targetColumn = point.column targetColumn = point.column
} }
}) })
if (targetColumn !== $reorder.targetColumn) { if (targetColumn !== $reorder.targetColumn) {
reorder.update(state => ({ reorder.update(state => ({
...state, ...state,
@ -88,8 +129,35 @@ export const deriveStores = context => {
} }
} }
// Commences auto-scrolling in a certain direction, triggered when the mouse
// approaches the edges of the grid
const startAutoScroll = () => {
if (isAutoScrolling) {
return
}
isAutoScrolling = true
autoScrollInterval = setInterval(() => {
const $maxLeft = get(maxScrollLeft)
const { increment } = get(reorder)
scroll.update(state => ({
...state,
left: Math.max(0, Math.min($maxLeft, state.left + increment)),
}))
considerReorderPosition()
}, 10)
}
// Stops auto scrolling
const stopAutoScroll = () => {
isAutoScrolling = false
clearInterval(autoScrollInterval)
}
// Callback when stopping reordering columns // Callback when stopping reordering columns
const stopReordering = async () => { const stopReordering = async () => {
// Ensure auto-scrolling is stopped
stopAutoScroll()
// Swap position of columns // Swap position of columns
let { sourceColumn, targetColumn } = get(reorder) let { sourceColumn, targetColumn } = get(reorder)
moveColumn(sourceColumn, targetColumn) moveColumn(sourceColumn, targetColumn)

View File

@ -25,14 +25,33 @@ export const createStores = () => {
null null
) )
// Toggles whether a certain row ID is selected or not
const toggleSelectedRow = id => {
selectedRows.update(state => {
let newState = {
...state,
[id]: !state[id],
}
if (!newState[id]) {
delete newState[id]
}
return newState
})
}
return { return {
focusedCellId, focusedCellId,
focusedCellAPI, focusedCellAPI,
focusedRowId, focusedRowId,
previousFocusedRowId, previousFocusedRowId,
selectedRows,
hoveredRowId, hoveredRowId,
rowHeight, rowHeight,
selectedRows: {
...selectedRows,
actions: {
toggleRow: toggleSelectedRow,
},
},
} }
} }