Skip to content

Commit 7a5e6a2

Browse files
authored
Entire Selection (#161)
Support selecting entire rows, columns, and tables
1 parent ba570b1 commit 7a5e6a2

25 files changed

Lines changed: 1606 additions & 233 deletions

src/Cell.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ import * as React from "react";
22
import classnames from "classnames";
33
import * as PointSet from "./point-set";
44
import * as PointMap from "./point-map";
5-
import * as PointRange from "./point-range";
65
import * as Matrix from "./matrix";
76
import * as Types from "./types";
87
import * as Point from "./point";
98
import * as Actions from "./actions";
9+
import * as Selection from "./selection";
1010
import { isActive, getOffsetRect } from "./util";
1111
import useDispatch from "./use-dispatch";
1212
import useSelector from "./use-selector";
@@ -95,7 +95,7 @@ export const Cell: React.FC<Types.CellComponentProps> = ({
9595
};
9696

9797
export const enhance = (
98-
CellComponent: React.FC<Types.CellComponentProps>
98+
CellComponent: React.ComponentType<Types.CellComponentProps>
9999
): React.FC<
100100
Omit<
101101
Types.CellComponentProps,
@@ -137,7 +137,7 @@ export const enhance = (
137137
Matrix.get({ row, column }, state.data)
138138
);
139139
const selected = useSelector((state) =>
140-
state.selected ? PointRange.has(state.selected, { row, column }) : false
140+
Selection.hasPoint(state.selected, state.data, { row, column })
141141
);
142142
const dragging = useSelector((state) => state.dragging);
143143
const copied = useSelector((state) =>

src/ColumnIndicator.test.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,33 @@
44

55
import React from "react";
66
import { render, screen } from "@testing-library/react";
7+
import * as Types from "./types";
78
import ColumnIndicator from "./ColumnIndicator";
89

10+
const EXAMPLE_PROPS: Types.ColumnIndicatorProps = {
11+
column: 0,
12+
selected: false,
13+
onSelect: jest.fn(),
14+
};
15+
916
describe("<ColumnIndicator />", () => {
1017
test("renders with column letter", () => {
11-
render(<ColumnIndicator column={0} />);
18+
render(<ColumnIndicator {...EXAMPLE_PROPS} />);
1219
expect(document.querySelectorAll("th.Spreadsheet__header").length).toBe(1);
1320
expect(screen.queryByText("A")).not.toBeNull();
1421
});
1522
test("renders with label", () => {
16-
render(<ColumnIndicator column={0} label="Example Label" />);
23+
render(<ColumnIndicator {...EXAMPLE_PROPS} label="Example Label" />);
1724
expect(document.querySelectorAll("th.Spreadsheet__header").length).toBe(1);
1825
expect(screen.queryByText("Example Label")).not.toBeNull();
1926
});
27+
test("calls onSelect", () => {
28+
render(<ColumnIndicator {...EXAMPLE_PROPS} />);
29+
expect(document.querySelectorAll("th.Spreadsheet__header").length).toBe(1);
30+
const indicator = document.querySelector(
31+
"th.Spreadsheet__header"
32+
) as HTMLTableCellElement;
33+
indicator.click();
34+
expect(EXAMPLE_PROPS.onSelect).toBeCalledTimes(1);
35+
});
2036
});

src/ColumnIndicator.tsx

Lines changed: 54 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,60 @@
11
import * as React from "react";
2+
import classNames from "classnames";
23
import { columnIndexToLabel } from "hot-formula-parser";
34
import * as Types from "./types";
5+
import * as Actions from "./actions";
6+
import * as Selection from "./selection";
7+
import useDispatch from "./use-dispatch";
8+
import useSelector from "./use-selector";
49

5-
const ColumnIndicator: Types.ColumnIndicatorComponent = ({ column, label }) => (
6-
<th className="Spreadsheet__header">
7-
{label !== undefined ? label : columnIndexToLabel(String(column))}
8-
</th>
9-
);
10+
const ColumnIndicator: Types.ColumnIndicatorComponent = ({
11+
column,
12+
label,
13+
selected,
14+
onSelect,
15+
}) => {
16+
const handleClick = React.useCallback(
17+
(event: React.MouseEvent) => {
18+
onSelect(column, event.shiftKey);
19+
},
20+
[onSelect, column]
21+
);
22+
return (
23+
<th
24+
className={classNames("Spreadsheet__header", {
25+
"Spreadsheet__header--selected": selected,
26+
})}
27+
onClick={handleClick}
28+
tabIndex={0}
29+
>
30+
{label !== undefined ? label : columnIndexToLabel(String(column))}
31+
</th>
32+
);
33+
};
1034

1135
export default ColumnIndicator;
36+
37+
export const enhance = (
38+
ColumnIndicatorComponent: Types.ColumnIndicatorComponent
39+
): React.FC<Omit<Types.ColumnIndicatorProps, "selected" | "onSelect">> => {
40+
return function ColumnIndicatorWrapper(props) {
41+
const dispatch = useDispatch();
42+
const selectEntireColumn = React.useCallback(
43+
(column: number, extend: boolean) =>
44+
dispatch(Actions.selectEntireColumn(column, extend)),
45+
[dispatch]
46+
);
47+
const selected = useSelector(
48+
(state) =>
49+
Selection.hasEntireColumn(state.selected, props.column) ||
50+
Selection.isEntireTable(state.selected)
51+
);
52+
return (
53+
<ColumnIndicatorComponent
54+
{...props}
55+
selected={selected}
56+
onSelect={selectEntireColumn}
57+
/>
58+
);
59+
};
60+
};

src/CorderIndicator.test.tsx

Lines changed: 0 additions & 14 deletions
This file was deleted.

src/CornerIndicator.test.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* @jest-environment jsdom
3+
*/
4+
5+
import React from "react";
6+
import { render } from "@testing-library/react";
7+
import * as Types from "./types";
8+
import CornerIndicator from "./CornerIndicator";
9+
10+
const EXAMPLE_PROPS: Types.CornerIndicatorProps = {
11+
selected: false,
12+
onSelect: jest.fn(),
13+
};
14+
15+
describe("<CornerIndicator />", () => {
16+
test("renders", () => {
17+
render(<CornerIndicator {...EXAMPLE_PROPS} />);
18+
expect(document.querySelectorAll("th.Spreadsheet__header").length).toBe(1);
19+
});
20+
test("calls onSelect", () => {
21+
render(<CornerIndicator {...EXAMPLE_PROPS} />);
22+
expect(document.querySelectorAll("th.Spreadsheet__header").length).toBe(1);
23+
const indicator = document.querySelector(
24+
"th.Spreadsheet__header"
25+
) as HTMLTableCellElement;
26+
indicator.click();
27+
expect(EXAMPLE_PROPS.onSelect).toBeCalledTimes(1);
28+
});
29+
});

src/CornerIndicator.tsx

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,49 @@
11
import * as React from "react";
2+
import classNames from "classnames";
3+
import * as Actions from "./actions";
4+
import * as Selection from "./selection";
25
import * as Types from "./types";
6+
import useDispatch from "./use-dispatch";
7+
import useSelector from "./use-selector";
38

4-
const CornerIndicator: Types.CornerIndicatorComponent = () => (
5-
<th className="Spreadsheet__header" />
6-
);
9+
const CornerIndicator: Types.CornerIndicatorComponent = ({
10+
selected,
11+
onSelect,
12+
}) => {
13+
const handleClick = React.useCallback(() => {
14+
onSelect();
15+
}, [onSelect]);
16+
return (
17+
<th
18+
className={classNames("Spreadsheet__header", {
19+
"Spreadsheet__header--selected": selected,
20+
})}
21+
onClick={handleClick}
22+
tabIndex={0}
23+
/>
24+
);
25+
};
726

827
export default CornerIndicator;
28+
29+
export const enhance = (
30+
CornerIndicatorComponent: Types.CornerIndicatorComponent
31+
): React.FC<Omit<Types.CornerIndicatorProps, "selected" | "onSelect">> => {
32+
return function CornerIndicatorWrapper(props) {
33+
const dispatch = useDispatch();
34+
const selectEntireTable = React.useCallback(
35+
() => dispatch(Actions.selectEntireTable()),
36+
[dispatch]
37+
);
38+
const selected = useSelector((state) =>
39+
Selection.isEntireTable(state.selected)
40+
);
41+
return (
42+
<CornerIndicatorComponent
43+
{...props}
44+
selected={selected}
45+
onSelect={selectEntireTable}
46+
/>
47+
);
48+
};
49+
};

src/RowIndicator.test.tsx

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,33 @@
44

55
import React from "react";
66
import { render, screen } from "@testing-library/react";
7+
import * as Types from "./types";
78
import RowIndicator from "./RowIndicator";
89

10+
const EXAMPLE_PROPS: Types.RowIndicatorProps = {
11+
row: 0,
12+
selected: false,
13+
onSelect: jest.fn(),
14+
};
15+
916
describe("<RowIndicator />", () => {
1017
test("renders with row number", () => {
11-
render(<RowIndicator row={0} />);
18+
render(<RowIndicator {...EXAMPLE_PROPS} />);
1219
expect(document.querySelectorAll("th.Spreadsheet__header").length).toBe(1);
1320
expect(screen.queryByText("1")).not.toBeNull();
1421
});
1522
test("renders with label", () => {
16-
render(<RowIndicator row={0} label="Example Label" />);
23+
render(<RowIndicator {...EXAMPLE_PROPS} label="Example Label" />);
1724
expect(document.querySelectorAll("th.Spreadsheet__header").length).toBe(1);
1825
expect(screen.queryByText("Example Label")).not.toBeNull();
1926
});
27+
test("calls on select", () => {
28+
render(<RowIndicator {...EXAMPLE_PROPS} />);
29+
expect(document.querySelectorAll("th.Spreadsheet__header").length).toBe(1);
30+
const indicator = document.querySelector(
31+
"th.Spreadsheet__header"
32+
) as HTMLTableCellElement;
33+
indicator.click();
34+
expect(EXAMPLE_PROPS.onSelect).toBeCalledTimes(1);
35+
});
2036
});

src/RowIndicator.tsx

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,60 @@
11
import * as React from "react";
2+
import classNames from "classnames";
3+
import * as Actions from "./actions";
24
import * as Types from "./types";
5+
import * as Selection from "./selection";
6+
import useDispatch from "./use-dispatch";
7+
import useSelector from "./use-selector";
38

4-
const RowIndicator: Types.RowIndicatorComponent = ({ row, label }) => (
5-
<th className="Spreadsheet__header">
6-
{label !== undefined ? label : row + 1}
7-
</th>
8-
);
9+
const RowIndicator: Types.RowIndicatorComponent = ({
10+
row,
11+
label,
12+
selected,
13+
onSelect,
14+
}) => {
15+
const handleClick = React.useCallback(
16+
(event: React.MouseEvent) => {
17+
onSelect(row, event.shiftKey);
18+
},
19+
[onSelect, row]
20+
);
21+
22+
return (
23+
<th
24+
className={classNames("Spreadsheet__header", {
25+
"Spreadsheet__header--selected": selected,
26+
})}
27+
onClick={handleClick}
28+
tabIndex={0}
29+
>
30+
{label !== undefined ? label : row + 1}
31+
</th>
32+
);
33+
};
934

1035
export default RowIndicator;
36+
37+
export const enhance = (
38+
RowIndicatorComponent: Types.RowIndicatorComponent
39+
): React.FC<Omit<Types.RowIndicatorProps, "selected" | "onSelect">> => {
40+
return function RowIndicatorWrapper(props) {
41+
const dispatch = useDispatch();
42+
const selected = useSelector(
43+
(state) =>
44+
Selection.hasEntireRow(state.selected, props.row) ||
45+
Selection.isEntireTable(state.selected)
46+
);
47+
const selectEntireRow = React.useCallback(
48+
(row: number, extend: boolean) =>
49+
dispatch(Actions.selectEntireRow(row, extend)),
50+
[dispatch]
51+
);
52+
return (
53+
<RowIndicatorComponent
54+
{...props}
55+
selected={selected}
56+
onSelect={selectEntireRow}
57+
/>
58+
);
59+
};
60+
};

src/Selected.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,25 @@
11
import * as React from "react";
2+
import * as Selection from "./selection";
3+
import { getSelectedDimensions } from "./util";
24
import FloatingRect from "./FloatingRect";
3-
import * as PointRange from "./point-range";
4-
import { getRangeDimensions } from "./util";
55
import useSelector from "./use-selector";
66

77
const Selected: React.FC = () => {
88
const selected = useSelector((state) => state.selected);
99
const dimensions = useSelector(
1010
(state) =>
1111
selected &&
12-
getRangeDimensions(state.rowDimensions, state.columnDimensions, selected)
12+
getSelectedDimensions(
13+
state.rowDimensions,
14+
state.columnDimensions,
15+
state.data,
16+
state.selected
17+
)
1318
);
1419
const dragging = useSelector((state) => state.dragging);
15-
const hidden = React.useMemo(() => isHidden(selected), [selected]);
20+
const hidden = useSelector(
21+
(state) => Selection.size(state.selected, state.data) < 2
22+
);
1623
return (
1724
<FloatingRect
1825
variant="selected"
@@ -24,7 +31,3 @@ const Selected: React.FC = () => {
2431
};
2532

2633
export default Selected;
27-
28-
export function isHidden(selected: PointRange.PointRange | null): boolean {
29-
return !selected || Boolean(selected && PointRange.size(selected) === 1);
30-
}

src/Spreadsheet.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,11 @@
7474
font: inherit;
7575
}
7676

77+
.Spreadsheet__header--selected {
78+
background: #5f6268;
79+
color: #fff;
80+
}
81+
7782
.Spreadsheet__header,
7883
.Spreadsheet__data-viewer,
7984
.Spreadsheet__data-editor input {

0 commit comments

Comments
 (0)