Skip to content

Commit

Permalink
First pass at row selection with row indexes; Turn useSelection into …
Browse files Browse the repository at this point in the history
…a dict of params rather than many individual unnamed params
  • Loading branch information
schloerke committed Jun 5, 2024
1 parent c064eb7 commit d83f043
Show file tree
Hide file tree
Showing 6 changed files with 93 additions and 71 deletions.
66 changes: 33 additions & 33 deletions js/data-frame/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -243,18 +243,18 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({

// ### Row selection ###############################################################

const rowSelectionModes = initSelectionModes(selectionModesProp);
const selectionModes = initSelectionModes(selectionModesProp);

const canSelect = !rowSelectionModes.isNone();
const canMultiRowSelect =
rowSelectionModes.row !== SelectionModes._rowEnum.NONE;
const canSelect = !selectionModes.isNone();
const canMultiRowSelect = selectionModes.row !== SelectionModes._rowEnum.NONE;

const rowSelection = useSelection<string, HTMLTableRowElement>(
rowSelectionModes,
(el) => {
const selection = useSelection<string, HTMLTableRowElement>({
selectionModes,
keyAccessor: (el) => {
console.log(el.dataset, el);
return el.dataset.key!;
},
(key, offset) => {
focusOffset: (key, offset) => {
const rowModel = table.getSortedRowModel();
let index = rowModel.rows.findIndex((row) => row.id === key);
if (index < 0) {
Expand All @@ -274,9 +274,9 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
}, 0);
return targetKey;
},
(fromKey, toKey) =>
findKeysBetween(table.getSortedRowModel(), fromKey, toKey)
);
between: (fromKey, toKey) =>
findKeysBetween(table.getSortedRowModel(), fromKey, toKey),
});

useEffect(() => {
const handleCellSelection = (
Expand All @@ -288,13 +288,13 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
const cellSelection = event.detail.cellSelection;

if (cellSelection.type === "none") {
rowSelection.clear();
selection.clear();
return;
// } else if (cellSelection.type === "all") {
// rowSelection.setMultiple(rowData.map((_, i) => String(i)));
// return;
} else if (cellSelection.type === "row") {
rowSelection.setMultiple(cellSelection.rows.map(String));
selection.setMultiple(cellSelection.rows.map(String));
return;
} else {
console.error("Unhandled cell selection update:", cellSelection);
Expand All @@ -317,7 +317,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
handleCellSelection as EventListener
);
};
}, [id, rowSelection, rowData]);
}, [id, selection, rowData]);

useEffect(() => {
const handleColumnSort = (
Expand Down Expand Up @@ -390,10 +390,10 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
useEffect(() => {
if (!id) return;
let shinyValue: CellSelection | null = null;
if (rowSelectionModes.isNone()) {
if (selectionModes.isNone()) {
shinyValue = null;
} else if (rowSelectionModes.row !== SelectionModes._rowEnum.NONE) {
const rowSelectionKeys = rowSelection.keys().toList();
} else if (selectionModes.row !== SelectionModes._rowEnum.NONE) {
const rowSelectionKeys = selection.keys().toList();
const rowsById = table.getSortedRowModel().rowsById;
shinyValue = {
type: "row",
Expand All @@ -407,10 +407,10 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
.filter((x): x is number => x !== null),
};
} else {
console.error("Unhandled row selection mode:", rowSelectionModes);
console.error("Unhandled row selection mode:", selectionModes);
}
Shiny.setInputValue!(`${id}_cell_selection`, shinyValue);
}, [id, rowSelection, rowSelectionModes, table, table.getSortedRowModel]);
}, [id, selection, selectionModes, table, table.getSortedRowModel]);

useEffect(() => {
if (!id) return;
Expand Down Expand Up @@ -468,8 +468,8 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
useEffect(() => {
if (!id) return;
let shinyValue: number[] | null = null;
if (rowSelectionModes.row !== SelectionModes._rowEnum.NONE) {
const rowSelectionKeys = rowSelection.keys().toList();
if (selectionModes.row !== SelectionModes._rowEnum.NONE) {
const rowSelectionKeys = selection.keys().toList();
const rowsById = table.getSortedRowModel().rowsById;
shinyValue = rowSelectionKeys
.map((key) => {
Expand All @@ -482,7 +482,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
.sort();
}
Shiny.setInputValue!(`${id}_selected_rows`, shinyValue);
}, [id, rowSelection, rowSelectionModes, table]);
}, [id, selection, selectionModes, table]);

// ### End row selection ############################################################

Expand All @@ -491,14 +491,6 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
type TKey = typeof HTMLTableRowElement.prototype.dataset.key;
type TElement = HTMLTableRowElement;

if (editCellsIsAllowed && canSelect) {
// TODO-barret; maybe listen for a double click?
// Is is possible to rerender on double click independent of the row selection?
console.error(
"Should not have editable and row selection at the same time"
);
}

// ### End editable cells ###########################################################

//
Expand All @@ -520,7 +512,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
useEffect(() => {
return () => {
table.resetSorting();
rowSelection.clear();
selection.clear();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [payload]);
Expand Down Expand Up @@ -549,6 +541,8 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
className += " html-fill-item";
}

const includeRowNumbers = selectionModes.row !== SelectionModes._rowEnum.NONE;

return (
<>
<div
Expand All @@ -567,6 +561,8 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
<thead ref={theadRef} style={{ backgroundColor: bgcolor }}>
{table.getHeaderGroups().map((headerGroup, i) => (
<tr key={headerGroup.id} aria-rowindex={i + 1}>
{includeRowNumbers && <th className="table-corner"></th>}

{headerGroup.headers.map((header) => {
const headerContent = header.isPlaceholder ? undefined : (
<div
Expand Down Expand Up @@ -605,6 +601,7 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
))}
{withFilters && (
<tr className="filters">
{includeRowNumbers && <th className="table-corner"></th>}
{table.getFlatHeaders().map((header) => {
return (
<th key={`filter-${header.id}`}>
Expand All @@ -631,10 +628,13 @@ const ShinyDataGrid: FC<ShinyDataGridProps<unknown>> = ({
aria-rowindex={virtualRow.index + headerRowCount}
data-key={row.id}
ref={measureEl}
aria-selected={rowSelection.has(row.id)}
aria-selected={selection.has(row.id)}
tabIndex={-1}
{...rowSelection.itemHandlers()}
{...selection.itemHandlers()}
>
{selectionModes.row !== SelectionModes._rowEnum.NONE && (
<td className="row-number">{virtualRow.index}</td>
)}
{row.getVisibleCells().map((cell) => {
// TODO-barret; Only send in the cell data that is needed;
const rowIndex = cell.row.index;
Expand Down
17 changes: 11 additions & 6 deletions js/data-frame/selection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,12 +113,17 @@ export function initSelectionModes(
});
}

export function useSelection<TKey, TElement extends HTMLElement>(
selectionModes: SelectionModes,
keyAccessor: (el: TElement) => TKey,
focusOffset: (start: TKey, offset: number) => TKey | null,
between?: (from: TKey, to: TKey) => ReadonlyArray<TKey>
): SelectionSet<TKey, TElement> {
export function useSelection<TKey, TElement extends HTMLElement>({
selectionModes: selectionModes,
keyAccessor,
focusOffset,
between,
}: {
selectionModes: SelectionModes;
keyAccessor: (el: TElement) => TKey;
focusOffset: (start: TKey, offset: number) => TKey | null;
between?: (from: TKey, to: TKey) => ReadonlyArray<TKey>;
}): SelectionSet<TKey, TElement> {
const [selectedKeys, setSelectedKeys] = useState<ImmutableSet<TKey>>(
ImmutableSet.empty()
);
Expand Down
12 changes: 12 additions & 0 deletions js/data-frame/styles.scss
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,11 @@ shiny-data-frame {
--shiny-datagrid-grid-body-hover-bgcolor: var(--shiny-datagrid-grid-header-bgcolor);
--shiny-datagrid-grid-body-selected-bgcolor: #b4d5fe;
--shiny-datagrid-grid-body-selected-color: var(--bs-dark);
--shiny-datagrid-grid-header-selected-bgcolor: color-mix(
in srgb,
var(--shiny-datagrid-grid-header-bgcolor) 30%,
var(--shiny-datagrid-grid-body-selected-bgcolor)
);

// 2024-03-01: Greg: Do not use warning or info bs colors! Their contrast on white is not enough
// Saved cells
Expand Down Expand Up @@ -186,6 +191,13 @@ shiny-data-frame .shiny-data-grid.shiny-data-grid-grid {
> td {
padding: var(--shiny-datagrid-padding);
}

&:not([aria-selected="true"]) > td.row-number {
background-color: var(--shiny-datagrid-grid-header-bgcolor);
}
&[aria-selected="true"] > td.row-number {
background-color: var(--shiny-datagrid-grid-header-selected-bgcolor);
}
}
}
}
Expand Down
19 changes: 15 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.

44 changes: 19 additions & 25 deletions tests/playwright/shiny/components/data_frame/edit/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,43 +72,37 @@ def summary_data():
# return df
return render.DataGrid(
df,
selection_mode="rows",
editable=False,
selection_mode=("rows"),
editable=True,
filters=True,
)
# return render.DataTable(df, selection_mode="none", editable=True)
# return render.DataGrid(df, selection_mode="rows", editable=True)
return render.DataGrid(df, selection_mode="rows", editable=True)
# return render.DataTable(df, selection_mode="rows", editable=True)
return render.DataGrid(df, selection_mode="rows", editable=False)
# return render.DataGrid(df, selection_mode="rows", editable=False)
# return render.DataTable(df, selection_mode="rows", editable=False)

from shiny import reactive

@reactive.effect
def _():
print(
"Filters:",
summary_data.filter(),
)
# @reactive.effect
# def _():
# print("Filters:", summary_data.filter())

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

@reactive.effect
def _():
print("indices:", summary_data.data_view_rows())
# @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:\n", summary_data.data_view(selected=False))

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

@reactive.effect
def _():
Expand Down

0 comments on commit d83f043

Please sign in to comment.