@zargaryanvh/react-component-inspector 1.0.7 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/InspectionTooltip.js +98 -104
- package/dist/InspectionWrapper.js +18 -77
- package/dist/autoInspection.js +50 -8
- package/package.json +1 -1
|
@@ -5,6 +5,24 @@ import ContentCopyIcon from "@mui/icons-material/ContentCopy";
|
|
|
5
5
|
import { useInspection } from "./InspectionContext";
|
|
6
6
|
import { formatMetadataForClipboard, getParentWithGap, getTooltipHowToFindInfo } from "./inspection";
|
|
7
7
|
import { parseInspectionMetadata } from "./autoInspection";
|
|
8
|
+
/**
|
|
9
|
+
* Helper: Clamp a tooltip rect to the viewport so it's always fully visible
|
|
10
|
+
*/
|
|
11
|
+
const clampToViewport = (x, y, w, h, padding = 10) => {
|
|
12
|
+
const vw = window.innerWidth;
|
|
13
|
+
const vh = window.innerHeight;
|
|
14
|
+
let cx = x;
|
|
15
|
+
let cy = y;
|
|
16
|
+
if (cx + w > vw - padding)
|
|
17
|
+
cx = vw - w - padding;
|
|
18
|
+
if (cy + h > vh - padding)
|
|
19
|
+
cy = vh - h - padding;
|
|
20
|
+
if (cx < padding)
|
|
21
|
+
cx = padding;
|
|
22
|
+
if (cy < padding)
|
|
23
|
+
cy = padding;
|
|
24
|
+
return { x: cx, y: cy };
|
|
25
|
+
};
|
|
8
26
|
/**
|
|
9
27
|
* Helper: Get element text content (first 100 chars)
|
|
10
28
|
*/
|
|
@@ -84,49 +102,31 @@ const ComponentMetadataSection = ({ metadata }) => {
|
|
|
84
102
|
*/
|
|
85
103
|
export const InspectionTooltip = () => {
|
|
86
104
|
const { isInspectionActive, isLocked, isMobile, isMarginPaddingMode, hoveredComponent, hoveredElement } = useInspection();
|
|
87
|
-
const [position, setPosition] = useState({ x: 0, y: 0 });
|
|
88
105
|
const [stablePosition, setStablePosition] = useState(null);
|
|
89
106
|
const [copied, setCopied] = useState(null);
|
|
107
|
+
const [tooltipSize, setTooltipSize] = useState({ w: 400, h: 300 });
|
|
90
108
|
const tooltipRef = useRef(null);
|
|
91
|
-
//
|
|
109
|
+
// Measure tooltip size so we can clamp it to the viewport precisely
|
|
92
110
|
useEffect(() => {
|
|
93
|
-
if (!
|
|
94
|
-
setStablePosition(null);
|
|
95
|
-
return;
|
|
96
|
-
}
|
|
97
|
-
// If locked, don't update position at all - keep it completely fixed
|
|
98
|
-
if (isLocked) {
|
|
111
|
+
if (!tooltipRef.current || typeof ResizeObserver === "undefined")
|
|
99
112
|
return;
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
if (
|
|
104
|
-
|
|
105
|
-
const mouseX = e.clientX;
|
|
106
|
-
const mouseY = e.clientY;
|
|
107
|
-
// Create a "dead zone" around tooltip (80px padding for easier clicking)
|
|
108
|
-
const deadZone = {
|
|
109
|
-
left: tooltipRect.left - 80,
|
|
110
|
-
right: tooltipRect.right + 80,
|
|
111
|
-
top: tooltipRect.top - 80,
|
|
112
|
-
bottom: tooltipRect.bottom + 80,
|
|
113
|
-
};
|
|
114
|
-
// If mouse is in dead zone, keep position stable (don't update)
|
|
115
|
-
if (mouseX >= deadZone.left &&
|
|
116
|
-
mouseX <= deadZone.right &&
|
|
117
|
-
mouseY >= deadZone.top &&
|
|
118
|
-
mouseY <= deadZone.bottom) {
|
|
119
|
-
return; // Don't update position - keep it stable
|
|
120
|
-
}
|
|
113
|
+
const el = tooltipRef.current;
|
|
114
|
+
const observer = new ResizeObserver(() => {
|
|
115
|
+
const r = el.getBoundingClientRect();
|
|
116
|
+
if (r.width > 0 && r.height > 0) {
|
|
117
|
+
setTooltipSize((prev) => prev.w === r.width && prev.h === r.height ? prev : { w: r.width, h: r.height });
|
|
121
118
|
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
});
|
|
120
|
+
observer.observe(el);
|
|
121
|
+
return () => observer.disconnect();
|
|
122
|
+
}, [isInspectionActive, hoveredComponent]);
|
|
123
|
+
// Reset stored cursor position when inspection turns off so we don't reuse
|
|
124
|
+
// a stale value on the next activation.
|
|
125
|
+
useEffect(() => {
|
|
126
|
+
if (!isInspectionActive || !hoveredElement) {
|
|
125
127
|
setStablePosition(null);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
return () => window.removeEventListener("mousemove", updatePosition);
|
|
129
|
-
}, [isInspectionActive, isLocked, hoveredElement, stablePosition]);
|
|
128
|
+
}
|
|
129
|
+
}, [isInspectionActive, hoveredElement]);
|
|
130
130
|
// If no component metadata but we have an element, create basic metadata
|
|
131
131
|
const displayComponent = hoveredComponent || (hoveredElement ? {
|
|
132
132
|
componentName: hoveredElement.tagName.toLowerCase(),
|
|
@@ -136,84 +136,78 @@ export const InspectionTooltip = () => {
|
|
|
136
136
|
propsSignature: "default",
|
|
137
137
|
sourceFile: "DOM",
|
|
138
138
|
} : null);
|
|
139
|
-
//
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
139
|
+
// Compute anchor position once per element change. The value is intentionally
|
|
140
|
+
// NOT recomputed when tooltipSize updates (ResizeObserver may fire several
|
|
141
|
+
// times as the tooltip's content renders) — recomputing on every size update
|
|
142
|
+
// makes the tooltip visibly slide and shrink as it settles. Pin location is
|
|
143
|
+
// chosen as: below the element if there's room, otherwise above, right, left,
|
|
144
|
+
// and finally a fixed corner for elements that fill the viewport.
|
|
145
|
+
const tooltipSizeRef = useRef(tooltipSize);
|
|
146
|
+
tooltipSizeRef.current = tooltipSize;
|
|
147
|
+
const [anchoredPosition, setAnchoredPosition] = useState(null);
|
|
148
|
+
useEffect(() => {
|
|
149
|
+
if (!hoveredElement || !document.body.contains(hoveredElement)) {
|
|
150
|
+
setAnchoredPosition(null);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
143
153
|
const padding = 10;
|
|
144
|
-
const
|
|
145
|
-
const
|
|
154
|
+
const gap = 8;
|
|
155
|
+
const { w, h } = tooltipSizeRef.current;
|
|
156
|
+
const vw = window.innerWidth;
|
|
157
|
+
const vh = window.innerHeight;
|
|
146
158
|
const rect = hoveredElement.getBoundingClientRect();
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
if (y < padding)
|
|
157
|
-
y = padding;
|
|
158
|
-
return { x, y };
|
|
159
|
-
}, [hoveredElement]);
|
|
160
|
-
// Calculate adjusted position to avoid going off-screen
|
|
161
|
-
// Use stable position if available; on mobile or when locked use element-based position so tooltip appears over inspected area
|
|
162
|
-
const adjustedPosition = useMemo(() => {
|
|
163
|
-
if (!displayComponent) {
|
|
164
|
-
return { x: position.x + 15, y: position.y + 15 };
|
|
165
|
-
}
|
|
166
|
-
// If we have a stable position, use it (don't recalculate)
|
|
167
|
-
if (stablePosition) {
|
|
168
|
-
return stablePosition;
|
|
169
|
-
}
|
|
170
|
-
// Mobile or just locked: place tooltip near the inspected element (not cursor)
|
|
171
|
-
if ((isMobile || isLocked) && positionFromElement) {
|
|
172
|
-
return positionFromElement;
|
|
159
|
+
const spaceBelow = vh - rect.bottom;
|
|
160
|
+
const spaceAbove = rect.top;
|
|
161
|
+
const spaceRight = vw - rect.right;
|
|
162
|
+
const spaceLeft = rect.left;
|
|
163
|
+
let x;
|
|
164
|
+
let y;
|
|
165
|
+
if (spaceBelow >= h + gap + padding) {
|
|
166
|
+
y = rect.bottom + gap;
|
|
167
|
+
x = Math.min(Math.max(rect.left, padding), vw - w - padding);
|
|
173
168
|
}
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
let y = position.y + 15;
|
|
178
|
-
// Adjust if tooltip would go off right edge (estimate width)
|
|
179
|
-
const estimatedWidth = 400; // Approximate tooltip width
|
|
180
|
-
if (x + estimatedWidth > window.innerWidth - padding) {
|
|
181
|
-
x = position.x - estimatedWidth - 15;
|
|
169
|
+
else if (spaceAbove >= h + gap + padding) {
|
|
170
|
+
y = rect.top - h - gap;
|
|
171
|
+
x = Math.min(Math.max(rect.left, padding), vw - w - padding);
|
|
182
172
|
}
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
y = position.y - estimatedHeight - 15;
|
|
173
|
+
else if (spaceRight >= w + gap + padding) {
|
|
174
|
+
x = rect.right + gap;
|
|
175
|
+
y = Math.min(Math.max(rect.top, padding), vh - h - padding);
|
|
187
176
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
177
|
+
else if (spaceLeft >= w + gap + padding) {
|
|
178
|
+
x = rect.left - w - gap;
|
|
179
|
+
y = Math.min(Math.max(rect.top, padding), vh - h - padding);
|
|
191
180
|
}
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
181
|
+
else {
|
|
182
|
+
const cx = (rect.left + rect.right) / 2;
|
|
183
|
+
const cy = (rect.top + rect.bottom) / 2;
|
|
184
|
+
x = cx < vw / 2 ? vw - w - padding : padding;
|
|
185
|
+
y = cy < vh / 2 ? vh - h - padding : padding;
|
|
195
186
|
}
|
|
196
|
-
|
|
197
|
-
}, [
|
|
198
|
-
|
|
199
|
-
|
|
187
|
+
setAnchoredPosition(clampToViewport(x, y, w, h));
|
|
188
|
+
}, [hoveredElement]);
|
|
189
|
+
const positionFromElement = anchoredPosition;
|
|
190
|
+
// Position is anchored to the hovered element's rect, not the cursor. This
|
|
191
|
+
// keeps the tooltip static while the cursor moves around inside one element
|
|
192
|
+
// (e.g. when inspecting a large container) and only repositions when the
|
|
193
|
+
// hovered element changes.
|
|
194
|
+
const adjustedPosition = useMemo(() => {
|
|
195
|
+
const w = tooltipSize.w;
|
|
196
|
+
const h = tooltipSize.h;
|
|
197
|
+
if (positionFromElement)
|
|
198
|
+
return positionFromElement;
|
|
199
|
+
return clampToViewport(15, 15, w, h);
|
|
200
|
+
}, [positionFromElement, tooltipSize]);
|
|
201
|
+
// When the lock is engaged we freeze whatever position was being shown at
|
|
202
|
+
// that moment so it doesn't follow subsequent element changes.
|
|
200
203
|
useEffect(() => {
|
|
201
|
-
|
|
202
|
-
// When locked, ensure we have a stable position
|
|
203
|
-
if (isLocked && !stablePosition && displayComponent && adjustedPosition.x > 0 && adjustedPosition.y > 0) {
|
|
204
|
+
if (isLocked && !stablePosition && adjustedPosition.x > 0 && adjustedPosition.y > 0) {
|
|
204
205
|
setStablePosition(adjustedPosition);
|
|
205
|
-
return;
|
|
206
206
|
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
lastComponentIdRef.current = currentComponentId;
|
|
210
|
-
// Set stable position after a small delay to ensure tooltip is rendered
|
|
211
|
-
const timer = setTimeout(() => {
|
|
212
|
-
setStablePosition(adjustedPosition);
|
|
213
|
-
}, 50);
|
|
214
|
-
return () => clearTimeout(timer);
|
|
207
|
+
if (!isLocked && stablePosition) {
|
|
208
|
+
setStablePosition(null);
|
|
215
209
|
}
|
|
216
|
-
}, [
|
|
210
|
+
}, [isLocked, adjustedPosition, stablePosition]);
|
|
217
211
|
const fallbackCopy = (text) => {
|
|
218
212
|
const textarea = document.createElement("textarea");
|
|
219
213
|
textarea.value = text;
|
|
@@ -334,7 +328,7 @@ export const InspectionTooltip = () => {
|
|
|
334
328
|
backgroundColor: "rgba(18, 18, 18, 0.95)",
|
|
335
329
|
backdropFilter: "blur(8px)",
|
|
336
330
|
border: "1px solid rgba(255, 255, 255, 0.1)",
|
|
337
|
-
transition:
|
|
331
|
+
transition: "none",
|
|
338
332
|
}, children: [_jsxs(Box, { sx: { display: "flex", justifyContent: "space-between", alignItems: "flex-start", mb: 1 }, children: [_jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 1 }, children: [_jsx(Typography, { variant: "subtitle2", sx: { color: "#fff", fontWeight: 600, fontSize: "0.875rem" }, children: "Component Inspector" }), isMarginPaddingMode && (_jsx(Typography, { variant: "caption", sx: { color: "#ff9800", fontSize: "0.65rem" }, children: "M/P mode" })), isLocked && (_jsx(Typography, { variant: "caption", sx: { color: "#4caf50", fontSize: "0.7rem", fontStyle: "italic" }, children: "(Locked - Release H to unlock)" }))] }), _jsxs(Box, { sx: { display: "flex", alignItems: "center", gap: 0.75 }, children: [_jsx(MuiTooltip, { title: copied === "component" ? "Copied!" : "Copy component", children: _jsx(IconButton, { size: "small", onClick: (e) => { e.preventDefault(); handleCopy("component"); }, onPointerDown: (e) => { e.preventDefault(); handleCopy("component"); }, sx: {
|
|
339
333
|
color: copied === "component" ? "#4caf50" : "#fff",
|
|
340
334
|
"&:hover": { backgroundColor: "rgba(255, 255, 255, 0.1)" },
|
|
@@ -1,92 +1,33 @@
|
|
|
1
1
|
import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import React from "react";
|
|
3
|
-
import { useInspection } from "./InspectionContext";
|
|
4
3
|
import { generateComponentId, formatPropsSignature, getComponentName, getNextInstanceIndex, } from "./inspection";
|
|
5
4
|
/**
|
|
6
5
|
* Wrapper component that adds inspection metadata to a component
|
|
7
6
|
*/
|
|
8
7
|
export const InspectionWrapper = ({ componentName, variant, role, usagePath, props, sourceFile, children, }) => {
|
|
9
|
-
const { isInspectionActive, setHoveredComponent } = useInspection();
|
|
10
8
|
const instanceIndex = React.useMemo(() => getNextInstanceIndex(componentName), [componentName]);
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
const metadata = {
|
|
16
|
-
componentName,
|
|
17
|
-
componentId: generateComponentId(componentName, instanceIndex),
|
|
18
|
-
variant,
|
|
19
|
-
role,
|
|
20
|
-
usagePath,
|
|
21
|
-
instanceIndex,
|
|
22
|
-
propsSignature: formatPropsSignature(props),
|
|
23
|
-
sourceFile,
|
|
24
|
-
};
|
|
25
|
-
setHoveredComponent(metadata, target);
|
|
26
|
-
};
|
|
27
|
-
const handleMouseLeave = () => {
|
|
28
|
-
if (!isInspectionActive)
|
|
29
|
-
return;
|
|
30
|
-
setHoveredComponent(null, null);
|
|
31
|
-
};
|
|
32
|
-
// Touch handlers for mobile
|
|
33
|
-
const handleTouchStart = (e) => {
|
|
34
|
-
if (!isInspectionActive)
|
|
35
|
-
return;
|
|
36
|
-
const target = e.currentTarget;
|
|
37
|
-
const metadata = {
|
|
38
|
-
componentName,
|
|
39
|
-
componentId: generateComponentId(componentName, instanceIndex),
|
|
40
|
-
variant,
|
|
41
|
-
role,
|
|
42
|
-
usagePath,
|
|
43
|
-
instanceIndex,
|
|
44
|
-
propsSignature: formatPropsSignature(props),
|
|
45
|
-
sourceFile,
|
|
46
|
-
};
|
|
47
|
-
setHoveredComponent(metadata, target);
|
|
48
|
-
};
|
|
49
|
-
const handleTouchEnd = () => {
|
|
50
|
-
// Don't clear on touch end - let autoInspection handle it
|
|
51
|
-
};
|
|
52
|
-
// Clone the child element and add inspection handlers
|
|
9
|
+
// Attach data-inspection-* attributes only; the global autoInspection mousemove
|
|
10
|
+
// handler is the single source of truth for hover tracking. This avoids the
|
|
11
|
+
// border flicker caused by competing mouseEnter/mouseLeave handlers firing
|
|
12
|
+
// back and forth when the cursor sits on the edge between two components.
|
|
53
13
|
if (!React.isValidElement(children)) {
|
|
54
14
|
return _jsx(_Fragment, { children: children });
|
|
55
15
|
}
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
const existingOnTouchEnd = existingProps.onTouchEnd;
|
|
61
|
-
const childWithProps = React.cloneElement(children, {
|
|
62
|
-
onMouseEnter: (e) => {
|
|
63
|
-
handleMouseEnter(e);
|
|
64
|
-
if (existingOnMouseEnter) {
|
|
65
|
-
existingOnMouseEnter(e);
|
|
66
|
-
}
|
|
67
|
-
},
|
|
68
|
-
onMouseLeave: (e) => {
|
|
69
|
-
handleMouseLeave();
|
|
70
|
-
if (existingOnMouseLeave) {
|
|
71
|
-
existingOnMouseLeave(e);
|
|
72
|
-
}
|
|
73
|
-
},
|
|
74
|
-
onTouchStart: (e) => {
|
|
75
|
-
handleTouchStart(e);
|
|
76
|
-
if (existingOnTouchStart) {
|
|
77
|
-
existingOnTouchStart(e);
|
|
78
|
-
}
|
|
79
|
-
},
|
|
80
|
-
onTouchEnd: (e) => {
|
|
81
|
-
handleTouchEnd();
|
|
82
|
-
if (existingOnTouchEnd) {
|
|
83
|
-
existingOnTouchEnd(e);
|
|
84
|
-
}
|
|
85
|
-
},
|
|
86
|
-
"data-inspection-id": generateComponentId(componentName, instanceIndex),
|
|
16
|
+
const componentId = generateComponentId(componentName, instanceIndex);
|
|
17
|
+
const propsSignature = formatPropsSignature(props);
|
|
18
|
+
const dataAttrs = {
|
|
19
|
+
"data-inspection-id": componentId,
|
|
87
20
|
"data-inspection-name": componentName,
|
|
88
|
-
|
|
89
|
-
|
|
21
|
+
"data-inspection-usage-path": usagePath,
|
|
22
|
+
"data-inspection-instance": String(instanceIndex),
|
|
23
|
+
"data-inspection-props": propsSignature,
|
|
24
|
+
"data-inspection-file": sourceFile,
|
|
25
|
+
};
|
|
26
|
+
if (variant)
|
|
27
|
+
dataAttrs["data-inspection-variant"] = variant;
|
|
28
|
+
if (role)
|
|
29
|
+
dataAttrs["data-inspection-role"] = role;
|
|
30
|
+
return _jsx(_Fragment, { children: React.cloneElement(children, dataAttrs) });
|
|
90
31
|
};
|
|
91
32
|
/**
|
|
92
33
|
* HOC to wrap a component with inspection capabilities
|
package/dist/autoInspection.js
CHANGED
|
@@ -259,16 +259,52 @@ export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isL
|
|
|
259
259
|
const isTouchDevice = 'ontouchstart' in window ||
|
|
260
260
|
navigator.maxTouchPoints > 0 ||
|
|
261
261
|
/Android|webOS|iPhone|iPad|iPod|BlackBerry|IEMobile|Opera Mini/i.test(navigator.userAgent);
|
|
262
|
+
// De-dupe: if mousemove resolves to the same DOM element as last time, skip
|
|
263
|
+
// the state update. Combined with the throttle below this is what prevents
|
|
264
|
+
// the tooltip from flickering when the cursor sits on a border between two
|
|
265
|
+
// wrapped components and rapid mouseenter/leave events would otherwise toggle
|
|
266
|
+
// the hovered element back and forth.
|
|
267
|
+
let lastInspectedElement = null;
|
|
268
|
+
const dedupedSetHoveredComponent = (metadata, element) => {
|
|
269
|
+
if (element === lastInspectedElement)
|
|
270
|
+
return;
|
|
271
|
+
lastInspectedElement = element;
|
|
272
|
+
setHoveredComponent(metadata, element);
|
|
273
|
+
};
|
|
274
|
+
// Throttle mousemove processing to ~30 fps. DOM walking on every pixel of
|
|
275
|
+
// movement (the previous behavior) is wasted work and amplifies any border
|
|
276
|
+
// jitter into visible flicker.
|
|
277
|
+
let pendingTarget = null;
|
|
278
|
+
let throttleHandle = null;
|
|
279
|
+
const flushPendingTarget = () => {
|
|
280
|
+
throttleHandle = null;
|
|
281
|
+
const t = pendingTarget;
|
|
282
|
+
pendingTarget = null;
|
|
283
|
+
if (t)
|
|
284
|
+
inspectElement(t, isInspectionActive, isLocked, dedupedSetHoveredComponent);
|
|
285
|
+
};
|
|
262
286
|
const handleMouseMove = (e) => {
|
|
263
|
-
if (!isInspectionActive)
|
|
287
|
+
if (!isInspectionActive)
|
|
264
288
|
return;
|
|
265
|
-
|
|
266
|
-
// If locked, don't update on mouse move - keep current tooltip fixed
|
|
267
|
-
if (isLocked) {
|
|
289
|
+
if (isLocked)
|
|
268
290
|
return;
|
|
291
|
+
// Sticky: while the cursor is still within the bounds of the element we
|
|
292
|
+
// last anchored the tooltip to, do nothing. This stops the tooltip from
|
|
293
|
+
// sliding around as the deepest DOM element under the cursor changes
|
|
294
|
+
// pixel-by-pixel inside a large container.
|
|
295
|
+
if (lastInspectedElement && document.body.contains(lastInspectedElement)) {
|
|
296
|
+
const rect = lastInspectedElement.getBoundingClientRect();
|
|
297
|
+
if (e.clientX >= rect.left &&
|
|
298
|
+
e.clientX <= rect.right &&
|
|
299
|
+
e.clientY >= rect.top &&
|
|
300
|
+
e.clientY <= rect.bottom) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
269
303
|
}
|
|
270
|
-
|
|
271
|
-
|
|
304
|
+
pendingTarget = e.target;
|
|
305
|
+
if (throttleHandle !== null)
|
|
306
|
+
return;
|
|
307
|
+
throttleHandle = window.setTimeout(flushPendingTarget, 32);
|
|
272
308
|
};
|
|
273
309
|
// Touch move handler for mobile devices
|
|
274
310
|
const handleTouchMove = (e) => {
|
|
@@ -280,7 +316,7 @@ export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isL
|
|
|
280
316
|
const touch = e.touches[0];
|
|
281
317
|
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
282
318
|
if (target) {
|
|
283
|
-
inspectElement(target, isInspectionActive, isLocked,
|
|
319
|
+
inspectElement(target, isInspectionActive, isLocked, dedupedSetHoveredComponent);
|
|
284
320
|
}
|
|
285
321
|
};
|
|
286
322
|
// Touch start handler for mobile - inspect on touch
|
|
@@ -294,17 +330,19 @@ export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isL
|
|
|
294
330
|
const touch = e.touches[0];
|
|
295
331
|
const target = document.elementFromPoint(touch.clientX, touch.clientY);
|
|
296
332
|
if (target) {
|
|
297
|
-
inspectElement(target, isInspectionActive, isLocked,
|
|
333
|
+
inspectElement(target, isInspectionActive, isLocked, dedupedSetHoveredComponent);
|
|
298
334
|
}
|
|
299
335
|
};
|
|
300
336
|
const handleMouseLeave = () => {
|
|
301
337
|
if (isInspectionActive && !isLocked) {
|
|
338
|
+
lastInspectedElement = null;
|
|
302
339
|
setHoveredComponent(null, null);
|
|
303
340
|
}
|
|
304
341
|
};
|
|
305
342
|
const handleTouchEnd = () => {
|
|
306
343
|
// Clear inspection on touch end (unless locked)
|
|
307
344
|
if (isInspectionActive && !isLocked) {
|
|
345
|
+
lastInspectedElement = null;
|
|
308
346
|
setHoveredComponent(null, null);
|
|
309
347
|
}
|
|
310
348
|
};
|
|
@@ -320,6 +358,10 @@ export const setupAutoInspection = (setHoveredComponent, isInspectionActive, isL
|
|
|
320
358
|
return () => {
|
|
321
359
|
window.removeEventListener("mousemove", handleMouseMove);
|
|
322
360
|
document.removeEventListener("mouseleave", handleMouseLeave);
|
|
361
|
+
if (throttleHandle !== null) {
|
|
362
|
+
clearTimeout(throttleHandle);
|
|
363
|
+
throttleHandle = null;
|
|
364
|
+
}
|
|
323
365
|
if (isTouchDevice) {
|
|
324
366
|
window.removeEventListener("touchmove", handleTouchMove);
|
|
325
367
|
window.removeEventListener("touchstart", handleTouchStart);
|
package/package.json
CHANGED