@topconsultnpm/sdkui-react 6.20.0-dev1.11 → 6.20.0-dev1.111

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.
Files changed (177) hide show
  1. package/lib/assets/Toppy-help-center.png +0 -0
  2. package/lib/assets/headergradient.svg +87 -0
  3. package/lib/components/NewComponents/ContextMenu/TMContextMenu.js +285 -28
  4. package/lib/components/NewComponents/ContextMenu/hooks.d.ts +8 -1
  5. package/lib/components/NewComponents/ContextMenu/hooks.js +80 -8
  6. package/lib/components/NewComponents/ContextMenu/index.d.ts +3 -0
  7. package/lib/components/NewComponents/ContextMenu/index.js +2 -0
  8. package/lib/components/NewComponents/ContextMenu/styles.d.ts +9 -1
  9. package/lib/components/NewComponents/ContextMenu/styles.js +157 -37
  10. package/lib/components/NewComponents/ContextMenu/types.d.ts +14 -1
  11. package/lib/components/NewComponents/ContextMenu/useLongPress.d.ts +21 -0
  12. package/lib/components/NewComponents/ContextMenu/useLongPress.js +112 -0
  13. package/lib/components/NewComponents/FloatingMenuBar/TMFloatingMenuBar.js +563 -112
  14. package/lib/components/NewComponents/FloatingMenuBar/styles.d.ts +21 -5
  15. package/lib/components/NewComponents/FloatingMenuBar/styles.js +210 -58
  16. package/lib/components/NewComponents/FloatingMenuBar/types.d.ts +8 -2
  17. package/lib/components/base/TMAccordionNew.js +35 -14
  18. package/lib/components/base/TMCustomButton.js +61 -17
  19. package/lib/components/base/TMDataGrid.d.ts +7 -4
  20. package/lib/components/base/TMDataGrid.js +153 -11
  21. package/lib/components/base/TMDropDownMenu.js +19 -18
  22. package/lib/components/base/TMFileManager.d.ts +4 -3
  23. package/lib/components/base/TMFileManager.js +32 -24
  24. package/lib/components/base/TMFileManagerDataGridView.d.ts +3 -2
  25. package/lib/components/base/TMFileManagerDataGridView.js +1 -11
  26. package/lib/components/base/TMFileManagerThumbnailItems.d.ts +7 -1
  27. package/lib/components/base/TMFileManagerThumbnailItems.js +5 -2
  28. package/lib/components/base/TMFileManagerThumbnailsView.d.ts +17 -4
  29. package/lib/components/base/TMFileManagerThumbnailsView.js +18 -6
  30. package/lib/components/base/TMFileManagerUtils.d.ts +0 -12
  31. package/lib/components/base/TMListView.js +33 -15
  32. package/lib/components/base/TMPanel.d.ts +1 -1
  33. package/lib/components/base/TMPanel.js +1 -1
  34. package/lib/components/base/TMTreeView.d.ts +2 -1
  35. package/lib/components/base/TMTreeView.js +33 -26
  36. package/lib/components/choosers/TMDistinctValues.js +2 -2
  37. package/lib/components/choosers/TMInvoiceRetrieveFormats.js +1 -1
  38. package/lib/components/choosers/TMMetadataChooser.js +8 -1
  39. package/lib/components/choosers/TMOrderRetrieveFormats.js +1 -1
  40. package/lib/components/choosers/TMUserChooser.d.ts +0 -5
  41. package/lib/components/choosers/TMUserChooser.js +25 -45
  42. package/lib/components/editors/TMDateBox.js +18 -9
  43. package/lib/components/editors/TMLocalizedTextBox.d.ts +3 -1
  44. package/lib/components/editors/TMLocalizedTextBox.js +16 -14
  45. package/lib/components/editors/TMMetadataTextBox.d.ts +9 -0
  46. package/lib/components/editors/TMMetadataTextBox.js +92 -0
  47. package/lib/components/editors/TMMetadataValues.js +23 -5
  48. package/lib/components/editors/TMTextArea.js +18 -30
  49. package/lib/components/editors/TMTextBox.d.ts +1 -1
  50. package/lib/components/editors/TMTextBox.js +6 -3
  51. package/lib/components/editors/TMTextExpression.js +6 -91
  52. package/lib/components/features/archive/TMArchive.js +2 -2
  53. package/lib/components/features/assistant/TMToppyDraggableHelpCenter.d.ts +15 -0
  54. package/lib/components/features/assistant/TMToppyDraggableHelpCenter.js +460 -0
  55. package/lib/components/features/assistant/TMToppySpeechBubble.d.ts +11 -0
  56. package/lib/components/features/assistant/TMToppySpeechBubble.js +126 -0
  57. package/lib/components/features/documents/TMDcmtForm.d.ts +14 -2
  58. package/lib/components/features/documents/TMDcmtForm.js +457 -206
  59. package/lib/components/features/documents/TMDcmtPreview.js +44 -110
  60. package/lib/components/features/documents/TMDcmtTasks.js +9 -9
  61. package/lib/components/features/documents/TMMasterDetailDcmts.js +38 -53
  62. package/lib/components/features/documents/TMRelationViewer.d.ts +1 -1
  63. package/lib/components/features/documents/TMRelationViewer.js +2 -2
  64. package/lib/components/features/search/TMDcmtCheckoutInfoForm.d.ts +8 -0
  65. package/lib/components/features/search/{TMSearchResultCheckoutInfoForm.js → TMDcmtCheckoutInfoForm.js} +2 -2
  66. package/lib/components/features/search/TMSavedQuerySelector.js +72 -67
  67. package/lib/components/features/search/TMSearch.d.ts +3 -0
  68. package/lib/components/features/search/TMSearch.js +50 -11
  69. package/lib/components/features/search/TMSearchQueryPanel.d.ts +1 -0
  70. package/lib/components/features/search/TMSearchQueryPanel.js +29 -21
  71. package/lib/components/features/search/TMSearchResult.d.ts +3 -0
  72. package/lib/components/features/search/TMSearchResult.js +208 -250
  73. package/lib/components/features/search/TMSearchResultsMenuItems.d.ts +3 -3
  74. package/lib/components/features/search/TMSearchResultsMenuItems.js +205 -169
  75. package/lib/components/features/search/TMSignSettingsForm.js +1 -1
  76. package/lib/components/features/search/TMSignatureInfoContent.d.ts +6 -0
  77. package/lib/components/features/search/TMSignatureInfoContent.js +140 -0
  78. package/lib/components/features/search/TMViewHistoryDcmt.js +2 -2
  79. package/lib/components/features/tasks/TMTaskForm.js +20 -1
  80. package/lib/components/features/tasks/TMTasksAgenda.d.ts +3 -1
  81. package/lib/components/features/tasks/TMTasksAgenda.js +48 -9
  82. package/lib/components/features/tasks/TMTasksCalendar.d.ts +2 -0
  83. package/lib/components/features/tasks/TMTasksCalendar.js +19 -7
  84. package/lib/components/features/tasks/TMTasksUtils.d.ts +2 -2
  85. package/lib/components/features/tasks/TMTasksUtils.js +43 -36
  86. package/lib/components/features/tasks/TMTasksView.js +28 -19
  87. package/lib/components/features/workflow/TMWorkflowPopup.d.ts +33 -2
  88. package/lib/components/features/workflow/TMWorkflowPopup.js +139 -34
  89. package/lib/components/features/workflow/diagram/DiagramItemComponent.d.ts +2 -0
  90. package/lib/components/features/workflow/diagram/DiagramItemComponent.js +12 -7
  91. package/lib/components/features/workflow/diagram/DiagramItemForm.js +1 -1
  92. package/lib/components/features/workflow/diagram/RecipientList.js +3 -2
  93. package/lib/components/features/workflow/diagram/WFDiagram.d.ts +4 -0
  94. package/lib/components/features/workflow/diagram/WFDiagram.js +164 -13
  95. package/lib/components/forms/Login/LoginValidatorService.d.ts +2 -0
  96. package/lib/components/forms/Login/LoginValidatorService.js +7 -2
  97. package/lib/components/forms/Login/TMLoginForm.js +34 -6
  98. package/lib/components/forms/TMChooserForm.js +1 -1
  99. package/lib/components/grids/TMBlogsPost.js +56 -31
  100. package/lib/components/grids/TMRecentsManager.js +20 -10
  101. package/lib/components/index.d.ts +6 -3
  102. package/lib/components/index.js +6 -3
  103. package/lib/components/query/TMQueryEditor.d.ts +2 -1
  104. package/lib/components/query/TMQueryEditor.js +92 -92
  105. package/lib/components/settings/SettingsAppearance.d.ts +2 -1
  106. package/lib/components/settings/SettingsAppearance.js +99 -30
  107. package/lib/components/sidebar/TMHeader.js +7 -7
  108. package/lib/components/sidebar/TMSidebar.d.ts +0 -1
  109. package/lib/components/sidebar/TMSidebar.js +16 -44
  110. package/lib/components/sidebar/TMSidebarItem.js +34 -17
  111. package/lib/components/viewers/TMDataListItemViewer.d.ts +2 -1
  112. package/lib/components/viewers/TMDataListItemViewer.js +35 -71
  113. package/lib/components/viewers/TMDataUserIdItemViewer.d.ts +8 -0
  114. package/lib/components/viewers/TMDataUserIdItemViewer.js +39 -0
  115. package/lib/css/tm-sdkui.css +1 -1
  116. package/lib/helper/SDKUI_Globals.d.ts +22 -0
  117. package/lib/helper/SDKUI_Globals.js +10 -1
  118. package/lib/helper/SDKUI_Localizator.d.ts +17 -1
  119. package/lib/helper/SDKUI_Localizator.js +167 -1
  120. package/lib/helper/TMCommandsContextMenu.d.ts +4 -2
  121. package/lib/helper/TMCommandsContextMenu.js +15 -4
  122. package/lib/helper/TMIcons.d.ts +4 -0
  123. package/lib/helper/TMIcons.js +13 -3
  124. package/lib/helper/TMPdfViewer.d.ts +8 -0
  125. package/lib/helper/TMPdfViewer.js +373 -0
  126. package/lib/helper/checkinCheckoutManager.d.ts +31 -1
  127. package/lib/helper/checkinCheckoutManager.js +112 -30
  128. package/lib/helper/devextremeCustomMessages.d.ts +30 -0
  129. package/lib/helper/devextremeCustomMessages.js +30 -0
  130. package/lib/helper/helpers.d.ts +28 -1
  131. package/lib/helper/helpers.js +130 -3
  132. package/lib/helper/index.d.ts +2 -0
  133. package/lib/helper/index.js +2 -0
  134. package/lib/helper/queryHelper.d.ts +1 -1
  135. package/lib/helper/queryHelper.js +33 -3
  136. package/lib/helper/workItemsHelper.d.ts +6 -0
  137. package/lib/helper/workItemsHelper.js +230 -0
  138. package/lib/hooks/useCheckInOutOperations.d.ts +28 -0
  139. package/lib/hooks/useCheckInOutOperations.js +223 -0
  140. package/lib/hooks/useDataListItem.d.ts +12 -0
  141. package/lib/hooks/useDataListItem.js +132 -0
  142. package/lib/hooks/useDataUserIdItem.d.ts +10 -0
  143. package/lib/hooks/useDataUserIdItem.js +96 -0
  144. package/lib/hooks/useMetadataExpression.d.ts +19 -0
  145. package/lib/hooks/useMetadataExpression.js +99 -0
  146. package/lib/hooks/useSettingsFeedback.d.ts +11 -0
  147. package/lib/hooks/useSettingsFeedback.js +38 -0
  148. package/lib/hooks/useWorkflowApprove.d.ts +4 -0
  149. package/lib/hooks/useWorkflowApprove.js +14 -1
  150. package/lib/index.d.ts +1 -0
  151. package/lib/index.js +3 -2
  152. package/lib/services/platform_services.d.ts +3 -3
  153. package/lib/ts/types.d.ts +61 -1
  154. package/lib/utils/theme.d.ts +1 -1
  155. package/lib/utils/theme.js +1 -1
  156. package/package.json +7 -4
  157. package/lib/components/NewComponents/Notification/Notification.d.ts +0 -4
  158. package/lib/components/NewComponents/Notification/Notification.js +0 -60
  159. package/lib/components/NewComponents/Notification/NotificationContainer.d.ts +0 -8
  160. package/lib/components/NewComponents/Notification/NotificationContainer.js +0 -33
  161. package/lib/components/NewComponents/Notification/index.d.ts +0 -2
  162. package/lib/components/NewComponents/Notification/index.js +0 -2
  163. package/lib/components/NewComponents/Notification/styles.d.ts +0 -21
  164. package/lib/components/NewComponents/Notification/styles.js +0 -180
  165. package/lib/components/NewComponents/Notification/types.d.ts +0 -18
  166. package/lib/components/NewComponents/Notification/types.js +0 -1
  167. package/lib/components/base/TMContextMenu.d.ts +0 -25
  168. package/lib/components/base/TMContextMenu.js +0 -109
  169. package/lib/components/base/TMContextMenuOLD.d.ts +0 -26
  170. package/lib/components/base/TMContextMenuOLD.js +0 -56
  171. package/lib/components/base/TMFloatingToolbar.d.ts +0 -9
  172. package/lib/components/base/TMFloatingToolbar.js +0 -101
  173. package/lib/components/features/assistant/ToppyDraggableHelpCenter.d.ts +0 -30
  174. package/lib/components/features/assistant/ToppyDraggableHelpCenter.js +0 -482
  175. package/lib/components/features/assistant/ToppySpeechBubble.d.ts +0 -9
  176. package/lib/components/features/assistant/ToppySpeechBubble.js +0 -117
  177. package/lib/components/features/search/TMSearchResultCheckoutInfoForm.d.ts +0 -8
