Update new row component, fix z-index issues, improve UX

This commit is contained in:
Andrew Kingston 2023-04-05 17:36:38 +02:00
parent 5ab0652c87
commit da2023974e
14 changed files with 190 additions and 153 deletions

View File

@ -1,5 +1,5 @@
<script> <script>
import { getContext } from "svelte" import { getContext, onMount } from "svelte"
import SheetCell from "./SheetCell.svelte" import SheetCell from "./SheetCell.svelte"
import { getCellRenderer } from "../lib/renderers" import { getCellRenderer } from "../lib/renderers"
@ -17,30 +17,36 @@
export let row export let row
export let cellId export let cellId
export let updateRow = rows.actions.updateRow export let updateRow = rows.actions.updateRow
export let showPlaceholder = false
export let invert = false export let invert = false
let api let api
let error let error
// Determine if the cell is editable const cellAPI = {
$: readonly = column.schema.autocolumn || (!$config.allowEditRows && row._id) focus: () => api?.focus(),
blur: () => api?.blur(),
// Build cell API onKeyDown: (...params) => api?.onKeyDown(...params),
$: cellAPI = {
...api,
isReadonly: () => readonly, isReadonly: () => readonly,
isRequired: () => !!column.schema.constraints?.presence, isRequired: () => !!column.schema.constraints?.presence,
validate: value => {
if (value === undefined) {
value = row[column.name]
}
if (cellAPI.isReadonly() && !(value == null || value === "")) {
// Ensure cell isn't readonly
error = "Auto columns can't be edited"
} else if (cellAPI.isRequired() && (value == null || value === "")) {
// Sanity check required fields
error = "Required field"
} else {
error = null
}
return error
},
updateValue: value => { updateValue: value => {
error = null
try { try {
if (cellAPI.isReadonly()) { cellAPI.validate(value)
// Ensure cell isn't readonly if (!error) {
error = "Auto columns can't be edited"
} else if (cellAPI.isRequired() && (value == null || value === "")) {
// Sanity check required fields
error = "Required field"
} else {
updateRow(row._id, column.name, value) updateRow(row._id, column.name, value)
} }
} catch (err) { } catch (err) {
@ -49,34 +55,33 @@
}, },
} }
// Determine if the cell is editable
$: readonly = column.schema.autocolumn || (!$config.allowEditRows && row._id)
// Update selected cell API if selected // Update selected cell API if selected
$: { $: {
if (selected) { if (selected) {
selectedCellAPI.set(cellAPI) selectedCellAPI.set(cellAPI)
} else { } else if (error) {
error = null // error = null
} }
} }
</script> </script>
<SheetCell {#key error}
{rowSelected} <SheetCell
{rowHovered} {rowSelected}
{rowIdx} {rowHovered}
{selected} {rowIdx}
{selectedUser} {selected}
{reorderSource} {selectedUser}
{reorderTarget} {reorderSource}
{error} {reorderTarget}
on:click={() => selectedCellId.set(cellId)} {error}
on:contextmenu={e => menu.actions.open(cellId, e)} on:click={() => selectedCellId.set(cellId)}
width={column.width} on:contextmenu={e => menu.actions.open(cellId, e)}
> width={column.width}
{#if !selected && showPlaceholder && (row[column.name] == null || row[column.name] === "")} >
<div class="placeholder">
{column.name}
</div>
{:else}
<svelte:component <svelte:component
this={getCellRenderer(column)} this={getCellRenderer(column)}
bind:api bind:api
@ -86,9 +91,10 @@
onChange={cellAPI.updateValue} onChange={cellAPI.updateValue}
{readonly} {readonly}
{invert} {invert}
placeholder="error"
/> />
{/if} </SheetCell>
</SheetCell> {/key}
<style> <style>
.placeholder { .placeholder {

View File

@ -179,7 +179,6 @@
display: flex; display: flex;
} }
.header-cell :global(.cell) { .header-cell :global(.cell) {
background: var(--background);
padding: 0 var(--cell-padding); padding: 0 var(--cell-padding);
gap: calc(2 * var(--cell-spacing)); gap: calc(2 * var(--cell-spacing));
} }

View File

@ -92,7 +92,7 @@
{/each} {/each}
</div> </div>
{#if editable} {#if editable}
<div class="arrow"> <div class="arrow" on:click={open}>
<Icon name="ChevronDown" /> <Icon name="ChevronDown" />
</div> </div>
{/if} {/if}

View File

@ -40,11 +40,7 @@
data-row={rowIdx} data-row={rowIdx}
> >
<slot /> <slot />
{#if selected && error} {#if selectedUser && !selected}
<div class="label">
{error}
</div>
{:else if selectedUser && !selected}
<div class="label"> <div class="label">
{selectedUser.label} {selectedUser.label}
</div> </div>

View File

@ -113,7 +113,6 @@
rowSelected={containsSelectedCell} rowSelected={containsSelectedCell}
width={$stickyColumn.width} width={$stickyColumn.width}
{updateRow} {updateRow}
showPlaceholder
invert invert
/> />
{/if} {/if}
@ -131,7 +130,6 @@
rowSelected={containsSelectedCell} rowSelected={containsSelectedCell}
width={column.width} width={column.width}
{updateRow} {updateRow}
showPlaceholder
invert invert
/> />
{/each} {/each}

View File

@ -1,6 +1,6 @@
<script> <script>
import SheetCell from "../cells/SheetCell.svelte" import SheetCell from "../cells/SheetCell.svelte"
import { getContext, onMount } from "svelte" import { getContext, onMount, tick } from "svelte"
import { Icon, Button } from "@budibase/bbui" import { Icon, Button } from "@budibase/bbui"
import SheetScrollWrapper from "./SheetScrollWrapper.svelte" import SheetScrollWrapper from "./SheetScrollWrapper.svelte"
import DataCell from "../cells/DataCell.svelte" import DataCell from "../cells/DataCell.svelte"
@ -16,7 +16,6 @@
dispatch, dispatch,
visibleColumns, visibleColumns,
rows, rows,
wheel,
showHScrollbar, showHScrollbar,
tableId, tableId,
subscribe, subscribe,
@ -34,12 +33,36 @@
$: scrollLeft = $scroll.left $: scrollLeft = $scroll.left
$: $tableId, (isAdding = false) $: $tableId, (isAdding = false)
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms))
const addRow = async () => { const addRow = async () => {
// Validate new row
let allColumns = []
if ($stickyColumn) {
allColumns.push($stickyColumn)
}
allColumns = allColumns.concat($visibleColumns)
for (let col of allColumns) {
$selectedCellId = `new-${col.name}`
await tick()
const error = $selectedCellAPI.validate()
if (error) {
return
}
}
// Create row
const savedRow = await rows.actions.addRow(newRow, 0) const savedRow = await rows.actions.addRow(newRow, 0)
if (savedRow && firstColumn) { if (savedRow && firstColumn) {
$selectedCellId = `${savedRow._id}-${firstColumn.name}` $selectedCellId = `${savedRow._id}-${firstColumn.name}`
isAdding = false isAdding = false
} }
// Reset scroll
scroll.set({
left: 0,
top: 0,
})
} }
const cancel = () => { const cancel = () => {
@ -47,13 +70,14 @@
} }
const startAdding = () => { const startAdding = () => {
scroll.set({
left: 0,
top: 0,
})
newRow = {} newRow = {}
isAdding = true isAdding = true
if (firstColumn) { if (firstColumn) {
$selectedCellId = `new-${firstColumn.name}` $selectedCellId = `new-${firstColumn.name}`
setTimeout(() => {
$selectedCellAPI?.focus()
}, 100)
} }
} }
@ -79,59 +103,61 @@
on:mouseenter={() => ($hoveredRowId = "new")} on:mouseenter={() => ($hoveredRowId = "new")}
on:mouseleave={() => ($hoveredRowId = null)} on:mouseleave={() => ($hoveredRowId = null)}
> >
<SheetScrollWrapper scrollHorizontally={false} scrollVertically={false}> <div
<div class="sticky-column"
class="sticky-column" style="flex: 0 0 {width}px"
style="flex: 0 0 {width}px" class:scrolled={scrollLeft > 0}
class:scrolled={scrollLeft > 0} >
<SheetCell
width={gutterWidth}
{rowHovered}
rowSelected={containsSelectedCell}
> >
<SheetCell <div class="gutter">
width={gutterWidth} <div class="number">1</div>
{#if $config.allowExpandRows}
<Icon
name="Maximize"
size="S"
hoverable
on:click={addViaModal}
/>
{/if}
</div>
</SheetCell>
{#if $stickyColumn}
{@const cellId = `new-${$stickyColumn.name}`}
<DataCell
{cellId}
column={$stickyColumn}
row={newRow}
{rowHovered} {rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell} rowSelected={containsSelectedCell}
> width={$stickyColumn.width}
<div class="gutter"> {updateRow}
<div class="number">1</div> rowIdx={0}
{#if $config.allowExpandRows} />
<Icon {/if}
name="Maximize" </div>
size="S"
hoverable
on:click={addViaModal}
/>
{/if}
</div>
</SheetCell>
{#if $stickyColumn}
{@const cellId = `new-${$stickyColumn.name}`}
<DataCell
{cellId}
column={$stickyColumn}
row={newRow}
{rowHovered}
selected={$selectedCellId === cellId}
rowSelected={containsSelectedCell}
width={$stickyColumn.width}
{updateRow}
/>
{/if}
</div>
</SheetScrollWrapper>
<SheetScrollWrapper scrollVertically={false}> <SheetScrollWrapper scrollVertically={false} foo>
<div class="row"> <div class="row">
{#each $renderedColumns as column} {#each $visibleColumns as column}
{@const cellId = `new-${column.name}`} {@const cellId = `new-${column.name}`}
<DataCell {#key cellId}
{cellId} <DataCell
{column} {cellId}
row={newRow} {column}
{rowHovered} row={newRow}
selected={$selectedCellId === cellId} {rowHovered}
rowSelected={containsSelectedCell} selected={$selectedCellId === cellId}
width={column.width} rowSelected={containsSelectedCell}
{updateRow} width={column.width}
/> {updateRow}
rowIdx={0}
/>
{/key}
{/each} {/each}
</div> </div>
</SheetScrollWrapper> </SheetScrollWrapper>
@ -149,8 +175,8 @@
pointer-events: none; pointer-events: none;
position: absolute; position: absolute;
top: var(--row-height); top: var(--row-height);
left: 0;
transform: translateY(-100%); transform: translateY(-100%);
z-index: 1;
transition: transform 130ms ease-out; transition: transform 130ms ease-out;
background: linear-gradient( background: linear-gradient(
to bottom, to bottom,

View File

@ -75,6 +75,7 @@
context = { ...context, ...createUserStores(context) } context = { ...context, ...createUserStores(context) }
context = { ...context, ...createMenuStores(context) } context = { ...context, ...createMenuStores(context) }
context = { ...context, ...createPaginationStores(context) } context = { ...context, ...createPaginationStores(context) }
context = { ...context, ...context }
// Reference some stores for local use // Reference some stores for local use
const { isResizing, isReordering, ui, loaded, rowHeight } = context const { isResizing, isReordering, ui, loaded, rowHeight } = context
@ -125,13 +126,14 @@
<HeaderRow /> <HeaderRow />
<SheetBody /> <SheetBody />
</div> </div>
{#if $config.allowAddRows} <div class="overlays">
<!-- <NewRow />--> {#if $config.allowAddRows}
<NewRowTop /> <NewRowTop />
{/if} {/if}
<ResizeOverlay /> <ResizeOverlay />
<ScrollOverlay /> <ScrollOverlay />
<MenuOverlay /> <MenuOverlay />
</div>
</div> </div>
</div> </div>
{/if} {/if}
@ -215,4 +217,9 @@
.controls-right { .controls-right {
gap: 12px; gap: 12px;
} }
/* Overlays */
.overlays {
z-index: 10;
}
</style> </style>

View File

@ -18,13 +18,20 @@
export let scrollVertically = true export let scrollVertically = true
export let scrollHorizontally = true export let scrollHorizontally = true
export let wheelInteractive = true export let wheelInteractive = true
export let foo = false
$: hiddenWidths = calculateHiddenWidths($renderedColumns) $: hiddenWidths = calculateHiddenWidths($renderedColumns)
$: style = generateStyle($scroll, $rowHeight, hiddenWidths) $: style = generateStyle($scroll, $rowHeight, hiddenWidths, foo)
const generateStyle = (scroll, rowHeight, hiddenWidths) => { const generateStyle = (scroll, rowHeight, hiddenWidths, foo) => {
const offsetX = scrollHorizontally ? -1 * scroll.left + hiddenWidths : 0 let offsetX, offsetY
const offsetY = scrollVertically ? -1 * (scroll.top % rowHeight) : 0 if (!foo) {
offsetX = scrollHorizontally ? -1 * scroll.left + hiddenWidths : 0
offsetY = scrollVertically ? -1 * (scroll.top % rowHeight) : 0
} else {
offsetX = scrollHorizontally ? -1 * scroll.left : 0
offsetY = scrollVertically ? -1 * scroll.top : 0
}
return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);` return `transform: translate3d(${offsetX}px, ${offsetY}px, 0);`
} }
@ -63,8 +70,8 @@
// Update state // Update state
scroll.set({ scroll.set({
left: newScrollLeft, left: scrollHorizontally ? newScrollLeft : left,
top: newScrollTop, top: scrollVertically ? newScrollTop : top,
}) })
// Hover row under cursor // Hover row under cursor

View File

@ -160,16 +160,14 @@
.sticky-column { .sticky-column {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
position: relative;
border-right: var(--cell-border);
z-index: 3;
} }
/* Add shadow when scrolled */ /* Add shadow when scrolled */
.sticky-column.scrolled :global(.cell:last-child:after) { .sticky-column.scrolled {
/*content: " ";*/ box-shadow: 0 0 8px -2px rgba(0, 0, 0, 0.2);
/*position: absolute;*/
/*width: 10px;*/
/*height: 100%;*/
/*left: 100%;*/
/*background: linear-gradient(to right, rgba(0, 0, 0, 0.08), transparent);*/
} }
/* Don't show borders between cells in the sticky column */ /* Don't show borders between cells in the sticky column */
@ -178,8 +176,7 @@
} }
.header { .header {
position: relative; z-index: 1;
z-index: 3;
} }
.header :global(.cell) { .header :global(.cell) {
background: var(--spectrum-global-color-gray-100); background: var(--spectrum-global-color-gray-100);
@ -192,7 +189,6 @@
} }
.content { .content {
position: relative; position: relative;
z-index: 1;
flex: 1 1 auto; flex: 1 1 auto;
} }

View File

@ -57,7 +57,6 @@
<style> <style>
.menu { .menu {
z-index: 1;
position: absolute; position: absolute;
background: var(--cell-background); background: var(--cell-background);
border: var(--cell-border); border: var(--cell-border);
@ -65,5 +64,6 @@
border-radius: 4px; border-radius: 4px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
box-shadow: 0 0 32px 0 rgba(0, 0, 0, 0.1);
} }
</style> </style>

View File

@ -51,7 +51,6 @@
.resize-slider { .resize-slider {
position: absolute; position: absolute;
top: 0; top: 0;
z-index: 2;
height: var(--row-height); height: var(--row-height);
opacity: 0; opacity: 0;
padding: 0 8px; padding: 0 8px;

View File

@ -109,7 +109,6 @@
background: var(--spectrum-global-color-gray-500); background: var(--spectrum-global-color-gray-500);
opacity: 0.7; opacity: 0.7;
border-radius: 4px; border-radius: 4px;
z-index: 1;
transition: opacity 130ms ease-out; transition: opacity 130ms ease-out;
} }
div:hover { div:hover {

View File

@ -1,4 +1,5 @@
import { derived, get } from "svelte/store" import { derived, get } from "svelte/store"
import { tick } from "svelte"
export const createMaxScrollStores = context => { export const createMaxScrollStores = context => {
const { const {
@ -88,65 +89,68 @@ export const createMaxScrollStores = context => {
}) })
// Ensure the selected cell is visible // Ensure the selected cell is visible
selectedCellRow.subscribe(row => { selectedCellId.subscribe(async $selectedCellId => {
if (!row) { await tick()
return const $selectedCellRow = get(selectedCellRow)
}
const $scroll = get(scroll) const $scroll = get(scroll)
const $bounds = get(bounds) const $bounds = get(bounds)
const $rowHeight = get(rowHeight) const $rowHeight = get(rowHeight)
const scrollBarOffset = 16 const verticalOffset = $rowHeight * 1.5
// Ensure row is not below bottom of screen // Ensure vertical position is viewable
const rowYPos = row.__idx * $rowHeight if ($selectedCellRow) {
const bottomCutoff = // Ensure row is not below bottom of screen
$scroll.top + $bounds.height - $rowHeight - scrollBarOffset const rowYPos = $selectedCellRow.__idx * $rowHeight
let delta = rowYPos - bottomCutoff const bottomCutoff =
if (delta > 0) { $scroll.top + $bounds.height - $rowHeight - verticalOffset
scroll.update(state => ({ let delta = rowYPos - bottomCutoff
...state,
top: state.top + delta,
}))
}
// Ensure row is not above top of screen
else {
delta = $scroll.top - rowYPos
if (delta > 0) { if (delta > 0) {
scroll.update(state => ({ scroll.update(state => ({
...state, ...state,
top: Math.max(0, state.top - delta), top: state.top + delta,
})) }))
} }
// Ensure row is not above top of screen
else {
const delta = $scroll.top - rowYPos + verticalOffset
if (delta > 0) {
scroll.update(state => ({
...state,
top: Math.max(0, state.top - delta),
}))
}
}
} }
// Ensure horizontal position is viewable
// Check horizontal position of columns next // Check horizontal position of columns next
const $selectedCellId = get(selectedCellId)
const $visibleColumns = get(visibleColumns) const $visibleColumns = get(visibleColumns)
const columnName = $selectedCellId?.split("-")[1] const columnName = $selectedCellId?.split("-")[1]
const column = $visibleColumns.find(col => col.name === columnName) const column = $visibleColumns.find(col => col.name === columnName)
const horizontalOffset = 24
if (!column) { if (!column) {
return return
} }
// Ensure column is not cutoff on left edge // Ensure column is not cutoff on left edge
delta = $scroll.left - column.left let delta = $scroll.left - column.left + horizontalOffset
if (delta > 0) { if (delta > 0) {
scroll.update(state => ({ scroll.update(state => ({
...state, ...state,
left: state.left - delta, left: Math.max(0, state.left - delta),
})) }))
} }
// Ensure column is not cutoff on right edge // Ensure column is not cutoff on right edge
else { else {
const rightEdge = column.left + column.width const rightEdge = column.left + column.width
const rightBound = $bounds.width + $scroll.left const rightBound = $bounds.width + $scroll.left - horizontalOffset
delta = rightEdge - rightBound delta = rightEdge - rightBound
if (delta > 0) { if (delta > 0) {
scroll.update(state => ({ scroll.update(state => ({
...state, ...state,
left: state.left + delta + scrollBarOffset, left: state.left + delta,
})) }))
} }
} }

View File

@ -8,7 +8,7 @@ export const createUIStores = context => {
const selectedCellAPI = writable(null) const selectedCellAPI = writable(null)
const rowHeight = writable(36) const rowHeight = writable(36)
// Derive the row that contains the selected cell. // Derive the row that contains the selected cell
const selectedCellRow = derived( const selectedCellRow = derived(
[selectedCellId, rowLookupMap, rows], [selectedCellId, rowLookupMap, rows],
([$selectedCellId, $rowLookupMap, $rows]) => { ([$selectedCellId, $rowLookupMap, $rows]) => {