Skip to content

Add highlights prop for cell highlighting (multi-user collaboration) #420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/Cell.tsx
Original file line number Diff line number Diff line change
@@ -13,6 +13,7 @@ export const Cell: React.FC<Types.CellComponentProps> = ({
column,
DataViewer,
selected,
highlighted,
active,
dragging,
mode,
@@ -59,13 +60,13 @@ export const Cell: React.FC<Types.CellComponentProps> = ({

React.useEffect(() => {
const root = rootRef.current;
if (selected && root) {
if ((selected || highlighted) && root) {
setCellDimensions(point, getOffsetRect(root));
}
if (root && active && mode === "view") {
root.focus();
}
}, [setCellDimensions, selected, active, mode, point, data]);
}, [setCellDimensions, selected, highlighted, active, mode, point, data]);

if (data && data.DataViewer) {
// @ts-ignore
@@ -99,6 +100,7 @@ export const enhance = (
Omit<
Types.CellComponentProps,
| "selected"
| "highlighted"
| "active"
| "copied"
| "dragging"
@@ -146,13 +148,18 @@ export const enhance = (
const selected = useSelector((state) =>
state.selected.has(state.model.data, point)
);
const highlights = useSelector((state) => state.highlights);
const highlighted = highlights.some((highlight) =>
Point.isEqual(highlight.point, point)
);
const dragging = useSelector((state) => state.dragging);
const copied = useSelector((state) => state.copied?.has(point) || false);

return (
<CellComponent
{...props}
selected={selected}
highlighted={highlighted}
active={active}
copied={copied}
dragging={dragging}
54 changes: 54 additions & 0 deletions src/HighlightCell.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from "react";
import classnames from "classnames";
import useSelector from "./use-selector";
import { getCellDimensions } from "./util";
import {Highlight} from "./highlight";

/**
* A component that highlights a cell by taking a specific cell coordinate (`point`) and `color` value
* Like ActiveCell, it captures the position and size (cell bounding) to display the highlight.
*/
type HighlightCellComponentProps ={
highLight: Highlight;
}
const HighlightCell: React.FC<HighlightCellComponentProps> = ({ highLight }) => {
const { point, color } = highLight;
const rootRef = React.useRef<HTMLDivElement>(null);

const dimensions = useSelector((state) =>
getCellDimensions(point, state.rowDimensions, state.columnDimensions)
);

const hidden = !dimensions;
if (hidden) {
return null;
}

return (
<div
ref={rootRef}
className={classnames(
"Spreadsheet__highlight-cell"
)}
style={{
...dimensions,
borderColor: color,
}}
tabIndex={0}
/>
);
};

const HighlightCellContainer: React.FC = () => {
const highlights = useSelector((state) => state.highlights);

return (
<>
{highlights.map((highlight, index) => (
<HighlightCell key={index} highLight={highlight} />
))}
</>
);
}

export default HighlightCellContainer;
7 changes: 7 additions & 0 deletions src/Spreadsheet.css
Original file line number Diff line number Diff line change
@@ -34,6 +34,13 @@
box-shadow: var(--elevation);
}

.Spreadsheet__highlight-cell {
position: absolute;
border: 2px solid;
box-sizing: border-box;
pointer-events: none;
}

.Spreadsheet__table {
border-collapse: collapse;
table-layout: fixed;
20 changes: 19 additions & 1 deletion src/Spreadsheet.tsx
Original file line number Diff line number Diff line change
@@ -4,6 +4,7 @@ import * as Types from "./types";
import * as Actions from "./actions";
import * as Matrix from "./matrix";
import * as Point from "./point";
import * as Highlight from "./highlight";
import { Selection } from "./selection";
import reducer, { INITIAL_STATE, hasKeyDownHandler } from "./reducer";
import context from "./context";
@@ -34,6 +35,7 @@ import { Cell as DefaultCell, enhance as enhanceCell } from "./Cell";
import DefaultDataViewer from "./DataViewer";
import DefaultDataEditor from "./DataEditor";
import ActiveCell from "./ActiveCell";
import HighlightCellContainer from "./HighlightCell";
import Selected from "./Selected";
import Copied from "./Copied";

@@ -82,6 +84,8 @@ export type Props<CellType extends Types.CellBase> = {
hideColumnIndicators?: boolean;
/** The selected cells in the worksheet. */
selected?: Selection;
/** Highlights to apply to the spreadsheet */
highlights?: Highlight.Highlight[];
// Custom Components
/** Component rendered above each column. */
ColumnIndicator?: Types.ColumnIndicatorComponent;
@@ -160,8 +164,9 @@ const Spreadsheet = <CellType extends Types.CellBase>(
...INITIAL_STATE,
model,
selected: props.selected || INITIAL_STATE.selected,
highlights: props.highlights || INITIAL_STATE.highlights,
} as State;
}, [props.createFormulaParser, props.data, props.selected]);
}, [props.createFormulaParser, props.data, props.selected, props.highlights]);

const reducerElements = React.useReducer(
reducer as unknown as React.Reducer<State, Actions.Action>,
@@ -195,6 +200,7 @@ const Spreadsheet = <CellType extends Types.CellBase>(
const onDragStart = useAction(Actions.dragStart);
const onDragEnd = useAction(Actions.dragEnd);
const setData = useAction(Actions.setData);
const setHighlights = useAction(Actions.setHighlights);
const setCreateFormulaParser = useAction(Actions.setCreateFormulaParser);
const blur = useAction(Actions.blur);
const setSelection = useAction(Actions.setSelection);
@@ -302,6 +308,17 @@ const Spreadsheet = <CellType extends Types.CellBase>(
prevDataPropRef.current = props.data;
}, [props.data, setData]);

// Update highlights when props.highlights changes
const prevHighlightsPropRef = React.useRef<Highlight.Highlight[] | undefined>(
props.highlights
);
React.useEffect(() => {
if (props.highlights !== prevHighlightsPropRef.current) {
setHighlights(props.highlights || []);
}
prevHighlightsPropRef.current = props.highlights;
}, [props.highlights, setHighlights]);

// Update createFormulaParser when props.createFormulaParser changes
const prevCreateFormulaParserPropRef = React.useRef<
Types.CreateFormulaParser | undefined
@@ -535,6 +552,7 @@ const Spreadsheet = <CellType extends Types.CellBase>(
onBlur={handleBlur}
>
{tableNode}
<HighlightCellContainer />
{activeCellNode}
<Selected />
<Copied />
16 changes: 16 additions & 0 deletions src/actions.ts
Original file line number Diff line number Diff line change
@@ -7,13 +7,15 @@ import {
CreateFormulaParser,
} from "./types";
import { Selection } from "./selection";
import {Highlight} from "./highlight";

export const SET_DATA = "SET_DATA";
export const SET_CREATE_FORMULA_PARSER = "SET_CREATE_FORMULA_PARSER";
export const SELECT_ENTIRE_ROW = "SELECT_ENTIRE_ROW";
export const SELECT_ENTIRE_COLUMN = "SELECT_ENTIRE_COLUMN";
export const SELECT_ENTIRE_WORKSHEET = "SELECT_ENTIRE_WORKSHEET";
export const SET_SELECTION = "SET_SELECTION";
export const SET_HIGHLIGHTS = "SET_HIGHLIGHTS";
export const SELECT = "SELECT";
export const ACTIVATE = "ACTIVATE";
export const SET_CELL_DATA = "SET_CELL_DATA";
@@ -132,6 +134,19 @@ export function select(point: Point): SelectAction {
};
}

export type SetHighlightsAction = BaseAction<typeof SET_HIGHLIGHTS> & {
payload: {
highlights: Highlight[];
};
};

export function setHighlights(highlights: Highlight[]): SetHighlightsAction {
return {
type: SET_HIGHLIGHTS,
payload: { highlights },
};
}

export type ActivateAction = BaseAction<typeof ACTIVATE> & {
payload: {
point: Point;
@@ -283,6 +298,7 @@ export type Action =
| SelectEntireColumnAction
| SelectEntireWorksheetAction
| SetSelectionAction
| SetHighlightsAction
| SelectAction
| ActivateAction
| SetCellDataAction
7 changes: 7 additions & 0 deletions src/highlight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {Point} from "./point";

/** A highlight in the spreadsheet */
export type Highlight = {
point: Point;
color: string;
};
7 changes: 7 additions & 0 deletions src/reducer.ts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ export const INITIAL_STATE: Types.StoreState = {
selected: new EmptySelection(),
copied: null,
lastCommit: null,
highlights: [],
};

export default function reducer(
@@ -103,6 +104,12 @@ export default function reducer(
mode: "view",
};
}
case Actions.SET_HIGHLIGHTS: {
return {
...state,
highlights: action.payload.highlights,
};
}
case Actions.SELECT: {
const { point } = action.payload;
if (state.active && !isActive(state.active, point)) {
23 changes: 23 additions & 0 deletions src/stories/Spreadsheet.stories.tsx
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ import CustomCell from "./CustomCell";
import { RangeEdit, RangeView } from "./RangeDataComponents";
import { SelectEdit, SelectView } from "./SelectDataComponents";
import { CustomCornerIndicator } from "./CustomCornerIndicator";
import {Highlight} from "../highlight";

type StringCell = CellBase<string | undefined>;
type NumberCell = CellBase<number | undefined>;
@@ -305,3 +306,25 @@ export const ControlledSelection: StoryFn<Props<StringCell>> = (props) => {
</div>
);
};

export const ControlledHighlights: StoryFn<Props<StringCell>> = (props) => {
const [highlights, setHighlights] = React.useState<Highlight[]>([{ point: { row: 0, column: 0 }, color: "#FF0000" }]);

const handleHighlight = React.useCallback(() => {
setHighlights((highlights) => {
if (highlights.length === 0) {
return [{ point: { row: 0, column: 0 }, color: "#FF0000" }];
}
return [];
});
}, []);

return (
<div>
<div>
<button onClick={handleHighlight}>toggle highlight</button>
</div>
<Spreadsheet {...props} highlights={highlights} />;
</div>
);
};
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import { Selection } from "./selection";
import { Model } from "./engine";
import { PointRange } from "./point-range";
import { Matrix } from "./matrix";
import {Highlight} from "./highlight";

/** The base type of cell data in Spreadsheet */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -52,6 +53,7 @@ export type StoreState<Cell extends CellBase = CellBase> = {
hasPasted: boolean;
cut: boolean;
active: Point | null;
highlights: Highlight[];
mode: Mode;
rowDimensions: Record<number, Pick<Dimensions, "height" | "top"> | undefined>;
columnDimensions: Record<
@@ -78,6 +80,8 @@ export type CellComponentProps<Cell extends CellBase = CellBase> = {
DataViewer: DataViewerComponent<Cell>;
/** Whether the cell is selected */
selected: boolean;
/** Whether the cell is highlighted */
highlighted: boolean;
/** Whether the cell is active */
active: boolean;
/** Whether the cell is copied */