Binary file
@@ -0,0 +1,87 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <svg id="Livello_2" data-name="Livello 2" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1920 505.45">
3
+ <defs>
4
+ <style>
5
+ .cls-1 {
6
+ fill: url(#Sfumatura_senza_nome_6);
7
+ }
8
+
9
+ .cls-1, .cls-2, .cls-3, .cls-4 {
10
+ opacity: .5;
11
+ }
12
+
13
+ .cls-5 {
14
+ clip-path: url(#clippath-2);
15
+ }
16
+
17
+ .cls-5, .cls-6, .cls-7, .cls-8, .cls-9 {
18
+ fill: none;
19
+ }
20
+
21
+ .cls-2 {
22
+ fill: url(#Sfumatura_senza_nome_2);
23
+ }
24
+
25
+ .cls-6 {
26
+ clip-path: url(#clippath-3);
27
+ }
28
+
29
+ .cls-8 {
30
+ clip-path: url(#clippath);
31
+ }
32
+
33
+ .cls-3 {
34
+ fill: url(#Sfumatura_senza_nome_3-2);
35
+ }
36
+
37
+ .cls-9 {
38
+ clip-path: url(#clippath-1);
39
+ }
40
+
41
+ .cls-4 {
42
+ fill: url(#Sfumatura_senza_nome_3);
43
+ }
44
+ </style>
45
+ <clipPath id="clippath">
46
+ <rect class="cls-7" x="1237.53" y="0" width="682.47" height="326.62"/>
47
+ </clipPath>
48
+ <radialGradient id="Sfumatura_senza_nome_3" data-name="Sfumatura senza nome 3" cx="1285.93" cy="3418.23" fx="1285.93" fy="3418.23" r="50" gradientTransform="translate(-6983.46 -9577.73) scale(6.82 2.8)" gradientUnits="userSpaceOnUse">
49
+ <stop offset="0" stop-color="#2459a4"/>
50
+ <stop offset="1" stop-color="#fff" stop-opacity="0"/>
51
+ </radialGradient>
52
+ <clipPath id="clippath-1">
53
+ <rect class="cls-7" x="0" y="0" width="682.47" height="326.62" transform="translate(682.47 326.62) rotate(-180)"/>
54
+ </clipPath>
55
+ <radialGradient id="Sfumatura_senza_nome_6" data-name="Sfumatura senza nome 6" cx="1673.12" cy="3418.23" fx="1673.12" fy="3418.23" r="50" gradientTransform="translate(11545.93 -9577.73) rotate(-180) scale(6.82 -2.8)" gradientUnits="userSpaceOnUse">
56
+ <stop offset="0" stop-color="#2459a4"/>
57
+ <stop offset="1" stop-color="#fff" stop-opacity="0"/>
58
+ </radialGradient>
59
+ <clipPath id="clippath-2">
60
+ <rect class="cls-7" x="0" y="0" width="1055.7" height="297.5"/>
61
+ </clipPath>
62
+ <radialGradient id="Sfumatura_senza_nome_3-2" data-name="Sfumatura senza nome 3" cx="955.6" cy="3436.6" fx="955.6" fy="3436.6" r="50" gradientTransform="translate(-9560.44 -8770.57) scale(10.56 2.55)" xlink:href="#Sfumatura_senza_nome_3"/>
63
+ <clipPath id="clippath-3">
64
+ <rect class="cls-7" x="297.09" y="0" width="1055.7" height="505.45"/>
65
+ </clipPath>
66
+ <radialGradient id="Sfumatura_senza_nome_2" data-name="Sfumatura senza nome 2" cx="983.74" cy="3351.83" fx="983.74" fy="3351.83" r="50" gradientTransform="translate(-9560.44 -14533.91) scale(10.56 4.33)" gradientUnits="userSpaceOnUse">
67
+ <stop offset="0" stop-color="#2459a4"/>
68
+ <stop offset="1" stop-color="#fff"/>
69
+ </radialGradient>
70
+ </defs>
71
+ <g id="Livello_1-2" data-name="Livello 1">
72
+ <g>
73
+ <g class="cls-8">
74
+ <ellipse class="cls-4" cx="1792.58" cy="-7.96" rx="341.23" ry="139.98"/>
75
+ </g>
76
+ <g class="cls-9">
77
+ <ellipse class="cls-1" cx="127.42" cy="-7.96" rx="341.23" ry="139.98"/>
78
+ </g>
79
+ <g class="cls-5">
80
+ <ellipse class="cls-3" cx="527.85" cy="-7.25" rx="527.85" ry="127.5"/>
81
+ </g>
82
+ <g class="cls-6">
83
+ <ellipse class="cls-2" cx="824.94" cy="-12.32" rx="527.85" ry="216.62"/>
84
+ </g>
85
+ </g>
86
+ </g>
87
+ </svg>
@@ -1,8 +1,10 @@
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
+ import { createPortal } from 'react-dom';
3
4
  import * as S from './styles';
4
- import { useIsMobile, useMenuPosition } from './hooks';
5
- const TMContextMenu = ({ items, trigger = 'right', children }) => {
5
+ import { useIsMobile, useMenuPosition, useIsIOS } from './hooks';
6
+ import { IconArrowLeft } from '../../../helper';
7
+ const TMContextMenu = ({ items, trigger = 'right', children, target, externalControl, keepOpenOnClick = false }) => {
6
8
  const [menuState, setMenuState] = useState({
7
9
  visible: false,
8
10
  position: { x: 0, y: 0 },
@@ -11,23 +13,213 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
11
13
  });
12
14
  const [hoveredSubmenus, setHoveredSubmenus] = useState([]);
13
15
  const isMobile = useIsMobile();
16
+ const isIOS = useIsIOS();
14
17
  const menuRef = useRef(null);
15
18
  const triggerRef = useRef(null);
16
19
  const submenuTimeoutRef = useRef(null);
17
- const { openLeft, openUp } = useMenuPosition(menuRef, menuState.position);
20
+ const longPressTimeoutRef = useRef(null);
21
+ const touchStartPos = useRef(null);
22
+ // Get current menu to pass items count to positioning hook
23
+ const currentMenu = menuState.submenuStack.at(-1) || items;
24
+ const { openLeft, openUp, isCalculated, needsScroll, maxHeight } = useMenuPosition(menuRef, menuState.position, currentMenu.length);
18
25
  const handleClose = () => {
19
- setMenuState(prev => ({
20
- ...prev,
21
- visible: false,
22
- submenuStack: [items],
23
- parentNames: [],
24
- }));
26
+ if (externalControl) {
27
+ externalControl.onClose();
28
+ }
29
+ else {
30
+ setMenuState(prev => ({
31
+ ...prev,
32
+ visible: false,
33
+ submenuStack: [items],
34
+ parentNames: [],
35
+ }));
36
+ }
25
37
  setHoveredSubmenus([]);
26
38
  };
39
+ // Sync with external control when provided
40
+ useEffect(() => {
41
+ if (externalControl) {
42
+ setMenuState(prev => ({
43
+ ...prev,
44
+ visible: externalControl.visible,
45
+ position: externalControl.position,
46
+ submenuStack: [items],
47
+ parentNames: [],
48
+ }));
49
+ }
50
+ }, [externalControl, items]);
51
+ // iOS long-press support: attach touch listeners to target elements
52
+ // On long-press, dispatch synthetic contextmenu event to trigger existing handlers
53
+ useEffect(() => {
54
+ if (!target || !isIOS)
55
+ return;
56
+ const elements = document.querySelectorAll(target);
57
+ if (elements.length === 0)
58
+ return;
59
+ const touchStateMap = new WeakMap();
60
+ const handleTouchStart = (e) => {
61
+ const touchEvent = e;
62
+ const element = e.currentTarget;
63
+ const touch = touchEvent.touches[0];
64
+ let state = touchStateMap.get(element);
65
+ if (!state) {
66
+ state = { timeout: null, startX: 0, startY: 0, longPressTriggered: false };
67
+ touchStateMap.set(element, state);
68
+ }
69
+ state.startX = touch.clientX;
70
+ state.startY = touch.clientY;
71
+ state.longPressTriggered = false;
72
+ if (state.timeout)
73
+ clearTimeout(state.timeout);
74
+ state.timeout = setTimeout(() => {
75
+ if (state)
76
+ state.longPressTriggered = true;
77
+ // Haptic feedback
78
+ if ('vibrate' in navigator)
79
+ navigator.vibrate(50);
80
+ const syntheticEvent = new MouseEvent('contextmenu', {
81
+ bubbles: true,
82
+ cancelable: true,
83
+ clientX: touch.clientX,
84
+ clientY: touch.clientY,
85
+ });
86
+ // Mark this as our synthetic event
87
+ syntheticEvent.isSynthetic = true;
88
+ element.dispatchEvent(syntheticEvent);
89
+ if (state)
90
+ state.timeout = null;
91
+ }, 500);
92
+ };
93
+ const handleTouchMove = (e) => {
94
+ const touchEvent = e;
95
+ const element = e.currentTarget;
96
+ const state = touchStateMap.get(element);
97
+ if (!state?.timeout)
98
+ return;
99
+ const touch = touchEvent.touches[0];
100
+ const dx = Math.abs(touch.clientX - state.startX);
101
+ const dy = Math.abs(touch.clientY - state.startY);
102
+ if (dx > 10 || dy > 10) {
103
+ clearTimeout(state.timeout);
104
+ state.timeout = null;
105
+ }
106
+ };
107
+ const handleTouchEnd = (e) => {
108
+ const element = e.currentTarget;
109
+ const state = touchStateMap.get(element);
110
+ if (state?.timeout) {
111
+ clearTimeout(state.timeout);
112
+ state.timeout = null;
113
+ }
114
+ };
115
+ // Prevent click event after long-press was triggered
116
+ const handleClick = (e) => {
117
+ const element = e.currentTarget;
118
+ const state = touchStateMap.get(element);
119
+ if (state?.longPressTriggered) {
120
+ e.preventDefault();
121
+ e.stopPropagation();
122
+ e.stopImmediatePropagation();
123
+ state.longPressTriggered = false;
124
+ }
125
+ };
126
+ // Prevent default iOS context menu (but allow our synthetic events)
127
+ const handleContextMenu = (e) => {
128
+ // Don't prevent our own synthetic contextmenu events
129
+ if (e.isSynthetic)
130
+ return;
131
+ e.preventDefault();
132
+ e.stopPropagation();
133
+ return false;
134
+ };
135
+ // Attach listeners to all matching elements
136
+ elements.forEach(element => {
137
+ const el = element;
138
+ // Prevent iOS native callout and text selection - must be set before touch starts
139
+ const style = el.style;
140
+ style.webkitTouchCallout = 'none';
141
+ style.webkitUserSelect = 'none';
142
+ el.addEventListener('touchstart', handleTouchStart, { passive: true });
143
+ el.addEventListener('touchmove', handleTouchMove, { passive: true });
144
+ el.addEventListener('touchend', handleTouchEnd);
145
+ el.addEventListener('touchcancel', handleTouchEnd);
146
+ el.addEventListener('contextmenu', handleContextMenu);
147
+ el.addEventListener('click', handleClick, { capture: true });
148
+ });
149
+ return () => {
150
+ elements.forEach(element => {
151
+ const el = element;
152
+ // Restore webkit properties
153
+ const style = el.style;
154
+ style.webkitTouchCallout = '';
155
+ style.webkitUserSelect = '';
156
+ el.removeEventListener('touchstart', handleTouchStart);
157
+ el.removeEventListener('touchmove', handleTouchMove);
158
+ el.removeEventListener('touchend', handleTouchEnd);
159
+ el.removeEventListener('touchcancel', handleTouchEnd);
160
+ el.removeEventListener('contextmenu', handleContextMenu);
161
+ el.removeEventListener('click', handleClick, { capture: true });
162
+ });
163
+ };
164
+ }, [target, isIOS]);
165
+ // Update items when they change while menu is visible (for keepOpenOnClick behavior)
166
+ useEffect(() => {
167
+ if (!keepOpenOnClick)
168
+ return;
169
+ if (!externalControl && menuState.visible) {
170
+ setMenuState(prev => ({
171
+ ...prev,
172
+ submenuStack: [items],
173
+ }));
174
+ // Update hoveredSubmenus with fresh items while keeping them open
175
+ setHoveredSubmenus(prev => {
176
+ if (prev.length === 0)
177
+ return prev;
178
+ // Rebuild hoveredSubmenus with updated items from the new items structure
179
+ return prev.map(submenu => {
180
+ // Find the matching submenu in the new items structure
181
+ const findSubmenuInItems = (searchItems) => {
182
+ for (const item of searchItems) {
183
+ if (item.submenu && item.submenu.length > 0) {
184
+ // Check if this submenu matches (compare first item name as identifier)
185
+ if (submenu.items.length > 0 && item.submenu.length > 0 &&
186
+ item.submenu[0].name === submenu.items[0].name) {
187
+ return item.submenu;
188
+ }
189
+ // Recursively search in nested submenus
190
+ const found = findSubmenuInItems(item.submenu);
191
+ if (found)
192
+ return found;
193
+ }
194
+ }
195
+ return null;
196
+ };
197
+ const updatedItems = findSubmenuInItems(items);
198
+ return {
199
+ ...submenu,
200
+ items: updatedItems || submenu.items, // Use updated items if found, otherwise keep old
201
+ };
202
+ });
203
+ });
204
+ }
205
+ }, [items, menuState.visible, externalControl, keepOpenOnClick]);
206
+ // Track when the menu was opened to prevent immediate close on iOS
207
+ const menuOpenedAtRef = useRef(0);
208
+ useEffect(() => {
209
+ if (menuState.visible) {
210
+ menuOpenedAtRef.current = Date.now();
211
+ }
212
+ }, [menuState.visible]);
27
213
  useEffect(() => {
28
214
  if (!menuState.visible)
29
215
  return;
30
216
  const handleClickOutside = (event) => {
217
+ // On iOS, prevent closing immediately after opening (within 500ms for iOS, 300ms for others)
218
+ // This handles the case where touchend from long-press triggers touchstart listener
219
+ const delay = isIOS ? 500 : 300;
220
+ if (Date.now() - menuOpenedAtRef.current < delay) {
221
+ return;
222
+ }
31
223
  const target = event.target;
32
224
  // Check if click is inside main menu
33
225
  if (menuRef.current?.contains(target)) {
@@ -45,20 +237,26 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
45
237
  };
46
238
  document.addEventListener('mousedown', handleClickOutside);
47
239
  document.addEventListener('touchstart', handleClickOutside);
240
+ document.addEventListener('contextmenu', handleClickOutside); // Close on right-click outside
48
241
  return () => {
49
242
  document.removeEventListener('mousedown', handleClickOutside);
50
243
  document.removeEventListener('touchstart', handleClickOutside);
244
+ document.removeEventListener('contextmenu', handleClickOutside);
51
245
  };
52
246
  }, [menuState.visible]);
53
247
  const handleContextMenu = (e) => {
54
248
  if (trigger === 'right') {
55
249
  e.preventDefault();
56
- setMenuState({
57
- visible: true,
58
- position: { x: e.clientX, y: e.clientY },
59
- submenuStack: [items],
60
- parentNames: [],
61
- });
250
+ e.stopPropagation(); // Prevent event from bubbling to close other menus prematurely
251
+ // Small delay to ensure other menus receive the contextmenu event and close first
252
+ setTimeout(() => {
253
+ setMenuState({
254
+ visible: true,
255
+ position: { x: e.clientX, y: e.clientY },
256
+ submenuStack: [items],
257
+ parentNames: [],
258
+ });
259
+ }, 0);
62
260
  }
63
261
  };
64
262
  const handleClick = (e) => {
@@ -72,27 +270,73 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
72
270
  });
73
271
  }
74
272
  };
273
+ // iOS-specific touch handlers for long press
274
+ const handleTouchStart = (e) => {
275
+ if (!isIOS || trigger !== 'right')
276
+ return;
277
+ const touch = e.touches[0];
278
+ touchStartPos.current = { x: touch.clientX, y: touch.clientY };
279
+ if (longPressTimeoutRef.current) {
280
+ clearTimeout(longPressTimeoutRef.current);
281
+ }
282
+ longPressTimeoutRef.current = setTimeout(() => {
283
+ if (touchStartPos.current) {
284
+ if ('vibrate' in navigator) {
285
+ navigator.vibrate(50);
286
+ }
287
+ setMenuState({
288
+ visible: true,
289
+ position: { x: touchStartPos.current.x, y: touchStartPos.current.y },
290
+ submenuStack: [items],
291
+ parentNames: [],
292
+ });
293
+ }
294
+ }, 500);
295
+ };
296
+ const handleTouchMove = (e) => {
297
+ if (!isIOS || trigger !== 'right' || !touchStartPos.current)
298
+ return;
299
+ const touch = e.touches[0];
300
+ const moveThreshold = 10; // pixels
301
+ // If finger moved too much, cancel long press
302
+ const deltaX = Math.abs(touch.clientX - touchStartPos.current.x);
303
+ const deltaY = Math.abs(touch.clientY - touchStartPos.current.y);
304
+ if (deltaX > moveThreshold || deltaY > moveThreshold) {
305
+ if (longPressTimeoutRef.current) {
306
+ clearTimeout(longPressTimeoutRef.current);
307
+ longPressTimeoutRef.current = null;
308
+ }
309
+ touchStartPos.current = null;
310
+ }
311
+ };
312
+ const handleTouchEnd = () => {
313
+ if (!isIOS || trigger !== 'right')
314
+ return;
315
+ if (longPressTimeoutRef.current) {
316
+ clearTimeout(longPressTimeoutRef.current);
317
+ longPressTimeoutRef.current = null;
318
+ }
319
+ touchStartPos.current = null;
320
+ };
75
321
  const handleItemClick = (item) => {
76
322
  if (item.disabled)
77
323
  return;
78
- // Always execute onClick if present
79
324
  if (item.onClick) {
80
325
  item.onClick();
81
326
  }
82
327
  if (item.submenu && item.submenu.length > 0) {
83
328
  if (isMobile) {
84
- // Mobile: Push submenu to stack
85
329
  setMenuState(prev => ({
86
330
  ...prev,
87
331
  submenuStack: [...prev.submenuStack, item.submenu],
88
332
  parentNames: [...prev.parentNames, item.name],
89
333
  }));
90
334
  }
91
- // Desktop: Submenus are handled by hover, don't close menu
92
335
  }
93
336
  else {
94
- // No submenu: close menu after executing action
95
- handleClose();
337
+ if (!keepOpenOnClick) {
338
+ handleClose();
339
+ }
96
340
  }
97
341
  };
98
342
  const handleBack = () => {
@@ -103,20 +347,24 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
103
347
  }));
104
348
  };
105
349
  const handleMouseEnter = (item, event, depth = 0) => {
106
- if (isMobile || !item.submenu || item.submenu.length === 0)
350
+ if (isMobile || !item.submenu || item.submenu.length === 0 || item.disabled)
107
351
  return;
108
352
  if (submenuTimeoutRef.current) {
109
353
  clearTimeout(submenuTimeoutRef.current);
110
354
  submenuTimeoutRef.current = null;
111
355
  }
112
356
  const rect = event.currentTarget.getBoundingClientRect();
113
- // Calculate if submenu should open upward based on available space
114
- // Estimate submenu height: ~35px per item (accounting for smaller padding) + container padding
357
+ // Calculate submenu height dynamically
115
358
  const estimatedSubmenuHeight = (item.submenu.length * 35) + 8;
116
359
  const spaceBelow = window.innerHeight - rect.top;
117
360
  const spaceAbove = rect.bottom;
118
- // Open upward only if there's not enough space below AND there's more space above
361
+ const padding = 8;
362
+ // Determine if submenu should open upward
119
363
  const shouldOpenUp = spaceBelow < estimatedSubmenuHeight && spaceAbove > spaceBelow;
364
+ // Calculate if submenu needs scroll and max-height
365
+ const availableSpace = shouldOpenUp ? spaceAbove : spaceBelow;
366
+ const needsScroll = estimatedSubmenuHeight > availableSpace - padding;
367
+ const maxHeight = needsScroll ? availableSpace - padding : undefined;
120
368
  // Remove all submenus at this depth and deeper
121
369
  setHoveredSubmenus(prev => {
122
370
  const filtered = prev.filter(sub => sub.depth < depth);
@@ -129,6 +377,8 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
129
377
  parentRect: rect,
130
378
  depth: depth,
131
379
  openUp: shouldOpenUp,
380
+ needsScroll,
381
+ maxHeight,
132
382
  }
133
383
  ];
134
384
  });
@@ -155,6 +405,9 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
155
405
  if (submenuTimeoutRef.current) {
156
406
  clearTimeout(submenuTimeoutRef.current);
157
407
  }
408
+ if (longPressTimeoutRef.current) {
409
+ clearTimeout(longPressTimeoutRef.current);
410
+ }
158
411
  };
159
412
  }, []);
160
413
  const renderMenuItems = (menuItems, depth = 0) => {
@@ -172,16 +425,20 @@ const TMContextMenu = ({ items, trigger = 'right', children }) => {
172
425
  e.stopPropagation();
173
426
  // if (item.disabled) return;
174
427
  item.onRightIconClick?.();
428
+ handleClose();
175
429
  };
176
- return (_jsxs(S.MenuItem, { "$disabled": item.disabled, "$hasSubmenu": !!item.submenu && item.submenu.length > 0, "$beginGroup": item.beginGroup, onMouseDown: handleClick, onMouseEnter: (e) => !isMobile && handleMouseEnter(item, e, depth + 1), onMouseLeave: () => !isMobile && handleMouseLeave(depth + 1), children: [_jsxs(S.MenuItemContent, { children: [item.icon && _jsx(S.IconWrapper, { children: item.icon }), _jsx(S.MenuItemName, { children: item.name })] }), item.rightIcon && item.onRightIconClick && (_jsx(S.RightIconButton, { onClick: handleRightIconClick, onMouseDown: (e) => e.stopPropagation(), "aria-label": `Action for ${item.name}`, children: item.rightIcon })), item.submenu && item.submenu.length > 0 && (_jsx(S.SubmenuIndicator, { "$isMobile": isMobile, children: isMobile ? '›' : '▸' }))] }, itemKey));
430
+ return (_jsxs(S.MenuItem, { "$disabled": item.disabled, "$hasSubmenu": !!item.submenu && item.submenu.length > 0, "$beginGroup": item.beginGroup, "data-disabled": item.disabled ? "true" : undefined, onMouseDown: handleClick, onMouseEnter: (e) => !isMobile && handleMouseEnter(item, e, depth + 1), onMouseLeave: () => !isMobile && handleMouseLeave(depth + 1), title: item.tooltip, children: [_jsxs(S.MenuItemContent, { children: [item.icon && _jsx(S.IconWrapper, { children: item.icon }), _jsx(S.MenuItemName, { children: item.name })] }), item.rightIcon && item.onRightIconClick && (_jsx(S.RightIconButton, { onClick: handleRightIconClick, onMouseDown: (e) => e.stopPropagation(), "aria-label": `Action for ${item.name}`, children: item.rightIcon })), item.submenu && item.submenu.length > 0 && (_jsx(S.SubmenuIndicator, { "$isMobile": isMobile, children: isMobile ? '›' : '▸' }))] }, itemKey));
177
431
  });
