Skip to content

Commit 58f25a6

Browse files
committed
AutoFill Handle
Add auto-fill handle to autofill values
1 parent bb80b60 commit 58f25a6

20 files changed

+1982
-28
lines changed

src/ActiveCell.tsx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,25 @@ const ActiveCell: React.FC<Props> = (props) => {
1616
const rootRef = React.useRef<HTMLDivElement>(null);
1717

1818
const dispatch = useDispatch();
19+
1920
const setCellData = React.useCallback(
2021
(active: Point.Point, data: Types.CellBase) =>
2122
dispatch(Actions.setCellData(active, data)),
2223
[dispatch]
2324
);
25+
2426
const edit = React.useCallback(() => dispatch(Actions.edit()), [dispatch]);
27+
2528
const commit = React.useCallback(
2629
(changes: Types.CommitChanges<Types.CellBase>) =>
2730
dispatch(Actions.commit(changes)),
2831
[dispatch]
2932
);
33+
3034
const view = React.useCallback(() => {
3135
dispatch(Actions.view());
3236
}, [dispatch]);
37+
3338
const active = useSelector((state) => state.active);
3439
const mode = useSelector((state) => state.mode);
3540
const cell = useSelector((state) =>

src/AutoFillHandle.tsx

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
import * as React from "react";
2+
import * as Actions from "./actions";
3+
import useDispatch from "./use-dispatch";
4+
5+
const AutoFillHandle: React.FC = () => {
6+
const dispatch = useDispatch();
7+
8+
const autoFillStart = React.useCallback(() => {
9+
dispatch(Actions.autoFillStart());
10+
}, [dispatch]);
11+
12+
const autoFillEnd = React.useCallback(() => {
13+
dispatch(Actions.autoFillEnd());
14+
}, [dispatch]);
15+
16+
const handleMouseDown = React.useCallback(
17+
(event: React.MouseEvent) => {
18+
event.stopPropagation();
19+
event.preventDefault();
20+
21+
autoFillStart();
22+
23+
const handleMouseUp = () => {
24+
autoFillEnd();
25+
window.removeEventListener("mouseup", handleMouseUp);
26+
};
27+
28+
window.addEventListener("mouseup", handleMouseUp);
29+
},
30+
[autoFillStart, autoFillEnd]
31+
);
32+
33+
return (
34+
<div
35+
className="Spreadsheet__auto-fill-handle"
36+
onMouseDown={handleMouseDown}
37+
/>
38+
);
39+
};
40+
41+
export default AutoFillHandle;
42+

src/Copied.test.tsx

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,10 @@ describe("<Copied />", () => {
1515
<Copied />
1616
</context.Provider>
1717
);
18+
expect(
19+
document.querySelector(
20+
".Spreadsheet__floating-rect.Spreadsheet__floating-rect--copied"
21+
)
22+
).not.toBeNull();
1823
});
19-
expect(
20-
document.querySelector(
21-
".Spreadsheet__floating-rect.Spreadsheet__floating-rect--copied"
22-
)
23-
);
2424
});

src/FloatingRect.tsx

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,24 +7,37 @@ export type Props = {
77
dimensions?: Types.Dimensions | null | undefined;
88
hidden?: boolean;
99
dragging?: boolean;
10+
autoFilling?: boolean;
11+
className?: string;
12+
children?: React.ReactNode;
1013
};
1114

1215
const FloatingRect: React.FC<Props> = ({
1316
dimensions,
1417
dragging,
18+
autoFilling,
1519
hidden,
1620
variant,
21+
className,
22+
children,
1723
}) => {
1824
const { width, height, top, left } = dimensions || {};
1925
return (
2026
<div
21-
className={classnames("Spreadsheet__floating-rect", {
22-
[`Spreadsheet__floating-rect--${variant}`]: variant,
23-
"Spreadsheet__floating-rect--dragging": dragging,
24-
"Spreadsheet__floating-rect--hidden": hidden,
25-
})}
27+
className={classnames(
28+
"Spreadsheet__floating-rect",
29+
{
30+
[`Spreadsheet__floating-rect--${variant}`]: variant,
31+
"Spreadsheet__floating-rect--dragging": dragging,
32+
"Spreadsheet__floating-rect--auto-filling": autoFilling,
33+
"Spreadsheet__floating-rect--hidden": hidden,
34+
},
35+
className
36+
)}
2637
style={{ width, height, top, left }}
27-
/>
38+
>
39+
{children}
40+
</div>
2841
);
2942
};
3043

src/Selected.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,14 @@ import * as React from "react";
22
import { getSelectedDimensions } from "./util";
33
import FloatingRect from "./FloatingRect";
44
import useSelector from "./use-selector";
5+
import classNames from "classnames";
6+
import AutoFillHandle from "./AutoFillHandle";
57

