@ticatec/uniface-element 5.0.0 → 5.0.2

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 (282) hide show
  1. package/README.md +29 -54
  2. package/README_CN.md +37 -57
  3. package/dist/app_layout/ClassicLayout.svelte +8 -40
  4. package/dist/app_layout/ColumnsLayout.svelte +3 -3
  5. package/dist/app_layout/HeaderLayout.svelte +4 -20
  6. package/dist/app_layout/RowsLayout.svelte +3 -3
  7. package/dist/app_layout/SidebarLayout.svelte +4 -20
  8. package/dist/base-calendar/CalendarBase.svelte +29 -6
  9. package/dist/base-calendar/CalendarBase.svelte.d.ts +4 -0
  10. package/dist/base-calendar/MonthPickupPanel.svelte +19 -6
  11. package/dist/base-calendar/MonthPickupPanel.svelte.d.ts +1 -1
  12. package/dist/base-calendar/MonthlyCalendar.svelte +18 -2
  13. package/dist/base-calendar/YearPickupPanel.svelte +24 -6
  14. package/dist/base-calendar/YearPickupPanel.svelte.d.ts +1 -1
  15. package/dist/composite/pagination_panel/PaginationPanel.svelte +2 -0
  16. package/dist/composite/transfer/Transfer.svelte +12 -6
  17. package/dist/data_display/accordion/Accordion.svelte +1 -4
  18. package/dist/data_display/card/CommonCardActionBar.svelte +31 -37
  19. package/dist/data_display/concise_data_table/Column.d.ts +6 -0
  20. package/dist/data_display/concise_data_table/ConciseListTable.svelte +4 -3
  21. package/dist/data_display/concise_data_table/DataCell.svelte +3 -3
  22. package/dist/data_display/list_box/ListBox.svelte +96 -23
  23. package/dist/data_display/list_box/ListBox.svelte.d.ts +4 -0
  24. package/dist/data_display/tree_view/TreeNodeView.svelte +25 -4
  25. package/dist/data_table/DataTable.svelte +5 -5
  26. package/dist/data_table/DataTable.svelte.d.ts +36 -16
  27. package/dist/data_table/lib/ActionsColumn.d.ts +2 -2
  28. package/dist/data_table/lib/DataColumn.d.ts +7 -1
  29. package/dist/data_table/lib/RowAction.d.ts +5 -5
  30. package/dist/data_table/lib/clickOutSide.js +6 -5
  31. package/dist/data_table/parts/FixedHeaderPanel.svelte +18 -3
  32. package/dist/data_table/parts/FixedRow.svelte +10 -1
  33. package/dist/data_table/parts/HrefCell.svelte +1 -1
  34. package/dist/data_table/parts/TableHeaderPanel.svelte +18 -2
  35. package/dist/data_table/parts/TextCell.svelte +2 -2
  36. package/dist/form/attachment_files/AttachmentEditor.svelte +20 -15
  37. package/dist/form/attachment_files/FileRender.svelte +1 -1
  38. package/dist/form/attachment_files/FileUploadBar.svelte +5 -2
  39. package/dist/form/attachment_files/FileUploadPanel.svelte +11 -73
  40. package/dist/form/cascade_options_select/CascadeOptionSelect.svelte +10 -2
  41. package/dist/form/cascade_options_select/OptionsBox.svelte +48 -2
  42. package/dist/form/checkbox/CheckBox.svelte +2 -7
  43. package/dist/form/color_picker/ColorPicker.svelte +150 -35
  44. package/dist/form/color_picker/ColorPicker.svelte.d.ts +7 -25
  45. package/dist/form/color_picker/ColorPickerPanel.svelte +23 -20
  46. package/dist/form/color_picker/ColorPickerPanel.svelte.d.ts +5 -18
  47. package/dist/form/color_picker/README.md +14 -7
  48. package/dist/form/color_picker/README_CN.md +14 -7
  49. package/dist/form/common_editor/CommonEditor.svelte +18 -4
  50. package/dist/form/common_editor/CommonPicker.svelte +14 -10
  51. package/dist/form/common_editor/CommonPopupEditor.svelte +8 -1
  52. package/dist/form/common_range_editor/CommonRangeEditor.svelte +11 -2
  53. package/dist/form/date_picker/DatePicker.svelte +1 -1
  54. package/dist/form/date_picker/DateTimePicker.svelte +1 -1
  55. package/dist/form/date_picker/ScrollBar.svelte +20 -12
  56. package/dist/form/date_picker/ScrollBar.svelte.d.ts +4 -18
  57. package/dist/form/date_range/DateRangeEditor.svelte +11 -3
  58. package/dist/form/group_box/group-check-box/GroupCheckBox.svelte +6 -1
  59. package/dist/form/group_box/group-radio-box/GroupRadioBox.svelte +6 -1
  60. package/dist/form/image-files/ImageFilesField.svelte +51 -7
  61. package/dist/form/image-files/ImagePreview.svelte +23 -9
  62. package/dist/form/image-files/ImageRender.svelte +69 -11
  63. package/dist/form/input_options_select/InputOptionsSelect.svelte +10 -2
  64. package/dist/form/lookup_editor/LookupEditor.svelte +9 -2
  65. package/dist/form/memo_editor/MemoEditor.svelte +2 -2
  66. package/dist/form/number_editor/NumberEditor.svelte +2 -2
  67. package/dist/form/options_multi_select/OptionsMultiSelect.svelte +20 -12
  68. package/dist/form/options_select/OptionsSelect.svelte +15 -6
  69. package/dist/form/options_select/OptionsSelect.svelte.d.ts +39 -17
  70. package/dist/form/prompts_text_editor/PromptsTextEditor.svelte +10 -2
  71. package/dist/form/radio_button/RadioButton.svelte +2 -8
  72. package/dist/form/switch/Switch.svelte +4 -4
  73. package/dist/form/text_editor/TextEditor.svelte +2 -2
  74. package/dist/form/unit-number-editor/UnitNumberEditor.svelte +10 -2
  75. package/dist/general/button/Button.svelte +5 -0
  76. package/dist/general/button/Button.svelte.d.ts +1 -0
  77. package/dist/general/button/IconButton.svelte +3 -1
  78. package/dist/general/button/IconButton.svelte.d.ts +1 -0
  79. package/dist/general/button/TextButton.svelte +3 -0
  80. package/dist/general/button/TextButton.svelte.d.ts +1 -0
  81. package/dist/general/icon/Icon.svelte +18 -1
  82. package/dist/general/split/Split.svelte +24 -4
  83. package/dist/general/tag/Tag.svelte +9 -1
  84. package/dist/navigation/nav_menu/MenuItem.d.ts +4 -2
  85. package/dist/navigation/nav_menu/NavigatorMenu.svelte +157 -34
  86. package/dist/navigation/nav_menu/NavigatorMenu.svelte.d.ts +2 -3
  87. package/dist/navigation/nav_menu/NavigatorMenuItem.svelte +25 -13
  88. package/dist/navigation/nav_menu/NavigatorMenuItem.svelte.d.ts +3 -3
  89. package/dist/navigation/navigator/Navigator.svelte +17 -17
  90. package/dist/navigation/page-stack/PageStack.svelte +160 -0
  91. package/dist/navigation/page-stack/PageStack.svelte.d.ts +24 -0
  92. package/dist/navigation/page-stack/PageStackContext.d.ts +25 -0
  93. package/dist/navigation/page-stack/PageStackContext.js +12 -0
  94. package/dist/navigation/page-stack/PageStackFrame.svelte +24 -0
  95. package/dist/navigation/page-stack/PageStackFrame.svelte.d.ts +11 -0
  96. package/dist/navigation/page-stack/README.md +154 -0
  97. package/dist/navigation/page-stack/README_CN.md +154 -0
  98. package/dist/navigation/page-stack/index.d.ts +6 -0
  99. package/dist/navigation/page-stack/index.js +4 -0
  100. package/dist/navigation/pagination/Pagination.svelte +114 -14
  101. package/dist/navigation/progress_step_bar/ProgressStepBlock.svelte +1 -5
  102. package/dist/navigation/tabs/Tabs.svelte +42 -10
  103. package/dist/navigation/tabs/Tabs.svelte.d.ts +2 -2
  104. package/dist/overlay/common/FloatingPanel.svelte +3 -17
  105. package/dist/overlay/common/FullscreenOverlay.svelte +27 -3
  106. package/dist/overlay/common/FullscreenOverlay.svelte.d.ts +6 -27
  107. package/dist/overlay/common/ModalPopover.svelte +18 -2
  108. package/dist/overlay/common/Popover.svelte +61 -57
  109. package/dist/overlay/common/Popover.svelte.d.ts +1 -5
  110. package/dist/overlay/common/escapeClose.d.ts +28 -0
  111. package/dist/overlay/common/escapeClose.js +47 -0
  112. package/dist/overlay/common/focusTrap.d.ts +8 -0
  113. package/dist/overlay/common/focusTrap.js +83 -0
  114. package/dist/overlay/common/mountSingleton.d.ts +27 -0
  115. package/dist/overlay/common/mountSingleton.js +45 -0
  116. package/dist/overlay/common/outsideClick.d.ts +28 -0
  117. package/dist/overlay/common/outsideClick.js +20 -0
  118. package/dist/overlay/common/scrollLock.d.ts +8 -0
  119. package/dist/overlay/common/scrollLock.js +49 -0
  120. package/dist/overlay/common/uniface-utils.js +0 -1
  121. package/dist/overlay/context-menu/ContextMenu.svelte.js +2 -15
  122. package/dist/overlay/context-menu/ContextMenuPanel.svelte +27 -3
  123. package/dist/overlay/dialog/Dialog.svelte +48 -32
  124. package/dist/overlay/dialog/DialogManager.svelte.js +7 -9
  125. package/dist/overlay/dialog/DialogWrapper.svelte +5 -2
  126. package/dist/overlay/drawer/Drawer.svelte +30 -3
  127. package/dist/overlay/indicator/Indicator.svelte.js +2 -15
  128. package/dist/overlay/indicator/IndicatorPanel.svelte +4 -1
  129. package/dist/overlay/message-box/MessageBox.svelte.js +2 -15
  130. package/dist/overlay/message-box/MessageBoxPanel.svelte +19 -3
  131. package/dist/overlay/toast/Toast.svelte.js +2 -15
  132. package/dist/overlay/toast/ToastPanel.svelte +1 -1
  133. package/dist/overlay/tooltip/Tooltip.svelte +1 -1
  134. package/dist/ticatec-uniface-web.css +128 -13
  135. package/dist/types.d.ts +4 -3
  136. package/dist/utils/prefixFilter.d.ts +3 -3
  137. package/dist/utils/prefixFilter.js +3 -5
  138. package/docs/components/README.md +133 -0
  139. package/docs/components/accordion/README.md +60 -0
  140. package/docs/components/accordion/example.svelte +7 -0
  141. package/docs/components/action-bar/README.md +114 -0
  142. package/docs/components/action-bar/example.svelte +7 -0
  143. package/docs/components/app-top-bar/README.md +60 -0
  144. package/docs/components/app-top-bar/example.svelte +9 -0
  145. package/docs/components/attachment-files/README.md +332 -0
  146. package/docs/components/attachment-files/example.svelte +16 -0
  147. package/docs/components/box/README.md +63 -0
  148. package/docs/components/box/example.svelte +10 -0
  149. package/docs/components/breadcrumbs/README.md +86 -0
  150. package/docs/components/breadcrumbs/example.svelte +11 -0
  151. package/docs/components/button/README.md +103 -0
  152. package/docs/components/button/example.svelte +10 -0
  153. package/docs/components/card/README.md +113 -0
  154. package/docs/components/card/example.svelte +10 -0
  155. package/docs/components/cascade-options-select/README.md +162 -0
  156. package/docs/components/cascade-options-select/example.svelte +18 -0
  157. package/docs/components/checkbox/README.md +103 -0
  158. package/docs/components/checkbox/example.svelte +15 -0
  159. package/docs/components/classic-layout/README.md +76 -0
  160. package/docs/components/classic-layout/example.svelte +16 -0
  161. package/docs/components/color-picker/README.md +132 -0
  162. package/docs/components/color-picker/example.svelte +16 -0
  163. package/docs/components/columns-layout/README.md +70 -0
  164. package/docs/components/columns-layout/example.svelte +12 -0
  165. package/docs/components/common-dialog/README.md +109 -0
  166. package/docs/components/common-dialog/example.svelte +12 -0
  167. package/docs/components/concise-data-table/README.md +307 -0
  168. package/docs/components/concise-data-table/example.svelte +16 -0
  169. package/docs/components/context-menu/README.md +99 -0
  170. package/docs/components/context-menu/example.svelte +10 -0
  171. package/docs/components/criteria-field/README.md +69 -0
  172. package/docs/components/criteria-field/example.svelte +9 -0
  173. package/docs/components/data-table/README.md +365 -0
  174. package/docs/components/data-table/example.svelte +23 -0
  175. package/docs/components/date-picker/README.md +136 -0
  176. package/docs/components/date-picker/example.svelte +16 -0
  177. package/docs/components/date-range/README.md +122 -0
  178. package/docs/components/date-range/example.svelte +15 -0
  179. package/docs/components/date-time-picker/README.md +142 -0
  180. package/docs/components/date-time-picker/example.svelte +16 -0
  181. package/docs/components/dialog/README.md +174 -0
  182. package/docs/components/dialog/example.svelte +13 -0
  183. package/docs/components/dialog-board/README.md +41 -0
  184. package/docs/components/dialog-board/example.svelte +5 -0
  185. package/docs/components/drawer/README.md +70 -0
  186. package/docs/components/drawer/example.svelte +12 -0
  187. package/docs/components/flex-form/README.md +55 -0
  188. package/docs/components/flex-form/example.svelte +7 -0
  189. package/docs/components/flex-row-form/README.md +56 -0
  190. package/docs/components/flex-row-form/example.svelte +7 -0
  191. package/docs/components/form-field/README.md +84 -0
  192. package/docs/components/form-field/example.svelte +10 -0
  193. package/docs/components/full-screen-overlay/README.md +66 -0
  194. package/docs/components/full-screen-overlay/example.svelte +11 -0
  195. package/docs/components/grid-form/README.md +75 -0
  196. package/docs/components/grid-form/example.svelte +9 -0
  197. package/docs/components/group-check-box/README.md +147 -0
  198. package/docs/components/group-check-box/example.svelte +21 -0
  199. package/docs/components/group-radio-box/README.md +144 -0
  200. package/docs/components/group-radio-box/example.svelte +21 -0
  201. package/docs/components/header-layout/README.md +65 -0
  202. package/docs/components/header-layout/example.svelte +12 -0
  203. package/docs/components/icon/README.md +87 -0
  204. package/docs/components/icon/example.svelte +9 -0
  205. package/docs/components/icon-button/README.md +91 -0
  206. package/docs/components/icon-button/example.svelte +10 -0
  207. package/docs/components/image-files/README.md +174 -0
  208. package/docs/components/image-files/example.svelte +19 -0
  209. package/docs/components/indicator/README.md +53 -0
  210. package/docs/components/indicator/example.svelte +14 -0
  211. package/docs/components/input-options-select/README.md +163 -0
  212. package/docs/components/input-options-select/example.svelte +16 -0
  213. package/docs/components/list-box/ListItem.svelte +14 -0
  214. package/docs/components/list-box/README.md +221 -0
  215. package/docs/components/list-box/example.svelte +61 -0
  216. package/docs/components/lookup-editor/README.md +137 -0
  217. package/docs/components/lookup-editor/example.svelte +16 -0
  218. package/docs/components/memo-editor/README.md +143 -0
  219. package/docs/components/memo-editor/example.svelte +16 -0
  220. package/docs/components/message-box/README.md +68 -0
  221. package/docs/components/message-box/example.svelte +10 -0
  222. package/docs/components/navigator/README.md +75 -0
  223. package/docs/components/navigator/example.svelte +13 -0
  224. package/docs/components/navigator-menu/README.md +113 -0
  225. package/docs/components/navigator-menu/example.svelte +10 -0
  226. package/docs/components/number-editor/README.md +144 -0
  227. package/docs/components/number-editor/example.svelte +16 -0
  228. package/docs/components/number-range/README.md +116 -0
  229. package/docs/components/number-range/example.svelte +15 -0
  230. package/docs/components/options-multi-select/README.md +161 -0
  231. package/docs/components/options-multi-select/example.svelte +20 -0
  232. package/docs/components/options-select/README.md +160 -0
  233. package/docs/components/options-select/example.svelte +21 -0
  234. package/docs/components/page/README.md +77 -0
  235. package/docs/components/page/example.svelte +10 -0
  236. package/docs/components/pagination/README.md +88 -0
  237. package/docs/components/pagination/example.svelte +10 -0
  238. package/docs/components/pagination-panel/README.md +120 -0
  239. package/docs/components/pagination-panel/example.svelte +16 -0
  240. package/docs/components/popup-hint/README.md +44 -0
  241. package/docs/components/popup-hint/example.svelte +5 -0
  242. package/docs/components/progress-bar/README.md +64 -0
  243. package/docs/components/progress-bar/example.svelte +8 -0
  244. package/docs/components/progress-step-bar/README.md +93 -0
  245. package/docs/components/progress-step-bar/example.svelte +7 -0
  246. package/docs/components/prompts-text-editor/README.md +146 -0
  247. package/docs/components/prompts-text-editor/example.svelte +17 -0
  248. package/docs/components/radio-button/README.md +121 -0
  249. package/docs/components/radio-button/example.svelte +18 -0
  250. package/docs/components/rows-layout/README.md +70 -0
  251. package/docs/components/rows-layout/example.svelte +12 -0
  252. package/docs/components/search-box/README.md +136 -0
  253. package/docs/components/search-box/example.svelte +16 -0
  254. package/docs/components/separator/README.md +44 -0
  255. package/docs/components/separator/example.svelte +7 -0
  256. package/docs/components/sidebar-layout/README.md +65 -0
  257. package/docs/components/sidebar-layout/example.svelte +12 -0
  258. package/docs/components/split/README.md +48 -0
  259. package/docs/components/split/example.svelte +8 -0
  260. package/docs/components/switch/README.md +77 -0
  261. package/docs/components/switch/example.svelte +11 -0
  262. package/docs/components/tabs/README.md +127 -0
  263. package/docs/components/tabs/example.svelte +22 -0
  264. package/docs/components/tag/README.md +63 -0
  265. package/docs/components/tag/example.svelte +8 -0
  266. package/docs/components/text-button/README.md +90 -0
  267. package/docs/components/text-button/example.svelte +9 -0
  268. package/docs/components/text-editor/README.md +147 -0
  269. package/docs/components/text-editor/example.svelte +16 -0
  270. package/docs/components/time-editor/README.md +130 -0
  271. package/docs/components/time-editor/example.svelte +16 -0
  272. package/docs/components/toast/README.md +52 -0
  273. package/docs/components/toast/example.svelte +9 -0
  274. package/docs/components/tooltip/README.md +44 -0
  275. package/docs/components/tooltip/example.svelte +5 -0
  276. package/docs/components/transfer/README.md +109 -0
  277. package/docs/components/transfer/example.svelte +13 -0
  278. package/docs/components/tree-view/README.md +163 -0
  279. package/docs/components/tree-view/example.svelte +26 -0
  280. package/docs/components/unit-number-editor/README.md +192 -0
  281. package/docs/components/unit-number-editor/example.svelte +18 -0
  282. package/package.json +15 -7