178
432
  };
179
- const currentMenu = menuState.submenuStack.at(-1) || items;
180
433
  const currentParentName = menuState.parentNames.at(-1) || '';
181
- return (_jsxs(_Fragment, { children: [_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onKeyDown: (e) => {
434
+ return (_jsxs(_Fragment, { children: [!externalControl && children && (_jsx("div", { ref: triggerRef, onContextMenu: handleContextMenu, onClick: handleClick, onTouchStart: handleTouchStart, onTouchMove: handleTouchMove, onTouchEnd: handleTouchEnd, onTouchCancel: handleTouchEnd, onKeyDown: (e) => {
182
435
  if (e.key === 'Enter' || e.key === ' ') {
183
436
  handleClick(e);
184
437
  }
185
- }, role: "button", tabIndex: 0, style: { display: 'inline-block' }, children: children }), menuState.visible && (_jsxs(_Fragment, { children: [_jsx(S.Overlay, { onClick: handleClose }), _jsxs(S.MenuContainer, { ref: menuRef, "$x": menuState.position.x, "$y": menuState.position.y, "$openLeft": openLeft, "$openUp": openUp, children: [isMobile && menuState.parentNames.length > 0 && (_jsxs(S.MobileMenuHeader, { children: [_jsx(S.BackButton, { onClick: handleBack, "aria-label": "Go back", children: "\u2190" }), _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}`)))] }))] }));
438
+ }, role: "button", tabIndex: 0, style: {
439
+ // display: 'inline-block',
440
+ WebkitTouchCallout: isIOS ? 'none' : undefined,
441
+ WebkitUserSelect: isIOS ? 'none' : undefined,
442
+ }, children: children })), menuState.visible && createPortal(_jsxs(_Fragment, { children: [_jsxs(S.MenuContainer, { ref: menuRef, "$x": menuState.position.x, "$y": menuState.position.y, "$openLeft": openLeft, "$openUp": openUp, "$isPositioned": isCalculated, "$externalControl": !!externalControl, "$needsScroll": needsScroll, "$maxHeight": maxHeight, 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}`)))] }), document.body)] }));
186
443
  };
187
444
  export default TMContextMenu;
@@ -1,11 +1,18 @@
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 {
4
5
  x: number;
5
6
  y: number;
6
7
  }
7
- export declare const useMenuPosition: (menuRef: React.RefObject<HTMLDivElement | null>, position: Position) => {
8
+ export declare const useMenuPosition: (menuRef: React.RefObject<HTMLDivElement | null>, position: Position, itemsCount?: number) => {
9
+ isCalculated: boolean;
8
10
  openLeft: boolean;
9
11
  openUp: boolean;
12
+ needsScroll: boolean;
13
+ maxHeight: number | undefined;
10
14
  };
15
+ export declare const getContextMenuTarget: <T extends {
16
+ id: string;
17
+ }>(event: React.MouseEvent | undefined, focusedItem: T | undefined, selectedItem: T | undefined, dataSource: T[], idPrefix: string, isMobile: boolean) => T | undefined;
11
18
  export {};
@@ -1,4 +1,13 @@
1
- import { useState, useEffect, useRef } from 'react';
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(() => {
@@ -29,20 +38,83 @@ export const useClickOutside = (callback) => {
29
38
  }, [callback]);
30
39
  return ref;
31
40
  };
32
- export const useMenuPosition = (menuRef, position) => {
33
- const [adjustedPosition, setAdjustedPosition] = useState({ openLeft: false, openUp: false });
34
- useEffect(() => {
35
- if (!menuRef.current)
41
+ export const useMenuPosition = (menuRef, position, itemsCount) => {
42
+ const [adjustedPosition, setAdjustedPosition] = useState({ openLeft: false, openUp: false, needsScroll: false, maxHeight: undefined });
43
+ const [isCalculated, setIsCalculated] = useState(false);
44
+ useLayoutEffect(() => {
45
+ if (!menuRef.current) {
46
+ setIsCalculated(false);
36
47
  return;
48
+ }
37
49
  const menuRect = menuRef.current.getBoundingClientRect();
38
50
  const viewportWidth = window.innerWidth;
39
51
  const viewportHeight = window.innerHeight;
52
+ const isMobile = viewportWidth <= 768;
40
53
  const spaceRight = viewportWidth - position.x;
41
54
  const spaceBottom = viewportHeight - position.y;
55
+ const spaceAbove = position.y;
56
+ const padding = 8; // Minimal padding from viewport edges - be more aggressive about using available space
57
+ // Use scrollHeight to get natural content height, not constrained height
58
+ const menuHeight = menuRef.current.scrollHeight;
59
+ // Mobile: Always calculate max-height based on position to prevent overflow
60
+ if (isMobile) {
61
+ const maxHeightFromBottom = spaceBottom - padding;
62
+ const maxHeightFromTop = spaceAbove - padding;
63
+ const mobileMaxHeight = Math.max(maxHeightFromBottom, maxHeightFromTop);
64
+ setAdjustedPosition({
65
+ openLeft: spaceRight < menuRect.width + 20,
66
+ openUp: maxHeightFromTop > maxHeightFromBottom && menuHeight > maxHeightFromBottom,
67
+ needsScroll: menuHeight > mobileMaxHeight,
68
+ maxHeight: mobileMaxHeight,
69
+ });
70
+ setIsCalculated(true);
71
+ return;
72
+ }
73
+ // Desktop: Check if menu is too tall to fit in either direction
74
+ const fitsBelow = menuHeight + padding <= spaceBottom;
75
+ const fitsAbove = menuHeight + padding <= spaceAbove;
76
+ const needsScroll = !fitsBelow && !fitsAbove;
77
+ // Calculate max height when scrolling is needed
78
+ let maxHeight = undefined;
79
+ let shouldOpenUp = false;
80
+ if (needsScroll) {
81
+ // When scrolling is needed, open in the direction with MORE space
82
+ const availableSpace = Math.max(spaceBottom, spaceAbove);
83
+ maxHeight = availableSpace - padding;
84
+ shouldOpenUp = spaceAbove > spaceBottom; // Open upward if more space above
85
+ }
86
+ else {
87
+ // Normal behavior: open up only if it fits above but not below
88
+ shouldOpenUp = !fitsBelow && fitsAbove;
89
+ }
42
90
  setAdjustedPosition({
43
91
  openLeft: spaceRight < menuRect.width + 20,
44
- openUp: spaceBottom < menuRect.height + 20,
92
+ openUp: shouldOpenUp,
93
+ needsScroll,
94
+ maxHeight,
45
95
  });
46
- }, [position, menuRef]);
47
- return adjustedPosition;
96
+ setIsCalculated(true);
97
+ }, [position, menuRef, itemsCount]); // Added itemsCount to recalculate when menu content changes
98
+ return { ...adjustedPosition, isCalculated };
99
+ };
100
+ export const getContextMenuTarget = (event, focusedItem, selectedItem, dataSource, idPrefix, isMobile) => {
101
+ if (!event)
102
+ return undefined;
103
+ let targetItem = focusedItem ?? selectedItem;
104
+ if (!focusedItem && isMobile) {
105
+ // Find the actual item that was long-pressed by traversing up from the event target
106
+ let element = event.target;
107
+ while (element && element !== event.currentTarget) {
108
+ if (element.id && element.id.startsWith(`${idPrefix}-`)) {
109
+ const itemId = element.id.replace(`${idPrefix}-`, '');
110
+ const foundItem = dataSource.find(item => item.id === itemId);
111
+ if (foundItem) {
112
+ targetItem = foundItem;
113
+ break;
114
+ }
115
+ }
116
+ element = element.parentElement;
117
+ }
118
+ }
119
+ return targetItem;
48
120
  };
@@ -1,2 +1,5 @@
1
1
  export { default as ContextMenu } from './TMContextMenu';
2
2
  export type { TMContextMenuItemProps, TMContextMenuProps } from './types';
3
+ export { useLongPress, triggerContextMenuEvent } from './useLongPress';
4
+ export type { UseLongPressOptions } from './useLongPress';
5
+ export { useIsIOS } from './hooks';
@@ -1 +1,3 @@
1
1
  export { default as ContextMenu } from './TMContextMenu';
2
+ export { useLongPress, triggerContextMenuEvent } from './useLongPress';
3
+ export { useIsIOS } from './hooks';