Skip to content

Commit 7bce46c

Browse files
Expose activate method (#421)
* Enhance Spreadsheet component with ref support and add related tests - Introduced type to expose an method for cell activation. - Updated component to support forwarding refs and added memoized methods. - Enhanced tests to cover new ref methods, ensuring they are stable and handle invalid points gracefully. - Added a new story for controlled activation demonstrating the use of the ref. This update improves the usability of the Spreadsheet component by allowing parent components to programmatically activate cells. * Improve documentation for SpreadsheetRef activation method
1 parent 8e5f151 commit 7bce46c

File tree

4 files changed

+154
-7
lines changed

4 files changed

+154
-7
lines changed

src/Spreadsheet.test.tsx

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import React from "react";
66
import { fireEvent, render, screen } from "@testing-library/react";
7-
import Spreadsheet, { Props } from "./Spreadsheet";
7+
import Spreadsheet, { Props, SpreadsheetRef } from "./Spreadsheet";
88
import * as Matrix from "./matrix";
99
import * as Types from "./types";
1010
import * as Point from "./point";
@@ -478,6 +478,76 @@ describe("<Spreadsheet />", () => {
478478
});
479479
});
480480

481+
describe("Spreadsheet Ref Methods", () => {
482+
beforeEach(() => {
483+
jest.clearAllMocks();
484+
});
485+
486+
test("ref.activate activates the specified cell", () => {
487+
const onActivate = jest.fn();
488+
const ref = React.createRef<SpreadsheetRef>();
489+
490+
render(
491+
<Spreadsheet {...EXAMPLE_PROPS} ref={ref} onActivate={onActivate} />
492+
);
493+
494+
// Ensure ref is defined
495+
expect(ref.current).not.toBeNull();
496+
497+
// Call activate method via ref
498+
const targetPoint = { row: 1, column: 1 };
499+
React.act(() => {
500+
ref.current?.activate(targetPoint);
501+
});
502+
503+
// Verify onActivate was called with correct point
504+
expect(onActivate).toHaveBeenCalledTimes(1);
505+
expect(onActivate).toHaveBeenCalledWith(targetPoint);
506+
});
507+
508+
test("ref methods are memoized and stable between renders", () => {
509+
const ref = React.createRef<SpreadsheetRef>();
510+
const { rerender } = render(<Spreadsheet {...EXAMPLE_PROPS} ref={ref} />);
511+
512+
// Store initial methods
513+
const initialActivate = ref.current?.activate;
514+
515+
// Trigger re-render
516+
rerender(<Spreadsheet {...EXAMPLE_PROPS} ref={ref} />);
517+
518+
// Methods should be referentially stable
519+
expect(ref.current?.activate).toBe(initialActivate);
520+
});
521+
522+
test("activate method handles invalid points gracefully", () => {
523+
const onActivate = jest.fn();
524+
const ref = React.createRef<SpreadsheetRef>();
525+
526+
render(
527+
<Spreadsheet {...EXAMPLE_PROPS} ref={ref} onActivate={onActivate} />
528+
);
529+
530+
// Try to activate cell outside grid bounds
531+
const invalidPoint = { row: ROWS + 1, column: COLUMNS + 1 };
532+
React.act(() => {
533+
ref.current?.activate(invalidPoint);
534+
});
535+
536+
// Should still call onActivate with the provided point
537+
expect(onActivate).toHaveBeenCalledTimes(1);
538+
expect(onActivate).toHaveBeenCalledWith(invalidPoint);
539+
});
540+
541+
test("ref is properly typed as SpreadsheetRef", () => {
542+
const ref = React.createRef<SpreadsheetRef>();
543+
544+
render(<Spreadsheet {...EXAMPLE_PROPS} ref={ref} />);
545+
546+
// TypeScript compilation would fail if ref typing is incorrect
547+
expect(typeof ref.current?.activate).toBe("function");
548+
});
549+
});
550+
481551
/** Like .querySelector() but throws for no match */
482552
function safeQuerySelector<T extends Element = Element>(
483553
node: ParentNode,

src/Spreadsheet.tsx

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -123,11 +123,24 @@ export type Props<CellType extends Types.CellBase> = {
123123
onEvaluatedDataChange?: (data: Matrix.Matrix<CellType>) => void;
124124
};
125125

126+
/**
127+
* The Spreadsheet Ref Type
128+
*/
129+
130+
export type SpreadsheetRef = {
131+
/**
132+
* Pass the desired point as a prop to specify which one should be activated.
133+
*/
134+
activate: (point: Point.Point) => void;
135+
};
136+
126137
/**
127138
* The Spreadsheet component
128139
*/
129-
const Spreadsheet = <CellType extends Types.CellBase>(
130-
props: Props<CellType>
140+
141+
const Spreadsheet = <SpreadsheetRef, CellType extends Types.CellBase>(
142+
props: Props<CellType>,
143+
ref: React.ForwardedRef<SpreadsheetRef>
131144
): React.ReactElement => {
132145
const {
133146
className,
@@ -198,6 +211,23 @@ const Spreadsheet = <CellType extends Types.CellBase>(
198211
const setCreateFormulaParser = useAction(Actions.setCreateFormulaParser);
199212
const blur = useAction(Actions.blur);
200213
const setSelection = useAction(Actions.setSelection);
214+
const activate = useAction(Actions.activate);
215+
216+
// Memoize methods to be exposed via ref
217+
const methods = React.useMemo(
218+
() => ({
219+
activate: (point: Point.Point) => {
220+
activate(point);
221+
},
222+
}),
223+
[]
224+
);
225+
226+
// Expose methods to parent via ref
227+
React.useImperativeHandle<SpreadsheetRef, SpreadsheetRef>(
228+
ref,
229+
() => methods as SpreadsheetRef
230+
);
201231

202232
// Track active
203233
const prevActiveRef = React.useRef<Point.Point | null>(state.active);
@@ -557,4 +587,4 @@ const Spreadsheet = <CellType extends Types.CellBase>(
557587
);
558588
};
559589

560-
export default Spreadsheet;
590+
export default React.forwardRef(Spreadsheet);

src/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
import Spreadsheet from "./Spreadsheet";
1+
import Spreadsheet, { SpreadsheetRef } from "./Spreadsheet";
22
import DataEditor from "./DataEditor";
33
import DataViewer from "./DataViewer";
44

55
export default Spreadsheet;
6-
export { Spreadsheet, DataEditor, DataViewer };
6+
export { Spreadsheet, DataEditor, DataViewer, SpreadsheetRef };
77
export type { Props } from "./Spreadsheet";
88
export { createEmpty as createEmptyMatrix } from "./matrix";
99
export type { Matrix } from "./matrix";

src/stories/Spreadsheet.stories.tsx

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@ import {
1010
EntireRowsSelection,
1111
EntireColumnsSelection,
1212
EmptySelection,
13+
Point,
14+
SpreadsheetRef,
1315
} from "..";
1416
import * as Matrix from "../matrix";
1517
import { AsyncCellDataEditor, AsyncCellDataViewer } from "./AsyncCellData";
1618
import CustomCell from "./CustomCell";
1719
import { RangeEdit, RangeView } from "./RangeDataComponents";
1820
import { SelectEdit, SelectView } from "./SelectDataComponents";
1921
import { CustomCornerIndicator } from "./CustomCornerIndicator";
20-
2122
type StringCell = CellBase<string | undefined>;
2223
type NumberCell = CellBase<number | undefined>;
2324

@@ -305,3 +306,49 @@ export const ControlledSelection: StoryFn<Props<StringCell>> = (props) => {
305306
</div>
306307
);
307308
};
309+
310+
export const ControlledActivation: StoryFn<Props<StringCell>> = (props) => {
311+
const spreadsheetRef = React.useRef<SpreadsheetRef>();
312+
313+
const [activationPoint, setActivationPoint] = React.useState<Point>({
314+
row: 0,
315+
column: 0,
316+
});
317+
318+
const handleActivate = React.useCallback(() => {
319+
spreadsheetRef.current?.activate(activationPoint);
320+
}, [activationPoint]);
321+
322+
return (
323+
<div>
324+
<div>
325+
<input
326+
id="row"
327+
title="row"
328+
type="number"
329+
value={activationPoint.row}
330+
onChange={(e) =>
331+
setActivationPoint(() => ({
332+
...activationPoint,
333+
row: Number(e.target.value),
334+
}))
335+
}
336+
/>
337+
<input
338+
id="column"
339+
title="row"
340+
type="column"
341+
value={activationPoint.column}
342+
onChange={(e) =>
343+
setActivationPoint(() => ({
344+
...activationPoint,
345+
column: Number(e.target.value),
346+
}))
347+
}
348+
/>
349+
<button onClick={handleActivate}>Activate</button>
350+
</div>
351+
<Spreadsheet ref={spreadsheetRef} {...props} />;
352+
</div>
353+
);
354+
};

0 commit comments

Comments
 (0)