68
const Selected: React.FC = () => {
79
const selected = useSelector((state) => state.selected);
10+
const selectedSize = useSelector((state) =>
11+
state.selected.size(state.model.data)
12+
);
813
const dimensions = useSelector(
914
(state) =>
1015
selected &&
@@ -16,16 +21,22 @@ const Selected: React.FC = () => {
1621
)
1722
);
1823
const dragging = useSelector((state) => state.dragging);
19-
const hidden = useSelector(
20-
(state) => state.selected.size(state.model.data) < 2
21-
);
24+
const autoFilling = useSelector((state) => state.autoFilling);
25+
const hidden = selectedSize === 0;
26+
2227
return (
2328
<FloatingRect
2429
variant="selected"
2530
dimensions={dimensions}
2631
dragging={dragging}
2732
hidden={hidden}
28-
/>
33+
className={classNames({
34+
"Spreadsheet__selected-single": selectedSize === 1,
35+
})}
36+
autoFilling={autoFilling}
37+
>
38+
{!hidden && <AutoFillHandle />}
39+
</FloatingRect>
2940
);
3041
};
3142

src/Spreadsheet.css

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,31 @@
124124
border: 2px var(--outline-color) solid;
125125
}
126126

127-
.Spreadsheet__floating-rect--dragging {
127+
.Spreadsheet__floating-rect--selected.Spreadsheet__selected-single {
128+
background: none;
128129
border: none;
129130
}
130131

132+
.Spreadsheet__floating-rect--selected.Spreadsheet__floating-rect--auto-filling {
133+
background: none;
134+
border: 2px var(--readonly-text-color) dashed;
135+
}
136+
131137
.Spreadsheet__floating-rect--copied {
132138
border: 2px var(--outline-color) dashed;
133139
}
140+
141+
.Spreadsheet__auto-fill-handle {
142+
position: absolute;
143+
bottom: 0;
144+
right: 0;
145+
transform: translate(50%, 50%);
146+
width: 8px;
147+
height: 8px;
148+
background: var(--outline-color);
149+
border-radius: 50%;
150+
box-shadow: var(--elevation);
151+
cursor: pointer;
152+
z-index: 10;
153+
pointer-events: auto;
154+
}

src/Spreadsheet.test.tsx

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ describe("<Spreadsheet />", () => {
165165
activeCell?.getBoundingClientRect()
166166
);
167167
// Check selected is not hidden
168-
expect(selected).toHaveClass("Spreadsheet__floating-rect--hidden");
168+
expect(selected).not.toHaveClass("Spreadsheet__floating-rect--hidden");
169169
// Check onActivate is called
170170
expect(onActivate).toHaveBeenCalledTimes(1);
171171
expect(onActivate).toHaveBeenCalledWith(Point.ORIGIN);
@@ -476,6 +476,123 @@ describe("<Spreadsheet />", () => {
476476
);
477477
expect(selected).not.toHaveClass("Spreadsheet__floating-rect--hidden");
478478
});
479+
test("auto fill handle is not rendered when no cell is selected", () => {
480+
render(<Spreadsheet {...EXAMPLE_PROPS} />);
481+
const element = getSpreadsheetElement();
482+
const autoFillHandle = element.querySelector(
483+
".Spreadsheet__auto-fill-handle"
484+
);
485+
expect(autoFillHandle).toBeNull();
486+
});
487+
test("auto fill handle is rendered when a cell is selected", () => {
488+
render(<Spreadsheet {...EXAMPLE_PROPS} />);
489+
const element = getSpreadsheetElement();
490+
const cell = safeQuerySelector(element, "td");
491+
// Select a cell
492+
fireEvent.mouseDown(cell);
493+
// Check auto fill handle is rendered
494+
const autoFillHandle = safeQuerySelector(
495+
element,
496+
".Spreadsheet__auto-fill-handle"
497+
);
498+
expect(autoFillHandle).toBeInTheDocument();
499+
});
500+
test("auto fill handle is rendered when a range is selected", () => {
501+
render(<Spreadsheet {...EXAMPLE_PROPS} />);
502+
const element = getSpreadsheetElement();
503+
const firstCell = safeQuerySelector(
504+
element,
505+
"tr:nth-of-type(2) td:nth-of-type(1)"
506+
);
507+
const thirdCell = safeQuerySelector(
508+
element,
509+
"tr:nth-of-type(3) td:nth-of-type(2)"
510+
);
511+
// Select first cell
512+
fireEvent.mouseDown(firstCell);
513+
// Extend selection to create a range
514+
fireEvent.mouseDown(thirdCell, { shiftKey: true });
515+
// Check auto fill handle is rendered
516+
const autoFillHandle = safeQuerySelector(
517+
element,
518+
".Spreadsheet__auto-fill-handle"
519+
);
520+
expect(autoFillHandle).toBeInTheDocument();
521+
});
522+
test("mousedown on auto fill handle initiates auto fill mode", () => {
523+
render(<Spreadsheet {...EXAMPLE_PROPS} />);
524+
const element = getSpreadsheetElement();
525+
const cell = safeQuerySelector(element, "td");
526+
// Select a cell
527+
fireEvent.mouseDown(cell);
528+
// Get auto fill handle
529+
const autoFillHandle = safeQuerySelector(
530+
element,
531+
".Spreadsheet__auto-fill-handle"
532+
);
533+
// Get selected floating rect
534+
const selected = safeQuerySelector(
535+
element,
536+
".Spreadsheet__floating-rect--selected"
537+
);
538+
// Check auto filling class is not present initially
539+
expect(selected).not.toHaveClass(
540+
"Spreadsheet__floating-rect--auto-filling"
541+
);
542+
// Trigger auto fill
543+
fireEvent.mouseDown(autoFillHandle);
544+
// Check auto filling class is present
545+
expect(selected).toHaveClass("Spreadsheet__floating-rect--auto-filling");
546+
});
547+
test("auto fill continues numeric sequence 1, 2, 3", () => {
548+
const onChange = jest.fn();
549+
const data = createEmptyMatrix<CellType>(ROWS, COLUMNS);
550+
// Set up a numeric sequence: 1, 2
551+
const dataWithSequence = Matrix.set(
552+
{ row: 0, column: 0 },
553+
{ value: "1" },
554+
Matrix.set({ row: 1, column: 0 }, { value: "2" }, data)
555+
);
556+
render(<Spreadsheet data={dataWithSequence} onChange={onChange} />);
557+
const element = getSpreadsheetElement();
558+
// Select first cell (1)
559+
const firstCell = safeQuerySelector(
560+
element,
561+
"tr:nth-of-type(2) td:nth-of-type(1)"
562+
);
563+
fireEvent.mouseDown(firstCell);
564+
// Extend selection to second cell (2) to establish pattern
565+
const secondCell = safeQuerySelector(
566+
element,
567+
"tr:nth-of-type(3) td:nth-of-type(1)"
568+
);
569+
fireEvent.mouseDown(secondCell, { shiftKey: true });
570+
// Get auto fill handle
571+
const autoFillHandle = safeQuerySelector(
572+
element,
573+
".Spreadsheet__auto-fill-handle"
574+
);
575+
// Start auto fill
576+
fireEvent.mouseDown(autoFillHandle);
577+
// Extend selection to include two more cells (simulating dragging down)
578+
const fourthCell = safeQuerySelector(
579+
element,
580+
"tr:nth-of-type(5) td:nth-of-type(1)"
581+
);
582+
fireEvent.mouseDown(fourthCell, { shiftKey: true });
583+
// End auto fill (trigger mouseup on window)
584+
fireEvent.mouseUp(window);
585+
// Check onChange was called with auto-filled data
586+
expect(onChange).toHaveBeenCalled();
587+
const resultData = onChange.mock.calls[
588+
onChange.mock.calls.length - 1
589+
][0] as Matrix.Matrix<CellType>;
590+
// Verify the sequence: 1, 2, 3, 4
591+
expect(Matrix.get({ row: 0, column: 0 }, resultData)?.value).toBe("1");
592+
expect(Matrix.get({ row: 1, column: 0 }, resultData)?.value).toBe("2");
593+
expect(Matrix.get({ row: 2, column: 0 }, resultData)?.value).toBe(3);
594+
expect(Matrix.get({ row: 3, column: 0 }, resultData)?.value).toBe(4);
595+
});
479596
});
480597