@@ -1,6 +1,9 @@
1
1
  <script lang="ts">
2
2
  import {onMount, type Snippet} from 'svelte';
3
3
  import Portal from "svelte-portal";
4
+ import {acquireScrollLock} from "./scrollLock";
5
+ import {outsideClick} from "./outsideClick";
6
+ import {registerEscapeHandler} from "./escapeClose";
4
7
 
5
8
  interface Props {
6
9
  style?: string;
@@ -27,25 +30,6 @@
27
30
  children
28
31
  }: Props = $props();
29
32
 
30
- export function outerClick(node: HTMLElement, callback: () => void) {
31
- const handleClick = (event: MouseEvent) => {
32
- // 如果点击目标不在node内,则触发回调
33
- if (!node.contains(event.target as Node)) {
34
- callback();
35
- }
36
- };
37
-
38
- // 监听全局点击事件(useCapture=true,避免事件冒泡造成的问题)
39
- document.addEventListener('click', handleClick, true);
40
-
41
- // action 必须返回一个对象,可以选实现 update 和 destroy
42
- return {
43
- destroy() {
44
- document.removeEventListener('click', handleClick, true);
45
- }
46
- };
47
- }
48
-
49
33
  let position = $state({top: 0, left: 0, width: 0, height: 0});
50
34
  let hidden = $state(true);
