Skip to content

Commit

Permalink
Update TS to allow for cell edit and selection at same time
Browse files Browse the repository at this point in the history
Update app.py
  • Loading branch information
schloerke committed Jun 10, 2024
1 parent 062c36c commit eb029f2
Show file tree
Hide file tree
Showing 7 changed files with 95 additions and 29 deletions.
10 changes: 9 additions & 1 deletion js/data-frame/cell-edit-map.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type { CellState } from "./cell";
// enableMapSet();

export type CellEdit = {
// rowIndex: number;
// columnIndex: number;
value?: string;
state?: CellState;
errorTitle?: string;
Expand Down Expand Up @@ -37,10 +39,16 @@ export const useCellEditMap = () => {
const key = makeCellEditMapKey(rowIndex, columnIndex);
const obj = draft.get(key) ?? ({} as CellEdit);
obj_fn(obj);
// obj.rowIndex = rowIndex;
// obj.columnIndex = columnIndex;
draft.set(key, obj);
});
};
return { cellEditMap, setCellEditMap, setCellEditMapAtLoc } as const;
return {
cellEditMap,
// setCellEditMap,
setCellEditMapAtLoc,
} as const;
};

export const makeCellEditMapKey = (rowIndex: number, columnIndex: number) => {
Expand Down
21 changes: 14 additions & 7 deletions js/data-frame/cell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import React, {
} from "react";
import { CellEdit, SetCellEditMapAtLoc } from "./cell-edit-map";
import { updateCellsData } from "./data-update";
import { SelectionSet } from "./selection";
import type { PatchInfo } from "./types";

// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -82,6 +83,7 @@ interface TableBodyCellProps {
setData: (fn: (draft: unknown[][]) => void) => void;
cellEditInfo: CellEdit | undefined;
setCellEditMapAtLoc: SetCellEditMapAtLoc;
selection: SelectionSet<string, HTMLTableCellElement>;
}

