Skip to content

Commit 4f8393c

Browse files
authored
Add experimental support for custom event target (glideapps#1025)
* Add experimental support for cusotm event target * Fix tests * Update * Simplify comment * Improve * Update * Also add to outside click container
1 parent 5983dca commit 4f8393c

File tree

8 files changed

+229
-23
lines changed

8 files changed

+229
-23
lines changed

.storybook/main.js

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const path = require("path");
2+
3+
module.exports = {
4+
stories: ["../**/src/**/*.stories.tsx"],
5+
addons: [getAbsolutePath("@storybook/addon-storysource"), getAbsolutePath("@storybook/addon-controls")],
6+
7+
typescript: {
8+
reactDocgen: false,
9+
},
10+
11+
async viteFinal(config) {
12+
// We need to dynamically import these since they use ESM
13+
const { mergeConfig } = await import("vite");
14+
const linaria = (await import("@linaria/vite")).default;
15+
16+
return mergeConfig(config, {
17+
plugins: [linaria()],
18+
});
19+
},
20+
21+
framework: {
22+
name: getAbsolutePath("@storybook/react-vite"),
23+
options: {},
24+
},
25+
26+
docs: {
27+
autodocs: false,
28+
},
29+
};
30+
31+
function getAbsolutePath(value) {
32+
return path.dirname(require.resolve(path.join(value, "package.json")));
33+
}

package-lock.json

+9-9
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/core/src/data-editor/data-editor.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -4123,6 +4123,7 @@ const DataEditorImpl: React.ForwardRefRenderFunction<DataEditorRef, DataEditorPr
41234123
onFinishEditing={onFinishEditing}
41244124
markdownDivCreateNode={markdownDivCreateNode}
41254125
isOutsideClick={isOutsideClick}
4126+
customEventTarget={experimental?.eventTarget}
41264127
/>
41274128
</React.Suspense>
41284129
)}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import React from "react";
2+
import { DataEditorAll as DataEditor } from "../../data-editor-all.js";
3+
import type { GridColumn, Item, TextCell } from "../../internal/data-grid/data-grid-types.js";
4+
import { GridCellKind } from "../../internal/data-grid/data-grid-types.js";
5+
import { BeautifulWrapper, Description, defaultProps } from "../../data-editor/stories/utils.js";
6+
import { SimpleThemeWrapper } from "../../stories/story-utils.js";
7+
8+
export default {
9+
title: "Glide-Data-Grid/DataEditor Demos",
10+
11+
decorators: [
12+
(Story: React.ComponentType) => (
13+
<SimpleThemeWrapper>
14+
<BeautifulWrapper
15+
title="Custom Event Target"
16+
description={
17+
<Description>
18+
This example demonstrates using a custom event target for the data grid. All window events
19+
are blocked, but the grid still works because it&apos;s using the container div as its event
20+
target instead of window.
21+
</Description>
22+
}>
23+
<Story />
24+
</BeautifulWrapper>
25+
</SimpleThemeWrapper>
26+
),
27+
],
28+
};
29+
30+
export const CustomEventTarget: React.VFC = () => {
31+
// Create columns
32+
const [cols] = React.useState<GridColumn[]>(() => {
33+
return [
34+
{
35+
title: "Column A",
36+
id: "a",
37+
width: 150,
38+
},
39+
{
40+
title: "Column B",
41+
id: "b",
42+
width: 150,
43+
},
44+
{
45+
title: "Column C",
46+
id: "c",
47+
width: 150,
48+
},
49+
];
50+
});
51+
52+
// Create data
53+
const getCellContent = React.useCallback((cell: Item): TextCell => {
54+
const [col, row] = cell;
55+
return {
56+
kind: GridCellKind.Text,
57+
allowOverlay: true,
58+
displayData: `${col}, ${row}`,
59+
data: `${col}, ${row}`,
60+
};
61+
}, []);
62+
63+
// Create a ref for our custom event target container
64+
const containerRef = React.useRef<HTMLDivElement>(null);
65+
66+
// State to track if the container is mounted
67+
const [containerMounted, setContainerMounted] = React.useState(false);
68+
69+
// State to track window click attempts
70+
const [windowClickAttempts, setWindowClickAttempts] = React.useState(0);
71+
72+
// Update containerMounted state after the component mounts
73+
React.useEffect(() => {
74+
if (containerRef.current !== null) {
75+
setContainerMounted(true);
76+
}
77+
}, []);
78+
79+
// Block all window events
80+
React.useEffect(() => {
81+
const blockEvent = (e: Event) => {
82+
// Don't block events if they're inside our container
83+
if (containerRef.current && e.target instanceof Node && containerRef.current.contains(e.target)) {
84+
return;
85+
}
86+
87+
e.stopPropagation();
88+
e.stopImmediatePropagation();
89+
if (e.cancelable) {
90+
e.preventDefault();
91+
}
92+
93+
// Count click attempts outside the grid
94+
if (e.type === "click") {
95+
setWindowClickAttempts(prev => prev + 1);
96+
}
97+
};
98+
99+
// Block all mouse and touch events on window
100+
const events = ["mousedown", "mouseup", "mousemove", "click", "touchstart", "touchend", "touchmove"];
101+
102+
// Add event blockers to window
103+
for (const event of events) {
104+
window.addEventListener(event, blockEvent, true);
105+
}
106+
107+
return () => {
108+
// Clean up event blockers
109+
for (const event of events) {
110+
window.removeEventListener(event, blockEvent, true);
111+
}
112+
};
113+
}, []);
114+
115+
return (
116+
<div style={{ display: "flex", flexDirection: "column", height: "100%" }}>
117+
<div style={{ marginBottom: 10, padding: 10, backgroundColor: "#f0f0f0", borderRadius: 4 }}>
118+
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center" }}>
119+
<span style={{ color: "#666" }}>Window click attempts blocked: {windowClickAttempts}</span>
120+
<button
121+
onClick={() => alert("This button should not work if window events are blocked!")}
122+
style={{ padding: "5px 10px" }}>
123+
Try clicking me (should not work)
124+
</button>
125+
</div>
126+
<div style={{ marginTop: 10, fontSize: 14, color: "#666" }}>
127+
Try clicking outside the grid or on the button above - these clicks should be blocked. But the grid
128+
below should still be fully interactive!
129+
</div>
130+
</div>
131+
132+
<div
133+
ref={containerRef}
134+
style={{
135+
flex: 1,
136+
position: "relative",
137+
border: "2px solid #3c78d8",
138+
borderRadius: 4,
139+
padding: 15,
140+
}}>
141+
{containerMounted && (
142+
<DataEditor
143+
{...defaultProps}
144+
width="100%"
145+
height="100%"
146+
rows={1000}
147+
columns={cols}
148+
getCellContent={getCellContent}
149+
experimental={{
150+
eventTarget: containerRef.current as HTMLElement,
151+
}}
152+
/>
153+
)}
154+
</div>
155+
</div>
156+
);
157+
};