51
35
  let width = $state(0);
@@ -70,18 +54,19 @@
70
54
  width = rect.width;
71
55
  vertPos = (h - position.top - position.height) > rect.height ? `top: ${(position.top + position.height + 1)}px` : `top: ${position.top - rect.height - 1}px`;
72
56
 
57
+ // Regression fix: this used to compare an absolute viewport
58
+ // x-coordinate (position.left/position.left+position.width) against
59
+ // the popup's own rendered width, which doesn't express "is there
60
+ // room to the right/left in the viewport" at all — for a narrow
61
+ // anchor near the left edge it would almost always pick "left"
62
+ // regardless of how wide the popup actually was, clipping wide
63
+ // content (e.g. a multi-column cascade picker) against the right
64
+ // edge of the window instead of flipping side. Compare against the
65
+ // actual viewport width `w` instead.
73
66
  if (align == 'left') {
74
- if (position.left + position.width < width) {
75
- horzPos = "left: 0";
76
- } else {
77
- horzPos = `right: 0`;
78
- }
67
+ horzPos = (position.left + width > w) ? "right: 0" : "left: 0";
79
68
  } else {
80
- if (position.left + position.width > width) {
81
- horzPos = `right: 0`;
82
- } else {
83
- horzPos = "left: 0";
84
- }
69
+ horzPos = (position.left + position.width - width < 0) ? "left: 0" : "right: 0";
85
70
  }
