• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

glideapps / glide-data-grid / 17399553621

02 Sep 2025 09:39AM UTC coverage: 90.845% (+0.003%) from 90.842%
17399553621

push

github

lukasmasuch
Revert "fix: shift + enter key should move up in editor overlay (#1066)"

This reverts commit 0a1552c4f.

2974 of 3684 branches covered (80.73%)

11 of 15 new or added lines in 1 file covered. (73.33%)

1 existing line in 1 file now uncovered.

18168 of 19999 relevant lines covered (90.84%)

3163.81 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

89.49
/packages/core/src/internal/data-grid-overlay-editor/data-grid-overlay-editor.tsx
1
import * as React from "react";
1✔
2
import { createPortal } from "react-dom";
1✔
3

1✔
4
import ClickOutsideContainer from "../click-outside-container/click-outside-container.js";
1✔
5
import { makeCSSStyle, type Theme, ThemeContext } from "../../common/styles.js";
1✔
6
import type { GetCellRendererCallback } from "../../cells/cell-types.js";
1✔
7
import {
1✔
8
    type EditableGridCell,
1✔
9
    type GridCell,
1✔
10
    isEditableGridCell,
1✔
11
    isInnerOnlyCell,
1✔
12
    isObjectEditorCallbackResult,
1✔
13
    type Item,
1✔
14
    type ProvideEditorCallback,
1✔
15
    type ProvideEditorCallbackResult,
1✔
16
    type Rectangle,
1✔
17
    type ValidatedGridCell,
1✔
18
} from "../data-grid/data-grid-types.js";
1✔
19

1✔
20
import type { CellActivatedEventArgs } from "../data-grid/event-args.js";
1✔
21
import { DataGridOverlayEditorStyle } from "./data-grid-overlay-editor-style.js";
1✔
22
import type { OverlayImageEditorProps } from "./private/image-overlay-editor.js";
1✔
23
import { useStayOnScreen } from "./use-stay-on-screen.js";
1✔
24

1✔
25
type ImageEditorType = React.ComponentType<OverlayImageEditorProps>;
1✔
26

1✔
27
interface DataGridOverlayEditorProps {
1✔
28
    readonly target: Rectangle;
1✔
29
    readonly cell: Item;
1✔
30
    readonly content: GridCell;
1✔
31
    readonly className?: string;
1✔
32
    readonly id: string;
1✔
33
    readonly initialValue?: string;
1✔
34
    readonly bloom?: readonly [number, number];
1✔
35
    readonly theme: Theme;
1✔
36
    readonly onFinishEditing: (newCell: GridCell | undefined, movement: readonly [-1 | 0 | 1, -1 | 0 | 1]) => void;
1✔
37
    readonly forceEditMode: boolean;
1✔
38
    readonly highlight: boolean;
1✔
39
    readonly portalElementRef?: React.RefObject<HTMLElement>
1✔
40
    readonly imageEditorOverride?: ImageEditorType;
1✔
41
    readonly getCellRenderer: GetCellRendererCallback;
1✔
42
    readonly markdownDivCreateNode?: (content: string) => DocumentFragment;
1✔
43
    readonly provideEditor?: ProvideEditorCallback<GridCell>;
1✔
44
    readonly activation: CellActivatedEventArgs;
1✔
45
    readonly validateCell?: (
1✔
46
        cell: Item,
1✔
47
        newValue: EditableGridCell,
1✔
48
        prevValue: GridCell
1✔
49
    ) => boolean | ValidatedGridCell;
1✔
50
    readonly isOutsideClick?: (e: MouseEvent | TouchEvent) => boolean;
1✔
51
    readonly customEventTarget?: HTMLElement | Window | Document;
1✔
52
}
1✔
53