481598
describe("Spreadsheet Ref Methods", () => {

src/actions.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export const KEY_DOWN = "KEY_DOWN";
3030
export const DRAG_START = "DRAG_START";
3131
export const DRAG_END = "DRAG_END";
3232
export const COMMIT = "COMMIT";
33+
export const AUTO_FILL_START = "AUTO_FILL_START";
34+
export const AUTO_FILL_END = "AUTO_FILL_END";
3335

3436
export type BaseAction<T extends string> = {
3537
type: T;
@@ -276,6 +278,18 @@ export function blur(): BlurAction {
276278
return { type: BLUR };
277279
}
278280

281+
export type AutoFillStartAction = BaseAction<typeof AUTO_FILL_START>;
282+
283+
export function autoFillStart(): AutoFillStartAction {
284+
return { type: AUTO_FILL_START };
285+
}
286+
287+
export type AutoFillEndAction = BaseAction<typeof AUTO_FILL_END>;
288+
289+
export function autoFillEnd(): AutoFillEndAction {
290+
return { type: AUTO_FILL_END };
291+
}
292+
279293
export type Action =
280294
| SetDataAction
281295
| SetCreateFormulaParserAction
@@ -298,4 +312,6 @@ export type Action =
298312
| EditAction
299313
| ViewAction
300314
| ClearAction
301-
| BlurAction;
315+
| BlurAction
316+
| AutoFillStartAction
317+
| AutoFillEndAction;

0 commit comments

Comments
 (0)