86
71
  if (autoFit) {
87
72
  horzPos += '; width: 100%'
@@ -89,40 +74,59 @@
89
74
  }
90
75
  }
91
76
 
92
- const checkPosition = () => {
93
- if (!hidden && target) {
94
- const rect = target.getBoundingClientRect();
95
- if (rect.top != position.top || rect.left != position.left) {
96
- updatePosition();
97
- }
98
- }
99
- }
100
-
77
+ // Coalesces bursts of triggers (resize + scroll + ResizeObserver firing in
78
+ // the same frame) into a single updatePosition() call instead of one per
79
+ // listener, so repositioning does one layout read/write pass per frame.
80
+ let rafId: number | null = null;
81
+ const scheduleUpdate = () => {
82
+ if (rafId != null) return;
83
+ rafId = requestAnimationFrame(() => {
84
+ rafId = null;
85
+ updatePosition();
86
+ });
87
+ };
101
88
 
102
89
  onMount(() => {
103
- const overflowY = window.getComputedStyle(document.body).overflowY;
104
- document.body.style.overflowY = 'hidden';
105
- setTimeout(() => {
106
- updatePosition();
90
+ const release = acquireScrollLock();
91
+ // Registered later than any overlay already open "under" this popover
92
+ // (e.g. a dialog), so this is topmost on the shared Escape stack and
93
+ // closes first.
94
+ const unregisterEscape = registerEscapeHandler(() => close());
95
+
96
+ // hidden must flip before updatePosition() — updatePosition() is a no-op
97
+ // while hidden, so calling it first (as this used to) skipped the initial
98
+ // positioning entirely and left the popover rendered at (0,0) until the
99
+ // next scroll/resize.
100
+ const showTimeout = setTimeout(() => {
107
101
  hidden = false;
108
- setTimeout(() => {
109
- document.body.style.overflowY = overflowY;
110
- }, 50);
102
+ scheduleUpdate();
111
103
  }, 50); // 延迟显示 portal
112
- window.addEventListener("resize", updatePosition);
113
- document.addEventListener("scroll", updatePosition, {passive: false});
114
- const interval = setInterval(checkPosition, 10); // 100ms 检测一次
115
- const resizeObserver = new ResizeObserver(entries => {
116
- for (let entry of entries) {
117
- updatePosition();
118
- }
119
- });
104
+ const releaseTimeout = setTimeout(release, 100);
105
+ window.addEventListener("resize", scheduleUpdate);
106
+ // capture:true scroll events don't bubble, so a listener on the bubble phase
107
+ // only ever sees window/document-level scroll. Capturing catches scroll on any
108
+ // nested scrollable ancestor (a table body, a dialog's scroll area, etc.) too,
109
+ // which is what keeps the popover pinned to its anchor while that container
110
+ // scrolls. Matches the pattern in FloatingPanel.svelte.
111
+ window.addEventListener("scroll", scheduleUpdate, true);
112
+ // Observing both the anchor and the popover body catches anchor-size changes
113
+ // and content-size changes, replacing the unconditional 10ms poll that used
114
+ // to run regardless of visibility/tab focus and forced a layout every tick.
115
+ const resizeObserver = new ResizeObserver(scheduleUpdate);
120
116
  resizeObserver.observe(contentPanel);
117
+ resizeObserver.observe(target);
121
118
  return () => {
122
- window.removeEventListener("resize", updatePosition);
123
- document.removeEventListener("scroll", updatePosition);
119
+ clearTimeout(showTimeout);
120
+ clearTimeout(releaseTimeout);
121
+ release();
122
+ unregisterEscape();
123
+ window.removeEventListener("resize", scheduleUpdate);
124
+ window.removeEventListener("scroll", scheduleUpdate, true);
124
125
  resizeObserver.disconnect();
125
- clearInterval(interval);
126
+ if (rafId != null) {
127
+ cancelAnimationFrame(rafId);
128
+ rafId = null;
129
+ }
126
130
  };
127
131
  });
128
132
 
@@ -135,7 +139,7 @@
135
139
  <Portal target="body">
136
140
  <div class="uniface-popover-wrapper" class:hidden={hidden}
137
141
  style="{vertPos}; height: auto; left: {position.left-margin}px; width: {position.width}px; ">
138
- <div class="uniface-popover" use:outerClick={close}>
142
+ <div class="uniface-popover" use:outsideClick={close}>
139
143
  <div bind:this={contentPanel} style="{horzPos}">
140
144
  <div class="contents" style="{style}">
141
145
  <div class="contents-inner {bodyClass}">
@@ -11,10 +11,6 @@ interface Props {
11
11
  bodyClass?: string;
12
12
  children?: Snippet;
13
13
  }
14
- declare const Popover: import("svelte").Component<Props, {
15
- outerClick: (node: HTMLElement, callback: () => void) => {
16
- destroy(): void;
17
- };
18
- }, "">;
14
+ declare const Popover: import("svelte").Component<Props, {}, "">;
19
15
  type Popover = ReturnType<typeof Popover>;
20
16
  export default Popover;
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared, stack-aware "close on Escape" primitive.
3
+ *
4
+ * Previously each modal-ish surface either reinvented this (Dialog's
5
+ * `svelte:window onkeydown` with a hand-rolled DOM query to special-case
6
+ * "defer to an open Popover first"), relied on the browser (MessageBox's
7
+ * native <dialog> — left alone here, it already does this correctly) or
8
+ * didn't handle Escape at all (Popover, ModalPopover, FullscreenOverlay,
9
+ * Drawer).
10
+ *
11
+ * Registered handlers form a stack. Only the topmost (most recently
12
+ * registered, i.e. most recently opened) handler runs on Escape — this is
13
+ * what "close the innermost open overlay first" means for nested overlays,
14
+ * and it replaces Dialog's ad-hoc "is a popover open" DOM query with the
15
+ * same mechanism every overlay uses.
16
+ */
17
+ type EscapeHandler = () => void;
18
+ /** Push a handler onto the stack; returns an unregister function. */
19
+ export declare function registerEscapeHandler(handler: EscapeHandler): () => void;
20
+ /**
21
+ * Svelte action form: `use:escapeClose={onclose}`. Registers for as long as
22
+ * the node is mounted.
23
+ */
24
+ export declare function escapeClose(_node: HTMLElement, onclose: EscapeHandler): {
25
+ update(newOnClose: EscapeHandler): void;
26
+ destroy(): void;
27
+ };
28
+ export {};
@@ -0,0 +1,47 @@
1
+ const stack = [];
2
+ let listenerInstalled = false;
3
+ function handleKeyDown(e) {
4
+ if (e.key !== 'Escape' && e.key !== 'Esc')
5
+ return;
6
+ const top = stack[stack.length - 1];
7
+ if (!top)
8
+ return;
9
+ e.preventDefault();
10
+ top();
11
+ }
12
+ function ensureListener() {
13
+ if (listenerInstalled || typeof window === 'undefined')
14
+ return;
15
+ window.addEventListener('keydown', handleKeyDown);
16
+ listenerInstalled = true;
17
+ }
18
+ /** Push a handler onto the stack; returns an unregister function. */
19
+ export function registerEscapeHandler(handler) {
20
+ ensureListener();
21
+ stack.push(handler);
22
+ let registered = true;
23
+ return () => {
24
+ if (!registered)
25
+ return;
26
+ registered = false;
27
+ const idx = stack.lastIndexOf(handler);
28
+ if (idx !== -1)
29
+ stack.splice(idx, 1);
30
+ };
31
+ }
32
+ /**
33
+ * Svelte action form: `use:escapeClose={onclose}`. Registers for as long as
34
+ * the node is mounted.
35
+ */
36
+ export function escapeClose(_node, onclose) {
37
+ let unregister = registerEscapeHandler(onclose);
38
+ return {
39
+ update(newOnClose) {
40
+ unregister();
41
+ unregister = registerEscapeHandler(newOnClose);
42
+ },
43
+ destroy() {
44
+ unregister();
45
+ }
46
+ };
47
+ }
@@ -0,0 +1,8 @@
1
+ export interface FocusTrapOptions {
2
+ /** Set false to make this a no-op — lets callers keep the action wired up
3
+ * unconditionally while still gating the behavior on a reactive value. */
4
+ enabled?: boolean;
5
+ }
6
+ export declare function focusTrap(node: HTMLElement, options?: FocusTrapOptions): {
7
+ destroy(): void;
8
+ };
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Shared focus-trap + focus-restore Svelte action for modal-like surfaces.
3
+ *
4
+ * Previously: Dialog and MessageBox each did a `setTimeout` +
5
+ * `querySelector('button')` to focus *something* on open, and nothing
6
+ * anywhere saved/restored the element that had focus before the overlay
7
+ * opened — closing a dialog silently dropped focus back to the document
8
+ * body instead of wherever the user was before opening it.
9
+ *
10
+ * `use:focusTrap` on a modal's root/box element will:
11
+ * - save `document.activeElement` on mount
12
+ * - move focus to the first focusable descendant (or the node itself)
13
+ * - keep Tab/Shift+Tab cycling within the node's focusable descendants
14
+ * - restore focus to the saved element on destroy
15
+ *
16
+ * Not applied to ContextMenuPanel, which already has its own working
17
+ * roving-focus/aria-activedescendant pattern more appropriate for a
18
+ * `role="menu"` — a second, competing trap there would fight it rather than
19
+ * help. Not applied to MessageBox, whose native <dialog> + showModal()
20
+ * already gives correct trap/restore behavior from the browser for free.
21
+ */
22
+ const FOCUSABLE_SELECTOR = [
23
+ 'a[href]',
24
+ 'button:not([disabled])',
25
+ 'textarea:not([disabled])',
26
+ 'input:not([disabled])',
27
+ 'select:not([disabled])',
28
+ '[tabindex]:not([tabindex="-1"])'
29
+ ].join(',');
30
+ function getFocusable(container) {
31
+ return Array.from(container.querySelectorAll(FOCUSABLE_SELECTOR));
32
+ }
33
+ export function focusTrap(node, options = {}) {
34
+ if (options.enabled === false) {
35
+ return { destroy() { } };
36
+ }
37
+ const previouslyFocused = document.activeElement;
38
+ const focusFirst = () => {
39
+ const focusable = getFocusable(node);
40
+ (focusable[0] ?? node).focus();
41
+ };
42
+ // Deferred one tick: the trap is typically mounted before children with
43
+ // their own autofocus/initial-value logic have settled.
44
+ const raf = requestAnimationFrame(focusFirst);
45
+ const handleKeyDown = (e) => {
46
+ if (e.key !== 'Tab')
47
+ return;
48
+ const focusable = getFocusable(node);
49
+ if (focusable.length === 0) {
50
+ e.preventDefault();
51
+ return;
52
+ }
53
+ const first = focusable[0];
54
+ const last = focusable[focusable.length - 1];
55
+ const active = document.activeElement;
56
+ if (e.shiftKey) {
57
+ if (active === first || !node.contains(active)) {
58
+ e.preventDefault();
59
+ last.focus();
60
+ }
61
+ }
62
+ else {
63
+ if (active === last || !node.contains(active)) {
64
+ e.preventDefault();
65
+ first.focus();
66
+ }
67
+ }
68
+ };
69
+ node.addEventListener('keydown', handleKeyDown);
70
+ return {
71
+ destroy() {
72
+ cancelAnimationFrame(raf);
73
+ node.removeEventListener('keydown', handleKeyDown);
74
+ // Only restore if focus is still somewhere inside this trap (or on
75
+ // the body) — if the app already moved focus elsewhere on its own
76
+ // (e.g. opening another overlay), don't fight it.
77
+ const active = document.activeElement;
78
+ if (previouslyFocused && (active == null || active === document.body || node.contains(active))) {
79
+ previouslyFocused.focus();
80
+ }
81
+ }
82
+ };
83
+ }
@@ -0,0 +1,27 @@
1
+ import { type Component } from 'svelte';
2
+ /**
3
+ * Attempts to mount `component` to `document.body` once. Returns whether it
4
+ * succeeded. SSR-safe (no-ops when window/document aren't available) and
5
+ * swallows mount errors (e.g. test environments where Svelte's client mount
6
+ * is bypassed) rather than throwing — this was silent-fallback-by-design in
7
+ * every controller that had its own copy of this check.
8
+ */
9
+ export declare function mountToBody(component: Component): boolean;
10
+ /**
11
+ * Shared "lazy-mount this controller's panel to document.body on first
12
+ * use, then reuse the same instance" helper.
13
+ *
14
+ * Previously each overlay controller singleton (Indicator, Toast,
15
+ * ContextMenu, MessageBox) carried an identical `panelMounted` flag +
16
+ * `ensureMounted()` pair. This is the one implementation; call the returned
17
+ * function at the start of whatever triggers showing the overlay (`show()`,
18
+ * etc.) — it's idempotent, so calling it on every show is fine.
19
+ *
20
+ * DialogManager is intentionally NOT migrated to this: its `<DialogBoard>`
21
+ * self-reports whether it's already mounted (so a board the *consumer*
22
+ * placed manually in their app root and the manager's own auto-mount don't
23
+ * both render), which needs the mounted flag to be externally observable —
24
+ * a different shape than this memoized-closure helper provides. It reuses
25
+ * `mountToBody` directly instead.
26
+ */
27
+ export declare function createSingletonMounter(component: Component): () => void;
@@ -0,0 +1,45 @@
1
+ import { mount } from 'svelte';
2
+ /**
3
+ * Attempts to mount `component` to `document.body` once. Returns whether it
4
+ * succeeded. SSR-safe (no-ops when window/document aren't available) and
5
+ * swallows mount errors (e.g. test environments where Svelte's client mount
6
+ * is bypassed) rather than throwing — this was silent-fallback-by-design in
7
+ * every controller that had its own copy of this check.
8
+ */
9
+ export function mountToBody(component) {
10
+ if (typeof window === 'undefined' || typeof document === 'undefined' || !document.body) {
11
+ return false;
12
+ }
13
+ try {
14
+ mount(component, { target: document.body });
15
+ return true;
16
+ }
17
+ catch {
18
+ return false;
19
+ }
20
+ }
21
+ /**
22
+ * Shared "lazy-mount this controller's panel to document.body on first
23
+ * use, then reuse the same instance" helper.
24
+ *
25
+ * Previously each overlay controller singleton (Indicator, Toast,
26
+ * ContextMenu, MessageBox) carried an identical `panelMounted` flag +
27
+ * `ensureMounted()` pair. This is the one implementation; call the returned
28
+ * function at the start of whatever triggers showing the overlay (`show()`,
29
+ * etc.) — it's idempotent, so calling it on every show is fine.
30
+ *
31
+ * DialogManager is intentionally NOT migrated to this: its `<DialogBoard>`
32
+ * self-reports whether it's already mounted (so a board the *consumer*
33
+ * placed manually in their app root and the manager's own auto-mount don't
34
+ * both render), which needs the mounted flag to be externally observable —
35
+ * a different shape than this memoized-closure helper provides. It reuses
36
+ * `mountToBody` directly instead.
37
+ */
38
+ export function createSingletonMounter(component) {
39
+ let mounted = false;
40
+ return () => {
41
+ if (mounted)
42
+ return;
43
+ mounted = mountToBody(component);
44
+ };
45
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Shared "click outside this element" Svelte action.
3
+ *
4
+ * Previously reimplemented near-identically in Popover.svelte (`outerClick`)
5
+ * and FloatingPanel.svelte (`handleClickOutside` + a manual
6
+ * document.addEventListener), plus a third, unused variant in
7
+ * uniface-utils.ts. This is the one canonical implementation; new
8
+ * outside-click needs should use this rather than adding a fourth.
9
+ *
10
+ * Uses the capture phase so it fires before the click can be stopped by a
11
+ * descendant's own handler, and so nested overlays (a popover inside a
12
+ * dialog) each see the click regardless of bubbling order.
13
+ */
14
+ export type OutsideClickOptions = (() => void) | {
15
+ callback: () => void;
16
+ /**
17
+ * Elements that should NOT count as "outside" even though they're
18
+ * not inside `node` — typically the trigger/anchor element, whose
19
+ * own click handler usually toggles the overlay open. Without this,
20
+ * clicking the trigger to close would fire this callback first and
21
+ * then the trigger's own handler would immediately reopen it.
22
+ */
23
+ exclude?: Array<HTMLElement | null | undefined>;
24
+ };
25
+ export declare function outsideClick(node: HTMLElement, options: OutsideClickOptions): {
26
+ update(newOptions: OutsideClickOptions): void;
27
+ destroy(): void;
28
+ };
@@ -0,0 +1,20 @@
1
+ export function outsideClick(node, options) {
2
+ let opts = typeof options === 'function' ? { callback: options } : options;
3
+ const handleClick = (event) => {
4
+ const t = event.target;
5
+ if (node.contains(t))
6
+ return;
7
+ if (opts.exclude?.some(el => el && el.contains(t)))
8
+ return;
9
+ opts.callback();
10
+ };
11
+ document.addEventListener('click', handleClick, true);
12
+ return {
13
+ update(newOptions) {
14
+ opts = typeof newOptions === 'function' ? { callback: newOptions } : newOptions;
15
+ },
16
+ destroy() {
17
+ document.removeEventListener('click', handleClick, true);
18
+ }
19
+ };
20
+ }
@@ -0,0 +1,8 @@
1
+ export declare function lockBodyScroll(): void;
2
+ export declare function unlockBodyScroll(): void;
3
+ /**
4
+ * Convenience wrapper for the common "lock on mount, unlock on cleanup"
5
+ * pattern, idempotent against being released more than once (e.g. both a
6
+ * timer and an unmount cleanup racing to call it).
7
+ */
8
+ export declare function acquireScrollLock(): () => void;
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Shared, reference-counted body scroll-lock.
3
+ *
4
+ * Any number of overlays (Popover, Dialog, Drawer, ModalPopover,
5
+ * FullscreenOverlay, ...) can call lockBodyScroll()/unlockBodyScroll()
6
+ * independently, in any open/close order, without one overlay's close
7
+ * clobbering another still-open overlay's lock. Only the very first lock
8
+ * captures the pre-lock overflowY value, and only the transition back to
9
+ * zero open locks restores it — this is what makes stacking safe.
10
+ *
11
+ * Originally lived inline in Popover.svelte; generalized here so every
12
+ * modal-ish overlay in the library shares one lock instead of each
13
+ * reinventing (or omitting) its own save/restore.
14
+ */
15
+ let lockCount = 0;
16
+ let savedOverflowY = null;
17
+ export function lockBodyScroll() {
18
+ if (typeof document === 'undefined')
19
+ return;
20
+ if (lockCount === 0) {
21
+ savedOverflowY = window.getComputedStyle(document.body).overflowY;
22
+ document.body.style.overflowY = 'hidden';
23
+ }
24
+ lockCount++;
25
+ }
26
+ export function unlockBodyScroll() {
27
+ if (typeof document === 'undefined')
28
+ return;
29
+ lockCount = Math.max(0, lockCount - 1);
30
+ if (lockCount === 0 && savedOverflowY != null) {
31
+ document.body.style.overflowY = savedOverflowY;
32
+ savedOverflowY = null;
33
+ }
34
+ }
35
+ /**
36
+ * Convenience wrapper for the common "lock on mount, unlock on cleanup"
37
+ * pattern, idempotent against being released more than once (e.g. both a
38
+ * timer and an unmount cleanup racing to call it).
39
+ */
40
+ export function acquireScrollLock() {
41
+ lockBodyScroll();
42
+ let released = false;
43
+ return () => {
44
+ if (released)
45
+ return;
46
+ released = true;
47
+ unlockBodyScroll();
48
+ };
49
+ }
@@ -1,6 +1,5 @@
1
1
  export function clickOutside(node) {
2
2
  const handleClick = (event) => {
3
- // @ts-ignore 因为 event.target 是 EventTarget,不能直接传给 contains
4
3
  if (node && !node.contains(event.target) && !event.defaultPrevented) {
5
4
  node.dispatchEvent(new CustomEvent('outerClick', {
6
5
  bubbles: true, // 通常自定义事件要冒泡,方便捕获
@@ -1,24 +1,11 @@
1
1
  // src/lib/overlay/context-menu/ContextMenu.svelte.ts
2
- import { mount } from 'svelte';
3
2
  import ContextMenuPanel from "./ContextMenuPanel.svelte";
4
- let panelMounted = false;
3
+ import { createSingletonMounter } from '../common/mountSingleton';
5
4
  /**
6
5
  * 懒挂载:第一次 show() 时把面板挂到 document.body,后续复用同一个实例。
7
6
  * 不需要用户在 app 根节点手动挂 <ContextMenu />。
8
7
  */
9
- function ensureMounted() {
10
- if (panelMounted)
11
- return;
12
- if (typeof window !== 'undefined' && typeof document !== 'undefined' && document.body) {
13
- try {
14
- mount(ContextMenuPanel, { target: document.body });
15
- panelMounted = true;
16
- }
17
- catch {
18
- // Silent fallback
19
- }
20
- }
21
- }
8
+ const ensureMounted = createSingletonMounter(ContextMenuPanel);
22
9
  class ContextMenuController {
23
10
  visible = $state(false);
24
11
  position = $state({ x: 0, y: 0 });
@@ -5,14 +5,22 @@
5
5
 
6
6
  let focusedIndex = $state(-1);
7
7
  let menuPanel = $state<HTMLDivElement | null>(null);
8
+ let previouslyFocused: HTMLElement | null = null;
8
9
 
9
10
  // 状态响应器:一旦菜单被激活,立即强制捕获焦点
10
11
  $effect(() => {
11
12
  if (contextMenuSvelte.visible) {
13
+ previouslyFocused = document.activeElement as HTMLElement | null;
12
14
  focusedIndex = -1; // 初始化键盘高亮位置
13
15
  tick().then(() => {
14
16
  menuPanel?.focus();
15
17
  });
18
+ } else if (previouslyFocused) {
19
+ // Regression: closing the menu previously just dropped focus back
20
+ // to the document body instead of wherever the user was before
21
+ // opening it (right-clicking a table row, say).
22
+ previouslyFocused.focus();
23
+ previouslyFocused = null;
16
24
  }
17
25
  });
18
26
 
@@ -71,6 +79,10 @@
71
79
  break;
72
80
  case 'Escape':
73
81
  event.preventDefault();
82
+ // Stop this from also reaching the shared window-level Escape
83
+ // stack (escapeClose.ts) — otherwise a context menu opened on
84
+ // top of a Dialog/Popover would close both on one Escape press.
85
+ event.stopPropagation();
74
86
  contextMenuSvelte.hide();
75
87
  break;
76
88
  case 'Tab':
@@ -78,6 +90,16 @@
78
90
  break;
79
91
  }
80
92
  }
93
+
94
+ function activateItem(event: MouseEvent | KeyboardEvent, index: number) {
95
+ if (event instanceof KeyboardEvent && event.key !== 'Enter' && event.key !== ' ') return;
96
+ event.preventDefault();
97
+ event.stopPropagation();
98
+ const item = contextMenuSvelte.items[index];
99
+ if (item.disabled) return;
100
+ item.action?.();
101
+ contextMenuSvelte.hide();
102
+ }
81
103
  </script>
82
104
 
83
105
  {#if contextMenuSvelte.visible}
@@ -103,8 +125,10 @@
103
125
  class:focused={focusedIndex === index}
104
126
  style={item.style || ''}
105
127
  role="menuitem"
128
+ tabindex="-1"
106
129
  aria-disabled={item.disabled}
107
- onclick={() => { if(!item.disabled && item.action) { item.action(); contextMenuSvelte.hide(); } }}
130
+ onclick={(event) => activateItem(event, index)}
131
+ onkeydown={(event) => activateItem(event, index)}
108
132
  onmouseenter={() => { if(!item.disabled) focusedIndex = index; }}
109
133
  >
110
134
  {#if item.icon}
@@ -125,7 +149,7 @@
125
149
  so the menu drifted away from the cursor by the page's scroll offset. `fixed`
126
150
  keeps the same coordinate frame as the values driving left/top. */
127
151
  position: fixed;
128
- z-index: 1000;
152
+ z-index: var(--uniface-z-context-menu, 1000);
129
153
  outline: none;
130
154
  border-radius: var(--uniface-radius);
131
155
  /* 1. 深度整合:基础环境与阴影规范 */
@@ -184,4 +208,4 @@
184
208
  height: 1px;
185
209
  background-color: var(--uniface-contextmenu-color-separator);
186
210
  margin: var(--uniface-contextmenu-separator-margin) 0;
187
- }</style>
211
+ }</style>