1✔
54
const DataGridOverlayEditor: React.FunctionComponent<DataGridOverlayEditorProps> = p => {
1✔
55
    const {
42✔
56
        target,
42✔
57
        content,
42✔
58
        onFinishEditing: onFinishEditingIn,
42✔
59
        forceEditMode,
42✔
60
        initialValue,
42✔
61
        imageEditorOverride,
42✔
62
        markdownDivCreateNode,
42✔
63
        highlight,
42✔
64
        className,
42✔
65
        theme,
42✔
66
        id,
42✔
67
        cell,
42✔
68
        bloom,
42✔
69
        portalElementRef,
42✔
70
        validateCell,
42✔
71
        getCellRenderer,
42✔
72
        provideEditor,
42✔
73
        isOutsideClick,
42✔
74
        customEventTarget,
42✔
75
        activation,
42✔
76
    } = p;
42✔
77

42✔
78
    const [tempValue, setTempValueRaw] = React.useState<GridCell | undefined>(forceEditMode ? content : undefined);
42✔
79
    const lastValueRef = React.useRef(tempValue ?? content);
42✔
80
    lastValueRef.current = tempValue ?? content;
42✔
81

42✔
82
    const [isValid, setIsValid] = React.useState(() => {
42✔
83
        if (validateCell === undefined) return true;
18!
84
        return !(isEditableGridCell(content) && validateCell?.(cell, content, lastValueRef.current) === false);
18✔
85
    });
42✔
86

42✔
87
    const onFinishEditing = React.useCallback<typeof onFinishEditingIn>(
42✔
88
        (newCell, movement) => {
42✔
89
            onFinishEditingIn(isValid ? newCell : undefined, movement);
12✔
90
        },
12✔
91
        [isValid, onFinishEditingIn]
42✔
92
    );
42✔
93

42✔
94
    const setTempValue = React.useCallback(
42✔
95
        (newVal: GridCell | undefined) => {
42✔
96
            if (validateCell !== undefined && newVal !== undefined && isEditableGridCell(newVal)) {
×
97
                const validResult = validateCell(cell, newVal, lastValueRef.current);
×
98
                if (validResult === false) {
×
99
                    setIsValid(false);
×
100
                } else if (typeof validResult === "object") {
×
101
                    newVal = validResult;
×
102
                    setIsValid(true);
×
103
                } else {
×
104
                    setIsValid(true);
×
105
                }
×
106
            }
×
107
            setTempValueRaw(newVal);
×
108
        },
×
109
        [cell, validateCell]
42✔
110
    );
42✔
111

42✔
112
    const finished = React.useRef(false);
42✔
113
    const customMotion = React.useRef<[-1 | 0 | 1, -1 | 0 | 1] | undefined>(undefined);
42✔
114

42✔
115
    const onClickOutside = React.useCallback(() => {
42✔
116
        onFinishEditing(tempValue, [0, 0]);
2✔
117
        finished.current = true;
2✔
118
    }, [tempValue, onFinishEditing]);
42✔
119

42✔
120
    const onEditorFinished = React.useCallback(
42✔
121
        (newValue: GridCell | undefined, movement?: readonly [-1 | 0 | 1, -1 | 0 | 1]) => {
42✔
122
            onFinishEditing(newValue, movement ?? customMotion.current ?? [0, 0]);
×
123
            finished.current = true;
×
124
        },
×
125
        [onFinishEditing]
42✔
126
    );
42✔
127

42✔
128
    const onKeyDown = React.useCallback(
42✔
129
        async (event: React.KeyboardEvent) => {
42✔
130
            let save = false;
10✔
131
            if (event.key === "Escape") {
10✔
132
                event.stopPropagation();
7✔
133
                event.preventDefault();
7✔
134
                customMotion.current = [0, 0];
7✔
135
            } else if (event.key === "Enter" && !event.shiftKey) {
10✔
136
                event.stopPropagation();
3✔
137
                event.preventDefault();
3✔
138
                customMotion.current = [0, 1];
3✔
139
                save = true;
3✔
140
            } else if (event.key === "Tab") {
3!
NEW
141
                event.stopPropagation();
×
NEW
142
                event.preventDefault();
×
NEW
143
                customMotion.current = [event.shiftKey ? -1 : 1, 0];
×
NEW
144
                save = true;
×
UNCOV
145
            }
×
146

10✔
147
            window.setTimeout(() => {
10✔
148
                if (!finished.current && customMotion.current !== undefined) {
10✔
149
                    onFinishEditing(save ? tempValue : undefined, customMotion.current);
10✔
150
                    finished.current = true;
10✔
151
                }
10✔
152
            }, 0);
10✔
153
        },
10✔
154
        [onFinishEditing, tempValue]
42✔
155
    );
42✔
156

42✔
157
    const targetValue = tempValue ?? content;
42✔
158

42✔
159
    const [editorProvider, useLabel] = React.useMemo((): [ProvideEditorCallbackResult<GridCell>, boolean] | [] => {
42✔
160
        if (isInnerOnlyCell(content)) return [];
18!
161
        const cellWithLocation = { ...content, location: cell, activation } as GridCell & {
18✔
162
            location: Item;
18✔
163
            activation: CellActivatedEventArgs;
18✔
164
        };
18✔
165
        const external = provideEditor?.(cellWithLocation);
18✔
166
        if (external !== undefined) return [external, false];
18!
167
        return [getCellRenderer(content)?.provideEditor?.(cellWithLocation), false];
18✔
168
    }, [cell, content, getCellRenderer, provideEditor, activation]);
42✔
169

42✔
170
    const { ref, style: stayOnScreenStyle } = useStayOnScreen();
42✔
171

42✔
172
    let pad = true;
42✔
173
    let editor: React.ReactNode;
42✔
174
    let style = true;
42✔
175
    let styleOverride: React.CSSProperties | undefined;
42✔
176

42✔
177
    if (editorProvider !== undefined) {
42✔
178
        pad = editorProvider.disablePadding !== true;
42✔
179
        style = editorProvider.disableStyling !== true;
42✔
180
        const isObjectEditor = isObjectEditorCallbackResult(editorProvider);
42✔
181
        if (isObjectEditor) {
42✔
182
            styleOverride = editorProvider.styleOverride;
40✔
183
        }
40✔
184
        const CustomEditor = isObjectEditor ? editorProvider.editor : editorProvider;
42✔
185
        editor = (
42✔
186
            <CustomEditor
42✔
187
                portalElementRef={portalElementRef}
42✔
188
                isHighlighted={highlight}
42✔
189
                activation={activation}
42✔
190
                onChange={setTempValue}
42✔
191
                value={targetValue}
42✔
192
                initialValue={initialValue}
42✔
193
                onFinishedEditing={onEditorFinished}
42✔
194
                validatedSelection={isEditableGridCell(targetValue) ? targetValue.selectionRange : undefined}
42!
195
                forceEditMode={forceEditMode}
42✔
196
                target={target}
42✔
197
                imageEditorOverride={imageEditorOverride}
42✔
198
                markdownDivCreateNode={markdownDivCreateNode}
42✔
199
                isValid={isValid}
42✔
200
                theme={theme}
42✔
201
            />
42✔
202
        );
42✔
203
    }
42✔
204

42✔
205
    styleOverride = { ...styleOverride, ...stayOnScreenStyle };
42✔
206

42✔
207
    // Consider imperatively creating and adding the element to the dom?
42✔
208
    const portalElement = portalElementRef?.current ?? document.getElementById("portal");
42!
209
    if (portalElement === null) {
42!
210
        // eslint-disable-next-line no-console
×
211
        console.error(
×
212
            'Cannot open Data Grid overlay editor, because portal not found. Please, either provide a portalElementRef or add `<div id="portal" />` as the last child of your `<body>`.'
×
213
        );
×
214
        return null;
×
215
    }
×
216

42✔
217
    let classWrap = style ? "gdg-style" : "gdg-unstyle";
42!
218
    if (!isValid) {
42✔
219
        classWrap += " gdg-invalid";
3✔
220
    }
3✔
221

42✔
222
    if (pad) {
42✔
223
        classWrap += " gdg-pad";
2✔
224
    }
2✔
225

42✔
226
    const bloomX = bloom?.[0] ?? 1;
42!
227
    const bloomY = bloom?.[1] ?? 1;
42!
228

42✔
229
    return createPortal(
42✔
230
        <ThemeContext.Provider value={theme}>
42✔
231
            <ClickOutsideContainer
42✔
232
                style={makeCSSStyle(theme)}
42✔
233
                className={className}
42✔
234
                onClickOutside={onClickOutside}
42✔
235
                isOutsideClick={isOutsideClick}
42✔
236
                customEventTarget={customEventTarget}>
42✔
237
                <DataGridOverlayEditorStyle
42✔
238
                    ref={ref}
42✔
239
                    id={id}
42✔
240
                    className={classWrap}
42✔
241
                    style={styleOverride}
42✔
242
                    as={useLabel === true ? "label" : undefined}
42!
243
                    targetX={target.x - bloomX}
42✔
244
                    targetY={target.y - bloomY}
42✔
245
                    targetWidth={target.width + bloomX * 2}
42✔
246
                    targetHeight={target.height + bloomY * 2}>
42✔
247
                    <div className="gdg-clip-region" onKeyDown={onKeyDown}>
42✔
248
                        {editor}
42✔
249
                    </div>
42✔
250
                </DataGridOverlayEditorStyle>
42✔
251
            </ClickOutsideContainer>
42✔
252
        </ThemeContext.Provider>,
42✔
253
        portalElement
42✔
254
    );
42✔
255
};
42✔
256

1✔
257
export default DataGridOverlayEditor;
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc