Skip to content
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

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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
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
Expand Up @@ -13,6 +13,7 @@ export const Cell: React.FC<Types.CellComponentProps> = ({
column,
DataViewer,
selected,
highlighted,
active,
dragging,
mode,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -99,6 +100,7 @@ export const enhance = (
Omit<
Types.CellComponentProps,
| "selected"
| "highlighted"
| "active"
| "copied"
| "dragging"
Expand Down Expand Up @@ -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}
Expand Down
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
Expand Up @@ -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;
Expand Down
20 changes: 19 additions & 1 deletion src/Spreadsheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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>,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -535,6 +552,7 @@ const Spreadsheet = <CellType extends Types.CellBase>(
onBlur={handleBlur}
>
{tableNode}
<HighlightCellContainer />
{activeCellNode}
<Selected />
<Copied />
Expand Down
16 changes: 16 additions & 0 deletions src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -283,6 +298,7 @@ export type Action =
| SelectEntireColumnAction
| SelectEntireWorksheetAction
| SetSelectionAction
| SetHighlightsAction
| SelectAction
| ActivateAction
| SetCellDataAction
Expand Down
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
Expand Up @@ -27,6 +27,7 @@ export const INITIAL_STATE: Types.StoreState = {
selected: new EmptySelection(),
copied: null,
lastCommit: null,
highlights: [],
};

export default function reducer(
Expand Down Expand Up @@ -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)) {
Expand Down
23 changes: 23 additions & 0 deletions src/stories/Spreadsheet.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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>;
Expand Down Expand Up @@ -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
Expand Up @@ -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
Expand Down Expand Up @@ -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<
Expand All @@ -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 */
Expand Down