From f71d1174be40c3f156e7fc3605a59a28446bdde4 Mon Sep 17 00:00:00 2001 From: nahooni0511 Date: Sat, 18 Jan 2025 16:42:10 +0900 Subject: [PATCH] Add custom feature for highlighting cell --- src/Cell.tsx | 11 ++++-- src/HighlightCell.tsx | 54 +++++++++++++++++++++++++++++ src/Spreadsheet.css | 7 ++++ src/Spreadsheet.tsx | 20 ++++++++++- src/actions.ts | 16 +++++++++ src/highlight.ts | 7 ++++ src/reducer.ts | 7 ++++ src/stories/Spreadsheet.stories.tsx | 23 ++++++++++++ src/types.ts | 4 +++ 9 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 src/HighlightCell.tsx create mode 100644 src/highlight.ts diff --git a/src/Cell.tsx b/src/Cell.tsx index 7bf79e08..069a7aa4 100644 --- a/src/Cell.tsx +++ b/src/Cell.tsx @@ -13,6 +13,7 @@ export const Cell: React.FC = ({ column, DataViewer, selected, + highlighted, active, dragging, mode, @@ -59,13 +60,13 @@ export const Cell: React.FC = ({ 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,6 +148,10 @@ 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); @@ -153,6 +159,7 @@ export const enhance = ( = ({ highLight }) => { + const { point, color } = highLight; + const rootRef = React.useRef(null); + + const dimensions = useSelector((state) => + getCellDimensions(point, state.rowDimensions, state.columnDimensions) + ); + + const hidden = !dimensions; + if (hidden) { + return null; + } + + return ( +
+ ); +}; + +const HighlightCellContainer: React.FC = () => { + const highlights = useSelector((state) => state.highlights); + + return ( + <> + {highlights.map((highlight, index) => ( + + ))} + + ); +} + +export default HighlightCellContainer; diff --git a/src/Spreadsheet.css b/src/Spreadsheet.css index b4346e5a..f2d27ad6 100755 --- a/src/Spreadsheet.css +++ b/src/Spreadsheet.css @@ -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; diff --git a/src/Spreadsheet.tsx b/src/Spreadsheet.tsx index 58bacc1a..428aa899 100644 --- a/src/Spreadsheet.tsx +++ b/src/Spreadsheet.tsx @@ -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 = { 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 = ( ...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, @@ -195,6 +200,7 @@ const Spreadsheet = ( 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 = ( prevDataPropRef.current = props.data; }, [props.data, setData]); + // Update highlights when props.highlights changes + const prevHighlightsPropRef = React.useRef( + 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 = ( onBlur={handleBlur} > {tableNode} + {activeCellNode} diff --git a/src/actions.ts b/src/actions.ts index cade0c6c..340d2595 100644 --- a/src/actions.ts +++ b/src/actions.ts @@ -7,6 +7,7 @@ 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"; @@ -14,6 +15,7 @@ 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 & { + payload: { + highlights: Highlight[]; + }; +}; + +export function setHighlights(highlights: Highlight[]): SetHighlightsAction { + return { + type: SET_HIGHLIGHTS, + payload: { highlights }, + }; +} + export type ActivateAction = BaseAction & { payload: { point: Point; @@ -283,6 +298,7 @@ export type Action = | SelectEntireColumnAction | SelectEntireWorksheetAction | SetSelectionAction + | SetHighlightsAction | SelectAction | ActivateAction | SetCellDataAction diff --git a/src/highlight.ts b/src/highlight.ts new file mode 100644 index 00000000..1b2b242c --- /dev/null +++ b/src/highlight.ts @@ -0,0 +1,7 @@ +import {Point} from "./point"; + +/** A highlight in the spreadsheet */ +export type Highlight = { + point: Point; + color: string; +}; diff --git a/src/reducer.ts b/src/reducer.ts index cb370e6f..c4cee38c 100644 --- a/src/reducer.ts +++ b/src/reducer.ts @@ -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)) { diff --git a/src/stories/Spreadsheet.stories.tsx b/src/stories/Spreadsheet.stories.tsx index 5fcdebce..982fe0b5 100644 --- a/src/stories/Spreadsheet.stories.tsx +++ b/src/stories/Spreadsheet.stories.tsx @@ -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; type NumberCell = CellBase; @@ -305,3 +306,25 @@ export const ControlledSelection: StoryFn> = (props) => {
); }; + +export const ControlledHighlights: StoryFn> = (props) => { + const [highlights, setHighlights] = React.useState([{ 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 ( +
+
+ +
+ ; +
+ ); +}; diff --git a/src/types.ts b/src/types.ts index fa47a46a..033be83d 100644 --- a/src/types.ts +++ b/src/types.ts @@ -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 = { hasPasted: boolean; cut: boolean; active: Point | null; + highlights: Highlight[]; mode: Mode; rowDimensions: Record | undefined>; columnDimensions: Record< @@ -78,6 +80,8 @@ export type CellComponentProps = { DataViewer: DataViewerComponent; /** 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 */