packages/core/src/internal/click-outside-container/click-outside-container.tsx

+14-9
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,30 @@ import * as React from "react";
22
interface Props extends React.HTMLAttributes<HTMLDivElement> {
33
onClickOutside: () => void;
44
isOutsideClick?: (event: MouseEvent | TouchEvent) => boolean;
5+
// If provided, it will use the provided element as the event target
6+
// instead of document.
7+
customEventTarget?: HTMLElement | Window | Document;
58
}
69

710
export default class ClickOutsideContainer extends React.PureComponent<Props> {
811
private wrapperRef = React.createRef<HTMLDivElement>();
912

1013
public componentDidMount() {
11-
document.addEventListener("touchend", this.clickOutside, true);
12-
document.addEventListener("mousedown", this.clickOutside, true);
13-
document.addEventListener("contextmenu", this.clickOutside, true);
14+
const eventTarget = this.props.customEventTarget ?? document;
15+
eventTarget.addEventListener("touchend", this.clickOutside, true);
16+
eventTarget.addEventListener("mousedown", this.clickOutside, true);
17+
eventTarget.addEventListener("contextmenu", this.clickOutside, true);
1418
}
1519

1620
public componentWillUnmount() {
17-
document.removeEventListener("touchend", this.clickOutside, true);
18-
document.removeEventListener("mousedown", this.clickOutside, true);
19-
document.removeEventListener("contextmenu", this.clickOutside, true);
21+
const eventTarget = this.props.customEventTarget ?? document;
22+
eventTarget.removeEventListener("touchend", this.clickOutside, true);
23+
eventTarget.removeEventListener("mousedown", this.clickOutside, true);
24+
eventTarget.removeEventListener("contextmenu", this.clickOutside, true);
2025
}
2126

22-
private clickOutside = (event: MouseEvent | TouchEvent) => {
23-
if (this.props.isOutsideClick && !this.props.isOutsideClick(event)) {
27+
private clickOutside = (event: Event) => {
28+
if (this.props.isOutsideClick && !this.props.isOutsideClick(event as MouseEvent | TouchEvent)) {
2429
return;
2530
}
2631
if (this.wrapperRef.current !== null && !this.wrapperRef.current.contains(event.target as Node | null)) {
@@ -37,7 +42,7 @@ export default class ClickOutsideContainer extends React.PureComponent<Props> {
3742
};
3843

3944
public render(): React.ReactNode {
40-
const { onClickOutside, isOutsideClick, ...rest } = this.props;
45+
const { onClickOutside, isOutsideClick, customEventTarget, ...rest } = this.props;
4146
return (
4247
<div {...rest} ref={this.wrapperRef}>
4348
{this.props.children}

packages/core/src/internal/data-grid-overlay-editor/data-grid-overlay-editor.tsx

+4-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ interface DataGridOverlayEditorProps {
4444
prevValue: GridCell
4545
) => boolean | ValidatedGridCell;
4646
readonly isOutsideClick?: (e: MouseEvent | TouchEvent) => boolean;
47+
readonly customEventTarget?: HTMLElement | Window | Document;
4748
}
4849

4950
const DataGridOverlayEditor: React.FunctionComponent<DataGridOverlayEditorProps> = p => {
@@ -65,6 +66,7 @@ const DataGridOverlayEditor: React.FunctionComponent<DataGridOverlayEditorProps>
6566
getCellRenderer,
6667
provideEditor,
6768
isOutsideClick,
69+
customEventTarget,
6870
} = p;
6971

7072
const [tempValue, setTempValueRaw] = React.useState<GridCell | undefined>(forceEditMode ? content : undefined);
@@ -218,7 +220,8 @@ const DataGridOverlayEditor: React.FunctionComponent<DataGridOverlayEditorProps>
218220
style={makeCSSStyle(theme)}
219221
className={className}
220222
onClickOutside={onClickOutside}
221-
isOutsideClick={isOutsideClick}>
223+
isOutsideClick={isOutsideClick}
224+
customEventTarget={customEventTarget}>
222225
<DataGridOverlayEditorStyle
223226
ref={ref}
224227
id={id}

packages/core/src/internal/data-grid/data-grid.tsx

+10-3
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,11 @@ export interface DataGridProps {
255255
readonly scrollbarWidthOverride?: number;
256256
readonly hyperWrapping?: boolean;
257257
readonly renderStrategy?: "single-buffer" | "double-buffer" | "direct";
258+
/**
259+
* Allows providing a custom event target for event listeners.
260+
* If not provided, the grid will use the window as the event target.
261+
*/
262+
readonly eventTarget?: HTMLElement | Window | Document;
258263
}
259264
| undefined;
260265

@@ -396,7 +401,7 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
396401
const cellXOffset = Math.max(freezeColumns, Math.min(columns.length - 1, cellXOffsetReal));
397402

398403
const ref = React.useRef<HTMLCanvasElement | null>(null);
399-
const windowEventTargetRef = React.useRef<Document | Window>(window);
404+
const windowEventTargetRef = React.useRef<HTMLElement | Window | Document>(experimental?.eventTarget ?? window);
400405
const windowEventTarget = windowEventTargetRef.current;
401406

402407
const imageLoader = imageWindowLoader;
@@ -1429,7 +1434,9 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
14291434
canvasRef.current = instance;
14301435
}
14311436

1432-
if (instance === null) {
1437+
if (experimental?.eventTarget) {
1438+
windowEventTargetRef.current = experimental.eventTarget;
1439+
} else if (instance === null) {
14331440
windowEventTargetRef.current = window;
14341441
} else {
14351442
const docRoot = instance.getRootNode();
@@ -1438,7 +1445,7 @@ const DataGrid: React.ForwardRefRenderFunction<DataGridRef, DataGridProps> = (p,
14381445
windowEventTargetRef.current = docRoot as any;
14391446
}
14401447
},
1441-
[canvasRef]
1448+
[canvasRef, experimental?.eventTarget]
14421449
);
14431450

14441451
const onDragStartImpl = React.useCallback(

setup-react-18-test.sh

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
#!/bin/bash
22

3-
npm i -D react@latest react-dom@latest @testing-library/react@latest @testing-library/react-hooks@latest @testing-library/[email protected] react-test-renderer@latest
3+
npm i -D react@latest react-dom@latest @testing-library/react@latest @testing-library/react-hooks@latest @testing-library/[email protected] react-test-renderer@latest @testing-library/dom

0 commit comments

Comments
 (0)