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

Expose activate method #421

Open
wants to merge 2 commits 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
72 changes: 71 additions & 1 deletion src/Spreadsheet.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import React from "react";
import { fireEvent, render, screen } from "@testing-library/react";
import Spreadsheet, { Props } from "./Spreadsheet";
import Spreadsheet, { Props, SpreadsheetRef } from "./Spreadsheet";
import * as Matrix from "./matrix";
import * as Types from "./types";
import * as Point from "./point";
Expand Down Expand Up @@ -478,6 +478,76 @@ describe("<Spreadsheet />", () => {
});
});

describe("Spreadsheet Ref Methods", () => {
beforeEach(() => {
jest.clearAllMocks();
});

test("ref.activate activates the specified cell", () => {
const onActivate = jest.fn();
const ref = React.createRef<SpreadsheetRef>();

render(
<Spreadsheet {...EXAMPLE_PROPS} ref={ref} onActivate={onActivate} />
);

// Ensure ref is defined
expect(ref.current).not.toBeNull();

// Call activate method via ref
const targetPoint = { row: 1, column: 1 };
React.act(() => {
ref.current?.activate(targetPoint);
});

// Verify onActivate was called with correct point
expect(onActivate).toHaveBeenCalledTimes(1);
expect(onActivate).toHaveBeenCalledWith(targetPoint);
});

test("ref methods are memoized and stable between renders", () => {
const ref = React.createRef<SpreadsheetRef>();
const { rerender } = render(<Spreadsheet {...EXAMPLE_PROPS} ref={ref} />);

// Store initial methods
const initialActivate = ref.current?.activate;

// Trigger re-render
rerender(<Spreadsheet {...EXAMPLE_PROPS} ref={ref} />);

// Methods should be referentially stable
expect(ref.current?.activate).toBe(initialActivate);
});

test("activate method handles invalid points gracefully", () => {
const onActivate = jest.fn();
const ref = React.createRef<SpreadsheetRef>();

render(
<Spreadsheet {...EXAMPLE_PROPS} ref={ref} onActivate={onActivate} />
);

// Try to activate cell outside grid bounds
const invalidPoint = { row: ROWS + 1, column: COLUMNS + 1 };
React.act(() => {
ref.current?.activate(invalidPoint);
});

// Should still call onActivate with the provided point
expect(onActivate).toHaveBeenCalledTimes(1);
expect(onActivate).toHaveBeenCalledWith(invalidPoint);
});

test("ref is properly typed as SpreadsheetRef", () => {
const ref = React.createRef<SpreadsheetRef>();

render(<Spreadsheet {...EXAMPLE_PROPS} ref={ref} />);

// TypeScript compilation would fail if ref typing is incorrect
expect(typeof ref.current?.activate).toBe("function");
});
});

/** Like .querySelector() but throws for no match */
function safeQuerySelector<T extends Element = Element>(
node: ParentNode,
Expand Down
36 changes: 33 additions & 3 deletions src/Spreadsheet.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,24 @@
onEvaluatedDataChange?: (data: Matrix.Matrix<CellType>) => void;
};

/**
* The Spreadsheet Ref Type
*/

export type SpreadsheetRef = {
/**
* Pass the desired point as a prop to specify which one should be activated.
*/
activate: (point: Point.Point) => void;
};

/**
* The Spreadsheet component
*/
const Spreadsheet = <CellType extends Types.CellBase>(
props: Props<CellType>

const Spreadsheet = <SpreadsheetRef, CellType extends Types.CellBase>(
props: Props<CellType>,
ref: React.ForwardedRef<SpreadsheetRef>
): React.ReactElement => {
const {
className,
Expand Down Expand Up @@ -198,6 +211,23 @@
const setCreateFormulaParser = useAction(Actions.setCreateFormulaParser);
const blur = useAction(Actions.blur);
const setSelection = useAction(Actions.setSelection);
const activate = useAction(Actions.activate);

// Memoize methods to be exposed via ref
const methods = React.useMemo(
() => ({
activate: (point: Point.Point) => {
activate(point);
},
}),
[]

Check warning on line 223 in src/Spreadsheet.tsx

View workflow job for this annotation

GitHub Actions / build

React Hook React.useMemo has a missing dependency: 'activate'. Either include it or remove the dependency array
);

// Expose methods to parent via ref
React.useImperativeHandle<SpreadsheetRef, SpreadsheetRef>(
ref,
() => methods as SpreadsheetRef
);

// Track active
const prevActiveRef = React.useRef<Point.Point | null>(state.active);
Expand Down Expand Up @@ -557,4 +587,4 @@
);
};

export default Spreadsheet;
export default React.forwardRef(Spreadsheet);
4 changes: 2 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import Spreadsheet from "./Spreadsheet";
import Spreadsheet, { SpreadsheetRef } from "./Spreadsheet";
import DataEditor from "./DataEditor";
import DataViewer from "./DataViewer";

export default Spreadsheet;
export { Spreadsheet, DataEditor, DataViewer };
export { Spreadsheet, DataEditor, DataViewer, SpreadsheetRef };
export type { Props } from "./Spreadsheet";
export { createEmpty as createEmptyMatrix } from "./matrix";
export type { Matrix } from "./matrix";
Expand Down
49 changes: 48 additions & 1 deletion src/stories/Spreadsheet.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@ import {
EntireRowsSelection,
EntireColumnsSelection,
EmptySelection,
Point,
SpreadsheetRef,
} from "..";
import * as Matrix from "../matrix";
import { AsyncCellDataEditor, AsyncCellDataViewer } from "./AsyncCellData";
import CustomCell from "./CustomCell";
import { RangeEdit, RangeView } from "./RangeDataComponents";
import { SelectEdit, SelectView } from "./SelectDataComponents";
import { CustomCornerIndicator } from "./CustomCornerIndicator";

type StringCell = CellBase<string | undefined>;
type NumberCell = CellBase<number | undefined>;

Expand Down Expand Up @@ -305,3 +306,49 @@ export const ControlledSelection: StoryFn<Props<StringCell>> = (props) => {
</div>
);
};

export const ControlledActivation: StoryFn<Props<StringCell>> = (props) => {
const spreadsheetRef = React.useRef<SpreadsheetRef>();

const [activationPoint, setActivationPoint] = React.useState<Point>({
row: 0,
column: 0,
});

const handleActivate = React.useCallback(() => {
spreadsheetRef.current?.activate(activationPoint);
}, [activationPoint]);

return (
<div>
<div>
<input
id="row"
title="row"
type="number"
value={activationPoint.row}
onChange={(e) =>
setActivationPoint(() => ({
...activationPoint,
row: Number(e.target.value),
}))
}
/>
<input
id="column"
title="row"
type="column"
value={activationPoint.column}
onChange={(e) =>
setActivationPoint(() => ({
...activationPoint,
column: Number(e.target.value),
}))
}
/>
<button onClick={handleActivate}>Activate</button>
</div>
<Spreadsheet ref={spreadsheetRef} {...props} />;
</div>
);
};
Loading