@topconsultnpm/sdkui-react 6.20.0-dev1.40 → 6.20.0-dev1.42
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/lib/components/NewComponents/ContextMenu/TMContextMenu.js +62 -3
- package/lib/components/NewComponents/ContextMenu/hooks.d.ts +1 -0
- package/lib/components/NewComponents/ContextMenu/hooks.js +9 -0
- package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +55 -1
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
2
|
import { useState, useRef, useEffect } from 'react';
|
|
3
3
|
import * as S from './styles';
|
|
4
|
-
import { useIsMobile, useMenuPosition } from './hooks';
|
|
4
|
+
import { useIsMobile, useMenuPosition, useIsIOS } from './hooks';
|
|
5
5
|
import { IconArrowLeft } from '../../../helper';
|
|
6
6
|
const TMContextMenu = ({ items, trigger = 'right', children, externalControl, keepOpenOnClick = false }) => {
|
|
7
7
|
const [menuState, setMenuState] = useState({
|
|
@@ -12,9 +12,12 @@ const TMContextMenu = ({ items, trigger = 'right', children, externalControl, ke
|
|
|
12
12
|
});
|
|
13
13
|
const [hoveredSubmenus, setHoveredSubmenus] = useState([]);
|
|
14
14
|
const isMobile = useIsMobile();
|
|
15
|
+
const isIOS = useIsIOS();
|
|
15
16
|
const menuRef = useRef(null);
|
|
16
17
|
const triggerRef = useRef(null);
|
|
17
18
|
const submenuTimeoutRef = useRef(null);
|
|
19
|
+
const longPressTimeoutRef = useRef(null);
|
|
20
|
+
const touchStartPos = useRef(null);
|
|
18
21
|
const { openLeft, openUp, isCalculated } = useMenuPosition(menuRef, menuState.position);
|
|
19
22
|
const handleClose = () => {
|
|
20
23
|
if (externalControl) {
|
|
@@ -131,6 +134,55 @@ const TMContextMenu = ({ items, trigger = 'right', children, externalControl, ke
|
|
|
131
134
|
});
|
|
132
135
|
}
|
|
133
136
|
};
|
|
137
|
+
// iOS-specific touch handlers for long press
|
|
138
|
+
const handleTouchStart = (e) => {
|
|
139
|
+
if (!isIOS || trigger !== 'right')
|
|
140
|
+
return;
|
|
141
|
+
const touch = e.touches[0];
|
|
142
|
+
touchStartPos.current = { x: touch.clientX, y: touch.clientY };
|
|
143
|
+
if (longPressTimeoutRef.current) {
|
|
144
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
145
|
+
}
|
|
146
|
+
longPressTimeoutRef.current = setTimeout(() => {
|
|
147
|
+
if (touchStartPos.current) {
|
|
148
|
+
if ('vibrate' in navigator) {
|
|
149
|
+
navigator.vibrate(50);
|
|
150
|
+
}
|
|
151
|
+
setMenuState({
|
|
152
|
+
visible: true,
|
|
153
|
+
position: { x: touchStartPos.current.x, y: touchStartPos.current.y },
|
|
154
|
+
submenuStack: [items],
|
|
155
|
+
parentNames: [],
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}, 500);
|
|
159
|
+
};
|
|
160
|
+
const handleTouchMove = (e) => {
|
|
161
|
+
if (!isIOS || !touchStartPos.current)
|
|
162
|
+
return;
|
|
163
|
+
const touch = e.touches[0];
|
|
164
|
+
const moveThreshold = 10; // pixels
|
|
165
|
+
// If finger moved too much, cancel long press
|
|
166
|
+
const deltaX = Math.abs(touch.clientX - touchStartPos.current.x);
|
|
167
|
+
const deltaY = Math.abs(touch.clientY - touchStartPos.current.y);
|
|
168
|
+
if (deltaX > moveThreshold || deltaY > moveThreshold) {
|
|
169
|
+
if (longPressTimeoutRef.current) {
|
|
170
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
171
|
+
longPressTimeoutRef.current = null;
|
|
172
|
+
}
|
|
173
|
+
touchStartPos.current = null;
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const handleTouchEnd = () => {
|
|
177
|
+
if (!isIOS)
|
|
178
|
+
return;
|
|
179
|
+
// Clear long press timeout if touch ended before long press completed
|
|
180
|
+
if (longPressTimeoutRef.current) {
|
|
181
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
182
|
+
longPressTimeoutRef.current = null;
|
|
183
|
+
}
|
|
184
|
+
touchStartPos.current = null;
|
|
185
|
+
};
|
|
134
186
|
const handleItemClick = (item) => {
|
|
135
187
|
if (item.disabled)
|
|
136
188
|
return;
|
|
@@ -216,6 +268,9 @@ const TMContextMenu = ({ items, trigger = 'right', children, externalControl, ke
|
|
|
216
268
|
if (submenuTimeoutRef.current) {
|
|
217
269
|
clearTimeout(submenuTimeoutRef.current);
|
|
218
270
|
}
|
|
271
|
+
if (longPressTimeoutRef.current) {
|
|
272
|
+
clearTimeout(longPressTimeoutRef.current);
|
|
273
|
+
}
|
|
219
274
|
};
|
|
220
275
|
}, []);
|
|
221
276
|
const renderMenuItems = (menuItems, depth = 0) => {
|
|
@@ -239,10 +294,14 @@ const TMContextMenu = ({ items, trigger = 'right', children, externalControl, ke
|
|
|
239
294
|
};
|
|
240
295
|
const currentMenu = menuState.submenuStack.at(-1) || items;
|
|
241
296
|
const currentParentName = menuState.parentNames.at(-1) || '';
|
|
242
|
-
return (_jsxs(_Fragment, { children: [!externalControl && children && (_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onKeyDown: (e) => {
|
|
297
|
+
return (_jsxs(_Fragment, { children: [!externalControl && children && (_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onTouchCancel: handleTouchEnd, onKeyDown: (e) => {
|
|
243
298
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
244
299
|
handleClick(e);
|
|
245
300
|
}
|
|
246
|
-
}, role: "button", tabIndex: 0, style: {
|
|
301
|
+
}, role: "button", tabIndex: 0, style: {
|
|
302
|
+
display: 'inline-block',
|
|
303
|
+
WebkitTouchCallout: isIOS ? 'none' : undefined,
|
|
304
|
+
WebkitUserSelect: isIOS ? 'none' : undefined,
|
|
305
|
+
}, children: children })), menuState.visible && (_jsxs(_Fragment, { children: [_jsxs(S.MenuContainer, { ref: menuRef, "$x": menuState.position.x, "$y": menuState.position.y, "$openLeft": openLeft, "$openUp": openUp, "$isPositioned": isCalculated, "$externalControl": !!externalControl, children: [isMobile && menuState.parentNames.length > 0 && (_jsxs(S.MobileMenuHeader, { children: [_jsx(S.BackButton, { onClick: handleBack, "aria-label": "Go back", children: _jsx(IconArrowLeft, {}) }), _jsx(S.HeaderTitle, { children: currentParentName })] })), renderMenuItems(currentMenu, 0)] }), !isMobile && hoveredSubmenus.map((submenu, idx) => (_jsx(S.Submenu, { "$parentRect": submenu.parentRect, "$openUp": submenu.openUp, "data-submenu": "true", onMouseEnter: handleSubmenuMouseEnter, onMouseLeave: () => handleMouseLeave(submenu.depth), children: renderMenuItems(submenu.items, submenu.depth) }, `submenu-${submenu.depth}-${idx}`)))] }))] }));
|
|
247
306
|
};
|
|
248
307
|
export default TMContextMenu;
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import { useState, useEffect, useLayoutEffect, useRef } from 'react';
|
|
2
|
+
export const useIsIOS = () => {
|
|
3
|
+
const [isIOS, setIsIOS] = useState(false);
|
|
4
|
+
useEffect(() => {
|
|
5
|
+
const iOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
6
|
+
(navigator.platform === 'MacIntel' && navigator.maxTouchPoints > 1);
|
|
7
|
+
setIsIOS(iOS);
|
|
8
|
+
}, []);
|
|
9
|
+
return isIOS;
|
|
10
|
+
};
|
|
2
11
|
export const useIsMobile = () => {
|
|
3
12
|
const [isMobile, setIsMobile] = useState(false);
|
|
4
13
|
useEffect(() => {
|
|
@@ -236,6 +236,8 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], isConstrained
|
|
|
236
236
|
setState(s => ({ ...s, isDragging: true }));
|
|
237
237
|
};
|
|
238
238
|
const handleGripDoubleClick = () => {
|
|
239
|
+
if (state.isConfigMode)
|
|
240
|
+
return;
|
|
239
241
|
toggleOrientation();
|
|
240
242
|
};
|
|
241
243
|
const handleMouseMove = useCallback((e) => {
|
|
@@ -313,6 +315,58 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], isConstrained
|
|
|
313
315
|
}
|
|
314
316
|
});
|
|
315
317
|
};
|
|
318
|
+
// Auto-reposition when entering edit mode to ensure Apply/Undo buttons are visible
|
|
319
|
+
useEffect(() => {
|
|
320
|
+
if (!state.isConfigMode || !floatingRef.current)
|
|
321
|
+
return;
|
|
322
|
+
requestAnimationFrame(() => {
|
|
323
|
+
if (!floatingRef.current)
|
|
324
|
+
return;
|
|
325
|
+
const floating = floatingRef.current.getBoundingClientRect();
|
|
326
|
+
let newX = state.position.x;
|
|
327
|
+
let newY = state.position.y;
|
|
328
|
+
let needsUpdate = false;
|
|
329
|
+
if (isConstrained && containerRef.current) {
|
|
330
|
+
const container = containerRef.current.getBoundingClientRect();
|
|
331
|
+
if (state.orientation === 'horizontal') {
|
|
332
|
+
if (state.position.x + floating.width > container.width) {
|
|
333
|
+
newX = Math.max(0, container.width - floating.width);
|
|
334
|
+
needsUpdate = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
if (state.position.y + floating.height > container.height) {
|
|
339
|
+
newY = Math.max(0, container.height - floating.height);
|
|
340
|
+
needsUpdate = true;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
if (state.orientation === 'horizontal') {
|
|
346
|
+
if (state.position.x + floating.width > window.innerWidth) {
|
|
347
|
+
newX = Math.max(0, window.innerWidth - floating.width - 10);
|
|
348
|
+
needsUpdate = true;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
else {
|
|
352
|
+
if (state.position.y + floating.height > window.innerHeight) {
|
|
353
|
+
newY = Math.max(0, window.innerHeight - floating.height - 10);
|
|
354
|
+
needsUpdate = true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (needsUpdate) {
|
|
359
|
+
setState(s => ({
|
|
360
|
+
...s,
|
|
361
|
+
position: { x: newX, y: newY },
|
|
362
|
+
}));
|
|
363
|
+
// Update snapshot position to the corrected position so Undo restores to visible position
|
|
364
|
+
if (stateSnapshot.current) {
|
|
365
|
+
stateSnapshot.current.position = { x: newX, y: newY };
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
}, [state.isConfigMode, state.orientation, isConstrained]);
|
|
316
370
|
const handleUndo = () => {
|
|
317
371
|
if (stateSnapshot.current) {
|
|
318
372
|
setState(s => ({
|
|
@@ -320,7 +374,7 @@ const TMFloatingMenuBar = ({ containerRef, contextMenuItems = [], isConstrained
|
|
|
320
374
|
items: [...stateSnapshot.current.items],
|
|
321
375
|
orientation: stateSnapshot.current.orientation,
|
|
322
376
|
position: { ...stateSnapshot.current.position },
|
|
323
|
-
isConfigMode: true,
|
|
377
|
+
isConfigMode: true,
|
|
324
378
|
}));
|
|
325
379
|
}
|
|
326
380
|
};
|