@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.
@@ -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: { display: 'inline-block' }, 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}`)))] }))] }));
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,3 +1,4 @@
1
+ export declare const useIsIOS: () => boolean;
1
2
  export declare const useIsMobile: () => boolean;
2
3
  export declare const useClickOutside: (callback: () => void) => import("react").RefObject<HTMLDivElement>;
3
4
  interface Position {
@@ -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, // Stay in edit mode
377
+ isConfigMode: true,
324
378
  }));
325
379
  }
326
380
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@topconsultnpm/sdkui-react",
3
- "version": "6.20.0-dev1.40",
3
+ "version": "6.20.0-dev1.42",
4
4
  "description": "",
5
5
  "scripts": {
6
6
  "test": "echo \"Error: no test specified\" && exit 1",