mirror of
https://github.com/overleaf/overleaf.git
synced 2024-11-07 20:31:06 -05:00
Merge pull request #14192 from overleaf/mj-table-keyboard-selection
[visual] move selection on keyboard buttons GitOrigin-RevId: 617be9188880a98c1803033c406501ac02083bbc
This commit is contained in:
parent
c332a65eb0
commit
2e944a6230
5 changed files with 187 additions and 15 deletions
|
@ -19,6 +19,7 @@ export const Cell: FC<{
|
|||
}> = ({ cellData, columnSpecification, rowIndex, columnIndex, row }) => {
|
||||
const { selection, setSelection } = useSelectionContext()
|
||||
const renderDiv = useRef<HTMLDivElement>(null)
|
||||
const cellRef = useRef<HTMLTableCellElement>(null)
|
||||
const {
|
||||
cellData: editingCellData,
|
||||
updateCellData: update,
|
||||
|
@ -64,7 +65,15 @@ export const Cell: FC<{
|
|||
return input.replaceAll(/(?<!\\)&/g, '\\&').replaceAll('\\\\', '')
|
||||
}
|
||||
|
||||
const hasFocus = selection?.contains({ row: rowIndex, cell: columnIndex })
|
||||
const isFocused =
|
||||
selection?.to.cell === columnIndex && selection?.to.row === rowIndex
|
||||
|
||||
useEffect(() => {
|
||||
if (isFocused && !editing && cellRef.current) {
|
||||
cellRef.current.focus()
|
||||
}
|
||||
}, [isFocused, editing])
|
||||
|
||||
useEffect(() => {
|
||||
const toDisplay = cellData.content.trim()
|
||||
if (renderDiv.current && !editing) {
|
||||
|
@ -97,6 +106,8 @@ export const Cell: FC<{
|
|||
)
|
||||
}
|
||||
|
||||
const inSelection = selection?.contains({ row: rowIndex, cell: columnIndex })
|
||||
|
||||
const onDoubleClick = useCallback(() => {
|
||||
startEditing(rowIndex, columnIndex, cellData.content.trim())
|
||||
}, [columnIndex, rowIndex, cellData, startEditing])
|
||||
|
@ -107,6 +118,7 @@ export const Cell: FC<{
|
|||
onDoubleClick={onDoubleClick}
|
||||
tabIndex={row.cells.length * rowIndex + columnIndex + 1}
|
||||
onMouseDown={onMouseDown}
|
||||
ref={cellRef}
|
||||
className={classNames('table-generator-cell', {
|
||||
'table-generator-cell-border-left': columnSpecification.borderLeft > 0,
|
||||
'table-generator-cell-border-right':
|
||||
|
@ -117,12 +129,14 @@ export const Cell: FC<{
|
|||
'alignment-center': columnSpecification.alignment === 'center',
|
||||
'alignment-right': columnSpecification.alignment === 'right',
|
||||
'alignment-paragraph': columnSpecification.alignment === 'paragraph',
|
||||
focused: hasFocus,
|
||||
'selection-edge-top': hasFocus && selection?.bordersTop(rowIndex),
|
||||
'selection-edge-bottom': hasFocus && selection?.bordersBottom(rowIndex),
|
||||
'selection-edge-left': hasFocus && selection?.bordersLeft(columnIndex),
|
||||
selected: inSelection,
|
||||
'selection-edge-top': inSelection && selection?.bordersTop(rowIndex),
|
||||
'selection-edge-bottom':
|
||||
inSelection && selection?.bordersBottom(rowIndex),
|
||||
'selection-edge-left':
|
||||
inSelection && selection?.bordersLeft(columnIndex),
|
||||
'selection-edge-right':
|
||||
hasFocus && selection?.bordersRight(columnIndex),
|
||||
inSelection && selection?.bordersRight(columnIndex),
|
||||
})}
|
||||
>
|
||||
{body}
|
||||
|
|
|
@ -54,6 +54,10 @@ export const EditingContextProvider: FC = ({ children }) => {
|
|||
if (!cellData) {
|
||||
return
|
||||
}
|
||||
if (!cellData.dirty) {
|
||||
setCellData(null)
|
||||
return
|
||||
}
|
||||
const { rowIndex, cellIndex, content } = cellData
|
||||
write(rowIndex, cellIndex, content)
|
||||
setCellData(null)
|
||||
|
|
|
@ -13,8 +13,14 @@ type TableCoordinate = {
|
|||
}
|
||||
|
||||
export class TableSelection {
|
||||
// eslint-disable-next-line no-useless-constructor
|
||||
constructor(public from: TableCoordinate, public to: TableCoordinate) {}
|
||||
public readonly from: TableCoordinate
|
||||
public readonly to: TableCoordinate
|
||||
|
||||
constructor(from: TableCoordinate, to?: TableCoordinate) {
|
||||
this.from = from
|
||||
this.to = to ?? from
|
||||
}
|
||||
|
||||
contains(point: TableCoordinate) {
|
||||
const { minX, maxX, minY, maxY } = this.normalized()
|
||||
|
||||
|
@ -64,6 +70,82 @@ export class TableSelection {
|
|||
const { minX, maxX, minY, maxY } = this.normalized()
|
||||
return cell >= minX && cell <= maxX && minY === 0 && maxY === totalRows - 1
|
||||
}
|
||||
|
||||
moveRight(totalColumns: number) {
|
||||
const newColumn = Math.min(totalColumns - 1, this.to.cell + 1)
|
||||
return new TableSelection({ row: this.to.row, cell: newColumn })
|
||||
}
|
||||
|
||||
moveLeft() {
|
||||
const newColumn = Math.max(0, this.to.cell - 1)
|
||||
return new TableSelection({ row: this.to.row, cell: newColumn })
|
||||
}
|
||||
|
||||
moveUp() {
|
||||
const newRow = Math.max(0, this.to.row - 1)
|
||||
return new TableSelection({ row: newRow, cell: this.to.cell })
|
||||
}
|
||||
|
||||
moveDown(totalRows: number) {
|
||||
const newRow = Math.min(totalRows - 1, this.to.row + 1)
|
||||
return new TableSelection({ row: newRow, cell: this.to.cell })
|
||||
}
|
||||
|
||||
moveNext(totalColumns: number, totalRows: number) {
|
||||
const { row, cell } = this.to
|
||||
if (cell === totalColumns - 1 && row === totalRows - 1) {
|
||||
return new TableSelection(this.to)
|
||||
}
|
||||
if (cell === totalColumns - 1) {
|
||||
return new TableSelection({ row: row + 1, cell: 0 })
|
||||
}
|
||||
return new TableSelection({ row, cell: cell + 1 })
|
||||
}
|
||||
|
||||
movePrevious(totalColumns: number) {
|
||||
if (this.to.cell === 0 && this.to.row === 0) {
|
||||
return new TableSelection(this.to)
|
||||
}
|
||||
if (this.to.cell === 0) {
|
||||
return new TableSelection({
|
||||
row: this.to.row - 1,
|
||||
cell: totalColumns - 1,
|
||||
})
|
||||
}
|
||||
return new TableSelection({ row: this.to.row, cell: this.to.cell - 1 })
|
||||
}
|
||||
|
||||
extendRight(totalColumns: number) {
|
||||
const newColumn = Math.min(totalColumns - 1, this.to.cell + 1)
|
||||
return new TableSelection(
|
||||
{ row: this.from.row, cell: this.from.cell },
|
||||
{ row: this.to.row, cell: newColumn }
|
||||
)
|
||||
}
|
||||
|
||||
extendLeft() {
|
||||
const newColumn = Math.max(0, this.to.cell - 1)
|
||||
return new TableSelection(
|
||||
{ row: this.from.row, cell: this.from.cell },
|
||||
{ row: this.to.row, cell: newColumn }
|
||||
)
|
||||
}
|
||||
|
||||
extendUp() {
|
||||
const newRow = Math.max(0, this.to.row - 1)
|
||||
return new TableSelection(
|
||||
{ row: this.from.row, cell: this.from.cell },
|
||||
{ row: newRow, cell: this.to.cell }
|
||||
)
|
||||
}
|
||||
|
||||
extendDown(totalRows: number) {
|
||||
const newRow = Math.min(totalRows - 1, this.to.row + 1)
|
||||
return new TableSelection(
|
||||
{ row: this.from.row, cell: this.from.cell },
|
||||
{ row: newRow, cell: this.to.cell }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const SelectionContext = createContext<
|
||||
|
|
|
@ -1,14 +1,55 @@
|
|||
import { FC, KeyboardEventHandler, useCallback } from 'react'
|
||||
import { FC, KeyboardEventHandler, useCallback, useMemo, useRef } from 'react'
|
||||
import { Row } from './row'
|
||||
import { ColumnSelector } from './selectors'
|
||||
import { useSelectionContext } from './contexts/selection-context'
|
||||
import {
|
||||
TableSelection,
|
||||
useSelectionContext,
|
||||
} from './contexts/selection-context'
|
||||
import { useEditingContext } from './contexts/editing-context'
|
||||
import { useTableContext } from './contexts/table-context'
|
||||
import { useCodeMirrorViewContext } from '../codemirror-editor'
|
||||
|
||||
type NavigationKey =
|
||||
| 'ArrowRight'
|
||||
| 'ArrowLeft'
|
||||
| 'ArrowUp'
|
||||
| 'ArrowDown'
|
||||
| 'Tab'
|
||||
|
||||
type NavigationMap = {
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
[key in NavigationKey]: [() => TableSelection, () => TableSelection]
|
||||
}
|
||||
|
||||
export const Table: FC = () => {
|
||||
const { selection, setSelection } = useSelectionContext()
|
||||
const { cellData, cancelEditing, startEditing } = useEditingContext()
|
||||
const { cellData, cancelEditing, startEditing, commitCellData } =
|
||||
useEditingContext()
|
||||
const { table: tableData } = useTableContext()
|
||||
const tableRef = useRef<HTMLTableElement>(null)
|
||||
const view = useCodeMirrorViewContext()
|
||||
|
||||
const navigation: NavigationMap = useMemo(
|
||||
() => ({
|
||||
ArrowRight: [
|
||||
() => selection!.moveRight(tableData.columns.length),
|
||||
() => selection!.extendRight(tableData.columns.length),
|
||||
],
|
||||
ArrowLeft: [() => selection!.moveLeft(), () => selection!.extendLeft()],
|
||||
ArrowUp: [() => selection!.moveUp(), () => selection!.extendUp()],
|
||||
ArrowDown: [
|
||||
() => selection!.moveDown(tableData.rows.length),
|
||||
() => selection!.extendDown(tableData.rows.length),
|
||||
],
|
||||
Tab: [
|
||||
() =>
|
||||
selection!.moveNext(tableData.columns.length, tableData.rows.length),
|
||||
() => selection!.movePrevious(tableData.columns.length),
|
||||
],
|
||||
}),
|
||||
[selection, tableData.columns.length, tableData.rows.length]
|
||||
)
|
||||
|
||||
const onKeyDown: KeyboardEventHandler = useCallback(
|
||||
event => {
|
||||
if (event.code === 'Enter') {
|
||||
|
@ -17,17 +58,40 @@ export const Table: FC = () => {
|
|||
if (!selection) {
|
||||
return
|
||||
}
|
||||
const cell =
|
||||
tableData.rows[selection.from.row].cells[selection.from.cell]
|
||||
startEditing(selection.from.row, selection.from.cell, cell.content)
|
||||
if (cellData) {
|
||||
commitCellData()
|
||||
return
|
||||
}
|
||||
const cell = tableData.rows[selection.to.row].cells[selection.to.cell]
|
||||
startEditing(selection.to.row, selection.to.cell, cell.content)
|
||||
setSelection(new TableSelection(selection.to, selection.to))
|
||||
} else if (event.code === 'Escape') {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (!cellData) {
|
||||
setSelection(null)
|
||||
view.focus()
|
||||
} else {
|
||||
cancelEditing()
|
||||
}
|
||||
} else if (Object.prototype.hasOwnProperty.call(navigation, event.code)) {
|
||||
const [defaultNavigation, shiftNavigation] =
|
||||
navigation[event.code as NavigationKey]
|
||||
if (cellData) {
|
||||
return
|
||||
}
|
||||
event.preventDefault()
|
||||
if (!selection) {
|
||||
setSelection(
|
||||
new TableSelection({ row: 0, cell: 0 }, { row: 0, cell: 0 })
|
||||
)
|
||||
return
|
||||
}
|
||||
if (event.shiftKey) {
|
||||
setSelection(shiftNavigation())
|
||||
} else {
|
||||
setSelection(defaultNavigation())
|
||||
}
|
||||
}
|
||||
},
|
||||
[
|
||||
|
@ -37,6 +101,9 @@ export const Table: FC = () => {
|
|||
setSelection,
|
||||
cancelEditing,
|
||||
startEditing,
|
||||
commitCellData,
|
||||
navigation,
|
||||
view,
|
||||
]
|
||||
)
|
||||
return (
|
||||
|
@ -45,6 +112,7 @@ export const Table: FC = () => {
|
|||
className="table-generator-table"
|
||||
onKeyDown={onKeyDown}
|
||||
tabIndex={-1}
|
||||
ref={tableRef}
|
||||
>
|
||||
<thead>
|
||||
<tr>
|
||||
|
|
|
@ -41,10 +41,14 @@
|
|||
border-bottom-width: @table-generator-active-border-width;
|
||||
}
|
||||
|
||||
.table-generator-cell.focused {
|
||||
.table-generator-cell.selected {
|
||||
background-color: @blue-10;
|
||||
}
|
||||
|
||||
.table-generator-cell:focus-visible {
|
||||
outline: 2px dotted @table-generator-focus-border-color;
|
||||
}
|
||||
|
||||
.table-generator-cell {
|
||||
&.selection-edge-top {
|
||||
--shadow-top: 0 @table-generator-focus-negative-border-width 0
|
||||
|
|
Loading…
Reference in a new issue