Skip to content

Autofill Handle #302

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

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
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
5 changes: 5 additions & 0 deletions src/ActiveCell.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,25 @@ const ActiveCell: React.FC<Props> = (props) => {
const rootRef = React.useRef<HTMLDivElement>(null);

const dispatch = useDispatch();

const setCellData = React.useCallback(
(active: Point.Point, data: Types.CellBase) =>
dispatch(Actions.setCellData(active, data)),
[dispatch]
);

const edit = React.useCallback(() => dispatch(Actions.edit()), [dispatch]);

const commit = React.useCallback(
(changes: Types.CommitChanges<Types.CellBase>) =>
dispatch(Actions.commit(changes)),
[dispatch]
);

const view = React.useCallback(() => {
dispatch(Actions.view());
}, [dispatch]);

const active = useSelector((state) => state.active);
const mode = useSelector((state) => state.mode);
const cell = useSelector((state) =>
Expand Down
25 changes: 19 additions & 6 deletions src/FloatingRect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,37 @@ export type Props = {
dimensions?: Types.Dimensions | null | undefined;
hidden?: boolean;
dragging?: boolean;
autoFilling?: boolean;
className: string;
children?: React.ReactNode;
};

const FloatingRect: React.FC<Props> = ({
dimensions,
dragging,
autoFilling,
hidden,
variant,
className,
children,
}) => {
const { width, height, top, left } = dimensions || {};
return (
<div
className={classnames("Spreadsheet__floating-rect", {
[`Spreadsheet__floating-rect--${variant}`]: variant,
"Spreadsheet__floating-rect--dragging": dragging,
"Spreadsheet__floating-rect--hidden": hidden,
})}
className={classnames(
"Spreadsheet__floating-rect",
{
[`Spreadsheet__floating-rect--${variant}`]: variant,
"Spreadsheet__floating-rect--dragging": dragging,
"Spreadsheet__floating-rect--auto-filling": autoFilling,
"Spreadsheet__floating-rect--hidden": hidden,
},
className
)}
style={{ width, height, top, left }}
/>
>
{children}
</div>
);
};

Expand Down
56 changes: 52 additions & 4 deletions src/Selected.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,17 @@
import * as React from "react";
import * as Actions from "./actions";
import * as Selection from "./selection";
import { getSelectedDimensions } from "./util";
import FloatingRect from "./FloatingRect";
import useSelector from "./use-selector";
import useDispatch from "./use-dispatch";
import classNames from "classnames";

const Selected: React.FC = () => {
const selected = useSelector((state) => state.selected);
const selectedSize = useSelector((state) =>
Selection.size(state.selected, state.model.data)
);
const dimensions = useSelector(
(state) =>
selected &&
Expand All @@ -17,17 +23,59 @@ const Selected: React.FC = () => {
)
);
const dragging = useSelector((state) => state.dragging);
const hidden = useSelector(
(state) => Selection.size(state.selected, state.model.data) < 2
);
const autoFilling = useSelector((state) => state.autoFilling);
const hidden = selectedSize === 0;

return (
<FloatingRect
variant="selected"
dimensions={dimensions}
dragging={dragging}
hidden={hidden}
/>
className={classNames({
"Spreadsheet__selected-single": selectedSize === 1,
})}
autoFilling={autoFilling}
>
{!hidden && <AutoFillHandle />}
</FloatingRect>
);
};

export default Selected;

const AutoFillHandle: React.FC = () => {
const dispatch = useDispatch();

const autoFillStart = React.useCallback(() => {
dispatch(Actions.autoFillStart());
}, [dispatch]);

const autoFillEnd = React.useCallback(() => {
dispatch(Actions.autoFillEnd());
}, [dispatch]);

const handleMouseDown = React.useCallback(
(event: React.MouseEvent) => {
event.stopPropagation();
event.preventDefault();

autoFillStart();

const handleMouseUp = () => {
autoFillEnd();
window.removeEventListener("mouseup", handleMouseUp);
};

window.addEventListener("mouseup", handleMouseUp);
},
[autoFillStart, autoFillEnd]
);

return (
<div
className="Spreadsheet__auto-fill-handle"
onMouseDown={handleMouseDown}
/>
);
};
23 changes: 22 additions & 1 deletion src/Spreadsheet.css
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,31 @@
border: 2px var(--outline-color) solid;
}

.Spreadsheet__floating-rect--dragging {
.Spreadsheet__floating-rect--selected.Spreadsheet__selected-single {
background: none;
border: none;
}

.Spreadsheet__floating-rect--selected.Spreadsheet__floating-rect--auto-filling {
background: none;
border: 2px var(--readonly-text-color) dashed;
}

.Spreadsheet__floating-rect--copied {
border: 2px var(--outline-color) dashed;
}

.Spreadsheet__auto-fill-handle {
position: absolute;
bottom: 0;
right: 0;
transform: translate(50%, 50%);
width: 8px;
height: 8px;
background: var(--outline-color);
border-radius: 50%;
box-shadow: var(--elevation);
cursor: pointer;
z-index: 10;
pointer-events: auto;
}
18 changes: 17 additions & 1 deletion src/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ export const KEY_DOWN = "KEY_DOWN";
export const DRAG_START = "DRAG_START";
export const DRAG_END = "DRAG_END";
export const COMMIT = "COMMIT";
export const AUTO_FILL_START = "AUTO_FILL_START";
export const AUTO_FILL_END = "AUTO_FILL_END";

export type BaseAction<T extends string> = {
type: T;
Expand Down Expand Up @@ -239,6 +241,18 @@ export function blur(): BlurAction {
return { type: BLUR };
}

export type AutoFillStartAction = BaseAction<typeof AUTO_FILL_START>;

export function autoFillStart(): AutoFillStartAction {
return { type: AUTO_FILL_START };
}

export type AutoFillEndAction = BaseAction<typeof AUTO_FILL_END>;

export function autoFillEnd(): AutoFillEndAction {
return { type: AUTO_FILL_END };
}

export type Action =
| SetDataAction
| SelectEntireRowAction
Expand All @@ -259,4 +273,6 @@ export type Action =
| EditAction
| ViewAction
| ClearAction
| BlurAction;
| BlurAction
| AutoFillStartAction
| AutoFillEndAction;
72 changes: 72 additions & 0 deletions src/reducer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import * as Types from "./types";
import * as Actions from "./actions";
import reducer, {
INITIAL_STATE,
autoFill,
getNextPoint,
hasKeyDownHandler,
isActiveReadOnly,
} from "./reducer";
Expand Down Expand Up @@ -253,3 +255,73 @@ describe("isActiveReadOnly", () => {
expect(isActiveReadOnly(state)).toBe(expected);
});
});

describe("autoFill", () => {
const cases: Array<
[
name: string,
data: Matrix.Matrix<Types.CellBase>,
selected: PointRange.PointRange,
active: Point.Point,
expected: Matrix.Matrix<Types.CellBase>
]
> = [
[
"increasing series",
[[{ value: 1 }], [{ value: 2 }]],
PointRange.create(Point.ORIGIN, { row: 2, column: 0 }),
Point.ORIGIN,
[[{ value: 1 }], [{ value: 2 }], [{ value: 3 }]],
],
[
"decreasing series",
[[{ value: 2 }], [{ value: 1 }]],
PointRange.create(Point.ORIGIN, { row: 2, column: 0 }),
Point.ORIGIN,
[[{ value: 2 }], [{ value: 1 }], [{ value: 0 }]],
],
[
"same value",
[[{ value: 1 }]],
PointRange.create(Point.ORIGIN, { row: 2, column: 0 }),
Point.ORIGIN,
[[{ value: 1 }], [{ value: 1 }], [{ value: 1 }]],
],
];
test.each(cases)("%s", (name, data, selected, active, expected) => {
expect(autoFill(data, selected, active)).toEqual(expected);
});
});

describe("getNextPoint", () => {
const cases: Array<
[
name: string,
active: Point.Point,
range: PointRange.PointRange,
expected: Point.Point | undefined
]
> = [
[
"returns undefined for single range",
Point.ORIGIN,
PointRange.create(Point.ORIGIN, Point.ORIGIN),
undefined,
],
[
"horizontal range",
Point.ORIGIN,
PointRange.create(Point.ORIGIN, { row: 0, column: 1 }),
{ row: 0, column: 1 },
],
[
"vertical range",
Point.ORIGIN,
PointRange.create(Point.ORIGIN, { row: 1, column: 0 }),
{ row: 1, column: 0 },
],
];
test.each(cases)("%s", (name, active, range, expected) => {
expect(getNextPoint(active, range)).toEqual(expected);
});
});
Loading