export const TableBodyCell: FC<TableBodyCellProps> = ({
Expand All @@ -98,6 +100,7 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
cellEditInfo,
setData,
setCellEditMapAtLoc,
selection,
}) => {
const initialValue = cell.getValue() as
| string
Expand All @@ -121,11 +124,11 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
// * When editing a cell:
// * On esc key:
// * √ Restore prior value / state / error
// * Move focus from input to td
// * Move focus from input to td
// * On enter key:
// * √ Save value
// * √ Move to the cell below (or above w/ shift) and edit the new cell
// * Should shift+enter add a newline in a cell?
// * X Should shift+enter add a newline in a cell?
// * On tab key:
// * √ Save value
// * √ Move to the cell to the right (or left w/ shift) and edit the new cell
Expand All @@ -137,8 +140,8 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
// * When focused on a td:
// * Allow for arrow key navigation
// * Have enter key enter edit mode for a cell
// * When a td is focused, Have esc key move focus to the table
// * When table is focused, Have esc key blur the focus
// * When a td is focused, Have esc key move focus to the table
// * X When table is focused, Have esc key blur the focus
// TODO-barret-future; Combat edit mode being independent of selection mode
// * In row / column selection mode, allow for arrow key navigation by focusing on a single cell, not a TR
// * If a cell is focused,
Expand Down Expand Up @@ -168,14 +171,17 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
if (e.key !== "Escape") return;
// Prevent default behavior
e.preventDefault();
e.stopPropagation();

// Turn off editing and the _temp_ edit value
resetEditing();
selection.focusOffset(rowId, 0);
};
const handleTab = (e: ReactKeyboardEvent<HTMLTextAreaElement>) => {
if (e.key !== "Tab") return;
// Prevent default behavior
e.preventDefault();
e.stopPropagation();

const hasShift = e.shiftKey;

Expand Down Expand Up @@ -209,6 +215,7 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
if (e.key !== "Enter") return;
// Prevent default behavior
e.preventDefault();
e.stopPropagation();

const hasShift = e.shiftKey;

Expand Down Expand Up @@ -373,7 +380,7 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
// };
// useAutosizeTextArea(inputRef.current, value as string);

let onClick:
let onCellDoubleClick:
| ((e: ReactMouseEvent<HTMLTableCellElement>) => void)
| undefined = undefined;
let content: ReactElement | ReturnType<typeof flexRender> | undefined =
Expand Down Expand Up @@ -417,7 +424,7 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
// Only allow transition to edit mode if the cell can be edited
if (editCellsIsAllowed && !isHtmlColumn) {
addToTableCellClass("cell-editable");
onClick = (e: ReactMouseEvent<HTMLTableCellElement>) => {
onCellDoubleClick = (e: ReactMouseEvent<HTMLTableCellElement>) => {
// Do not prevent default or stop propagation here!
// Other methods need to be able to handle the event as well. e.g. `onBodyClick` above.
// e.preventDefault();
Expand Down Expand Up @@ -463,7 +470,7 @@ export const TableBodyCell: FC<TableBodyCellProps> = ({
return (
<td
ref={tdRef}
onClick={onClick}
onDoubleClick={onCellDoubleClick}
title={cellTitle}
className={tableCellClass}
>
Expand Down
32 changes: 30 additions & 2 deletions js/data-frame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,27 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({

const editCellsIsAllowed = payloadOptions["editable"] === true;

// const [cellBeingEdited, setCellBeingEdited] = useState<{
// rowIndex: number;
// columnIndex: number;
// } | null>(null);

const [isEditingCell, setIsEditingCell] = useState(false);
useEffect(() => {
for (const cellEdit of cellEditMap.values()) {
if (cellEdit.isEditing) {
// setCellBeingEdited({
// rowIndex: cellEdit.rowIndex,
// columnIndex: cellEdit.columnIndex,
// });
setIsEditingCell(true);
return;
}
}
// setCellBeingEdited(null);
setIsEditingCell(false);
}, [cellEditMap, setIsEditingCell]);

const coldefs = useMemo<ColumnDef<unknown[], unknown>[]>(
() =>
columns.map((colname, colIndex) => {
Expand Down Expand Up @@ -255,12 +276,18 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
const canMultiRowSelect = selectionModes.row !== SelectionModes._rowEnum.NONE;

const selection = useSelection<string, HTMLTableRowElement>({
isEditingCell,
selectionModes,
keyAccessor: (el) => {
console.log(el.dataset, el);
return el.dataset.key!;
},
focusOffset: (key, offset) => {
focusEscape: (el) => {
setTimeout(() => {
el?.blur();
containerRef.current?.focus();
}, 0);
},
focusOffset: (key, offset = 0) => {
const rowModel = table.getSortedRowModel();
let index = rowModel.rows.findIndex((row) => row.id === key);
if (index < 0) {
Expand Down Expand Up @@ -667,6 +694,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
cellEditInfo={cellEditInfo}
setData={setData}
setCellEditMapAtLoc={setCellEditMapAtLoc}
selection={selection}
></TableBodyCell>
);
})}
Expand Down
30 changes: 27 additions & 3 deletions js/data-frame/selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ export interface SelectionSet<TKey, TElement extends HTMLElement> {
onMouseDown: (event: React.MouseEvent<TElement, MouseEvent>) => void;
onKeyDown: (event: React.KeyboardEvent<TElement>) => void;
};
focusOffset: (start: TKey, offset: number) => TKey | null;
}

// Keep as strings (and not pointer types) as this is a shape defined by the python side
Expand Down Expand Up @@ -114,15 +115,20 @@ export function initSelectionModes(
}

export function useSelection<TKey, TElement extends HTMLElement>({
selectionModes: selectionModes,
isEditingCell,
selectionModes,
keyAccessor,
focusOffset,
focusEscape,
between,
}: {
// cellBeingEdited: { rowIndex: number; columnIndex: number } | null;
isEditingCell: boolean;
selectionModes: SelectionModes;
keyAccessor: (el: TElement) => TKey;
focusOffset: (start: TKey, offset: number) => TKey | null;
between?: (from: TKey, to: TKey) => ReadonlyArray<TKey>;
focusEscape: (el: TElement) => void;
between: (from: TKey, to: TKey) => ReadonlyArray<TKey>;
}): SelectionSet<TKey, TElement> {
const [selectedKeys, setSelectedKeys] = useState<ImmutableSet<TKey>>(
ImmutableSet.empty()
Expand Down Expand Up @@ -159,6 +165,9 @@ export function useSelection<TKey, TElement extends HTMLElement>({
};

const onKeyDown = (event: React.KeyboardEvent<TElement>): void => {
if (isEditingCell) {
return;
}
if (selectionModes.isNone()) {
return;
}
Expand All @@ -167,6 +176,17 @@ export function useSelection<TKey, TElement extends HTMLElement>({
const key = keyAccessor(el);
const selected = selectedKeys.has(key);

if (event.key === "Escape") {
focusEscape(el);
event.preventDefault();
return;
}

// For both row and rows, do not allow for alphanumeric keys to trigger edit mode.
// Only allow for this once the anchor is a single cell, such as region selection.
// For region selection, allow for alphanumeric keys to trigger edit mode of current cell.
// For region selection, allow for enter key to trigger edit mode of current cell.

if (selectionModes.row === SelectionModes._rowEnum.SINGLE) {
if (event.key === " " || event.key === "Enter") {
if (selectedKeys.has(key)) {
Expand Down Expand Up @@ -196,7 +216,7 @@ export function useSelection<TKey, TElement extends HTMLElement>({
}
};

return {
const selection = {
has(key: TKey): boolean {
return selectedKeys.has(key);
},
Expand Down Expand Up @@ -224,7 +244,11 @@ export function useSelection<TKey, TElement extends HTMLElement>({
itemHandlers() {
return { onMouseDown, onKeyDown };
},

focusOffset,
};

return selection;
}

declare global {
Expand Down
8 changes: 4 additions & 4 deletions shiny/www/shared/py-shiny/data-frame/data-frame.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions shiny/www/shared/py-shiny/data-frame/data-frame.js.map

Large diffs are not rendered by default.

17 changes: 8 additions & 9 deletions tests/playwright/shiny/components/data_frame/edit/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@

# TODO-karan-test; Click outside the table. Tab to the column name, hit enter. Verify the table becomes sorted. Tab to an HTML column name, hit enter. Verify the sort does not update.

# TODO-karan-test; Enable rows selection and editable. Select (and verify) a row. Edit a cell content in that row. Verify the row is not focused. Hit escape key. Verify the cell value is not updated. Verify the row is focused. Hit escape key again. Verify the row is not focused. (Possibly verify the container div is focused?)
# TODO-karan-test; Enable rows selection and editable. Select (and verify) a row. Edit a cell content in that row. Click a cell in another row. Verify the new row is selected and focused. Verify the old row is not selected. Verify the old row cell value was updated.

# TODO-future; Can we maintain pre-processed value and use it within editing?
# A: Doesn't seem possible for now

Expand Down Expand Up @@ -89,19 +92,15 @@ def summary_data():
# @reactive.effect
# def _():
# print("Filters:", summary_data.filter())

# @reactive.effect
# def _():
# print("Sorting:", summary_data.sort())

# @reactive.effect
# def _():
# print("indices:", summary_data.data_view_rows())

# @reactive.effect
# def _():
# print("Data View:\n", summary_data.data_view(selected=False))

# @reactive.effect
# def _():
# print("Data View (selected):\n", summary_data.data_view(selected=True))
Expand Down Expand Up @@ -141,14 +140,14 @@ def upgrade_patch(
*,
patch: CellPatch,
):
from shinywidgets import output_widget
# from shinywidgets import output_widget

if len(summary_data.cell_patches()) == 0:
return ui.card(output_widget("country_detail_pop"), height="400px")
# if len(summary_data.cell_patches()) == 0:
# return ui.card(output_widget("country_detail_pop"), height="400px")

import time
# import time

time.sleep(2)
# time.sleep(2)
if len(summary_data.cell_patches()) > 3:
import random

Expand Down

0 comments on commit eb029f2

Please sign in to comment.