@ticatec/uniface-element 5.0.1 → 5.0.3

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 (261) hide show
  1. package/README.md +29 -54
  2. package/README_CN.md +37 -57
  3. package/dist/base-calendar/CalendarBase.svelte +29 -6
  4. package/dist/base-calendar/CalendarBase.svelte.d.ts +4 -0
  5. package/dist/base-calendar/MonthPickupPanel.svelte +19 -6
  6. package/dist/base-calendar/MonthPickupPanel.svelte.d.ts +1 -1
  7. package/dist/base-calendar/MonthlyCalendar.svelte +18 -2
  8. package/dist/base-calendar/YearPickupPanel.svelte +24 -6
  9. package/dist/base-calendar/YearPickupPanel.svelte.d.ts +1 -1
  10. package/dist/composite/pagination_panel/PaginationPanel.svelte +2 -0
  11. package/dist/data_display/card/CommonCardActionBar.svelte +34 -14
  12. package/dist/data_display/concise_data_table/Column.d.ts +6 -0
  13. package/dist/data_display/concise_data_table/ConciseListTable.svelte +4 -3
  14. package/dist/data_display/concise_data_table/DataCell.svelte +3 -3
  15. package/dist/data_display/list_box/ListBox.svelte +90 -15
  16. package/dist/data_display/list_box/ListBox.svelte.d.ts +4 -0
  17. package/dist/data_display/tree_view/TreeNodeView.svelte +25 -4
  18. package/dist/data_table/DataTable.svelte +5 -5
  19. package/dist/data_table/DataTable.svelte.d.ts +36 -16
  20. package/dist/data_table/lib/ActionsColumn.d.ts +2 -2
  21. package/dist/data_table/lib/DataColumn.d.ts +7 -1
  22. package/dist/data_table/lib/RowAction.d.ts +5 -5
  23. package/dist/data_table/parts/FixedHeaderPanel.svelte +18 -3
  24. package/dist/data_table/parts/FixedRow.svelte +10 -1
  25. package/dist/data_table/parts/HrefCell.svelte +1 -1
  26. package/dist/data_table/parts/TableHeaderPanel.svelte +18 -2
  27. package/dist/data_table/parts/TextCell.svelte +2 -2
  28. package/dist/form/attachment_files/AttachmentEditor.svelte +9 -9
  29. package/dist/form/attachment_files/FileRender.svelte +1 -1
  30. package/dist/form/attachment_files/FileUploadPanel.svelte +5 -1
  31. package/dist/form/cascade_options_select/CascadeOptionSelect.svelte +10 -2
  32. package/dist/form/cascade_options_select/OptionsBox.svelte +48 -2
  33. package/dist/form/color_picker/ColorPickerPanel.svelte +23 -20
  34. package/dist/form/color_picker/ColorPickerPanel.svelte.d.ts +5 -18
  35. package/dist/form/common_editor/CommonEditor.svelte +36 -5
  36. package/dist/form/common_editor/CommonPicker.svelte +11 -4
  37. package/dist/form/common_editor/CommonPopupEditor.svelte +11 -2
  38. package/dist/form/common_range_editor/CommonRangeEditor.svelte +11 -2
  39. package/dist/form/date_picker/DatePicker.svelte +1 -1
  40. package/dist/form/date_picker/DateTimePicker.svelte +1 -1
  41. package/dist/form/date_picker/ScrollBar.svelte +20 -12
  42. package/dist/form/date_picker/ScrollBar.svelte.d.ts +4 -18
  43. package/dist/form/date_range/DateRangeEditor.svelte +11 -3
  44. package/dist/form/group_box/group-check-box/GroupCheckBox.svelte +6 -1
  45. package/dist/form/group_box/group-radio-box/GroupRadioBox.svelte +6 -1
  46. package/dist/form/image-files/ImageFilesField.svelte +51 -7
  47. package/dist/form/image-files/ImagePreview.svelte +8 -2
  48. package/dist/form/image-files/ImageRender.svelte +69 -11
  49. package/dist/form/input_options_select/InputOptionsSelect.svelte +10 -2
  50. package/dist/form/lookup_editor/LookupEditor.svelte +9 -2
  51. package/dist/form/number_editor/NumberEditor.svelte +2 -0
  52. package/dist/form/options_multi_select/OptionsMultiSelect.svelte +19 -3
  53. package/dist/form/options_select/OptionsSelect.svelte +15 -6
  54. package/dist/form/options_select/OptionsSelect.svelte.d.ts +39 -17
  55. package/dist/form/prompts_text_editor/PromptsTextEditor.svelte +12 -3
  56. package/dist/form/search_box/SearchBox.svelte +2 -1
  57. package/dist/form/switch/Switch.svelte +4 -4
  58. package/dist/form/text_editor/PasswordEditor.svelte +2 -1
  59. package/dist/form/text_editor/TextEditor.svelte +2 -1
  60. package/dist/form/time_editor/TimeEditor.svelte +2 -0
  61. package/dist/form/unit-number-editor/UnitNumberEditor.svelte +13 -2
  62. package/dist/form/unit-number-editor/UnitNumberEditor.svelte.d.ts +1 -0
  63. package/dist/general/button/Button.svelte +3 -0
  64. package/dist/general/button/Button.svelte.d.ts +1 -0
  65. package/dist/general/button/IconButton.svelte +3 -1
  66. package/dist/general/button/IconButton.svelte.d.ts +1 -0
  67. package/dist/general/button/TextButton.svelte +3 -0
  68. package/dist/general/button/TextButton.svelte.d.ts +1 -0
  69. package/dist/general/icon/Icon.svelte +18 -1
  70. package/dist/general/tag/Tag.svelte +9 -1
  71. package/dist/navigation/nav_menu/NavigatorMenu.svelte +2 -2
  72. package/dist/navigation/nav_menu/NavigatorMenuItem.svelte +10 -1
  73. package/dist/navigation/navigator/Navigator.svelte +17 -17
  74. package/dist/navigation/page-stack/PageStack.svelte +160 -0
  75. package/dist/navigation/page-stack/PageStack.svelte.d.ts +24 -0
  76. package/dist/navigation/page-stack/PageStackContext.d.ts +25 -0
  77. package/dist/navigation/page-stack/PageStackContext.js +12 -0
  78. package/dist/navigation/page-stack/PageStackFrame.svelte +24 -0
  79. package/dist/navigation/page-stack/PageStackFrame.svelte.d.ts +11 -0
  80. package/dist/navigation/page-stack/README.md +154 -0
  81. package/dist/navigation/page-stack/README_CN.md +154 -0
  82. package/dist/navigation/page-stack/index.d.ts +6 -0
  83. package/dist/navigation/page-stack/index.js +4 -0
  84. package/dist/navigation/pagination/Pagination.svelte +114 -14
  85. package/dist/navigation/tabs/Tabs.svelte +42 -10
  86. package/dist/navigation/tabs/Tabs.svelte.d.ts +2 -2
  87. package/dist/overlay/common/FloatingPanel.svelte +3 -11
  88. package/dist/overlay/common/FullscreenOverlay.svelte +27 -3
  89. package/dist/overlay/common/FullscreenOverlay.svelte.d.ts +6 -27
  90. package/dist/overlay/common/ModalPopover.svelte +18 -2
  91. package/dist/overlay/common/Popover.svelte +56 -57
  92. package/dist/overlay/common/Popover.svelte.d.ts +1 -5
  93. package/dist/overlay/common/escapeClose.d.ts +28 -0
  94. package/dist/overlay/common/escapeClose.js +47 -0
  95. package/dist/overlay/common/focusTrap.d.ts +8 -0
  96. package/dist/overlay/common/focusTrap.js +83 -0
  97. package/dist/overlay/common/mountSingleton.d.ts +27 -0
  98. package/dist/overlay/common/mountSingleton.js +45 -0
  99. package/dist/overlay/common/outsideClick.d.ts +28 -0
  100. package/dist/overlay/common/outsideClick.js +20 -0
  101. package/dist/overlay/common/scrollLock.d.ts +8 -0
  102. package/dist/overlay/common/scrollLock.js +49 -0
  103. package/dist/overlay/context-menu/ContextMenu.svelte.js +2 -15
  104. package/dist/overlay/context-menu/ContextMenuPanel.svelte +13 -1
  105. package/dist/overlay/dialog/Dialog.svelte +46 -29
  106. package/dist/overlay/dialog/DialogManager.svelte.js +7 -9
  107. package/dist/overlay/drawer/Drawer.svelte +30 -3
  108. package/dist/overlay/indicator/Indicator.svelte.js +2 -15
  109. package/dist/overlay/indicator/IndicatorPanel.svelte +4 -1
  110. package/dist/overlay/message-box/MessageBox.svelte.js +2 -15
  111. package/dist/overlay/message-box/MessageBoxPanel.svelte +19 -3
  112. package/dist/overlay/toast/Toast.svelte.js +2 -15
  113. package/dist/overlay/toast/ToastPanel.svelte +1 -1
  114. package/dist/overlay/tooltip/Tooltip.svelte +1 -1
  115. package/dist/ticatec-uniface-web.css +102 -7
  116. package/dist/types.d.ts +4 -3
  117. package/docs/components/README.md +133 -0
  118. package/docs/components/accordion/README.md +60 -0
  119. package/docs/components/accordion/example.svelte +7 -0
  120. package/docs/components/action-bar/README.md +114 -0
  121. package/docs/components/action-bar/example.svelte +7 -0
  122. package/docs/components/app-top-bar/README.md +60 -0
  123. package/docs/components/app-top-bar/example.svelte +9 -0
  124. package/docs/components/attachment-files/README.md +332 -0
  125. package/docs/components/attachment-files/example.svelte +16 -0
  126. package/docs/components/box/README.md +63 -0
  127. package/docs/components/box/example.svelte +10 -0
  128. package/docs/components/breadcrumbs/README.md +86 -0
  129. package/docs/components/breadcrumbs/example.svelte +11 -0
  130. package/docs/components/button/README.md +103 -0
  131. package/docs/components/button/example.svelte +10 -0
  132. package/docs/components/card/README.md +113 -0
  133. package/docs/components/card/example.svelte +10 -0
  134. package/docs/components/cascade-options-select/README.md +162 -0
  135. package/docs/components/cascade-options-select/example.svelte +18 -0
  136. package/docs/components/checkbox/README.md +103 -0
  137. package/docs/components/checkbox/example.svelte +15 -0
  138. package/docs/components/classic-layout/README.md +76 -0
  139. package/docs/components/classic-layout/example.svelte +16 -0
  140. package/docs/components/color-picker/README.md +132 -0
  141. package/docs/components/color-picker/example.svelte +16 -0
  142. package/docs/components/columns-layout/README.md +70 -0
  143. package/docs/components/columns-layout/example.svelte +12 -0
  144. package/docs/components/common-dialog/README.md +109 -0
  145. package/docs/components/common-dialog/example.svelte +12 -0
  146. package/docs/components/concise-data-table/README.md +307 -0
  147. package/docs/components/concise-data-table/example.svelte +16 -0
  148. package/docs/components/context-menu/README.md +99 -0
  149. package/docs/components/context-menu/example.svelte +10 -0
  150. package/docs/components/criteria-field/README.md +69 -0
  151. package/docs/components/criteria-field/example.svelte +9 -0
  152. package/docs/components/data-table/README.md +365 -0
  153. package/docs/components/data-table/example.svelte +23 -0
  154. package/docs/components/date-picker/README.md +136 -0
  155. package/docs/components/date-picker/example.svelte +16 -0
  156. package/docs/components/date-range/README.md +122 -0
  157. package/docs/components/date-range/example.svelte +15 -0
  158. package/docs/components/date-time-picker/README.md +142 -0
  159. package/docs/components/date-time-picker/example.svelte +16 -0
  160. package/docs/components/dialog/README.md +174 -0
  161. package/docs/components/dialog/example.svelte +13 -0
  162. package/docs/components/dialog-board/README.md +41 -0
  163. package/docs/components/dialog-board/example.svelte +5 -0
  164. package/docs/components/drawer/README.md +70 -0
  165. package/docs/components/drawer/example.svelte +12 -0
  166. package/docs/components/flex-form/README.md +55 -0
  167. package/docs/components/flex-form/example.svelte +7 -0
  168. package/docs/components/flex-row-form/README.md +56 -0
  169. package/docs/components/flex-row-form/example.svelte +7 -0
  170. package/docs/components/form-field/README.md +84 -0
  171. package/docs/components/form-field/example.svelte +10 -0
  172. package/docs/components/full-screen-overlay/README.md +66 -0
  173. package/docs/components/full-screen-overlay/example.svelte +11 -0
  174. package/docs/components/grid-form/README.md +75 -0
  175. package/docs/components/grid-form/example.svelte +9 -0
  176. package/docs/components/group-check-box/README.md +147 -0
  177. package/docs/components/group-check-box/example.svelte +21 -0
  178. package/docs/components/group-radio-box/README.md +144 -0
  179. package/docs/components/group-radio-box/example.svelte +21 -0
  180. package/docs/components/header-layout/README.md +65 -0
  181. package/docs/components/header-layout/example.svelte +12 -0
  182. package/docs/components/icon/README.md +87 -0
  183. package/docs/components/icon/example.svelte +9 -0
  184. package/docs/components/icon-button/README.md +91 -0
  185. package/docs/components/icon-button/example.svelte +10 -0
  186. package/docs/components/image-files/README.md +174 -0
  187. package/docs/components/image-files/example.svelte +19 -0
  188. package/docs/components/indicator/README.md +53 -0
  189. package/docs/components/indicator/example.svelte +14 -0
  190. package/docs/components/input-options-select/README.md +163 -0
  191. package/docs/components/input-options-select/example.svelte +16 -0
  192. package/docs/components/list-box/ListItem.svelte +14 -0
  193. package/docs/components/list-box/README.md +221 -0
  194. package/docs/components/list-box/example.svelte +61 -0
  195. package/docs/components/lookup-editor/README.md +137 -0
  196. package/docs/components/lookup-editor/example.svelte +16 -0
  197. package/docs/components/memo-editor/README.md +143 -0
  198. package/docs/components/memo-editor/example.svelte +16 -0
  199. package/docs/components/message-box/README.md +68 -0
  200. package/docs/components/message-box/example.svelte +10 -0
  201. package/docs/components/navigator/README.md +75 -0
  202. package/docs/components/navigator/example.svelte +13 -0
  203. package/docs/components/navigator-menu/README.md +113 -0
  204. package/docs/components/navigator-menu/example.svelte +10 -0
  205. package/docs/components/number-editor/README.md +144 -0
  206. package/docs/components/number-editor/example.svelte +16 -0
  207. package/docs/components/number-range/README.md +116 -0
  208. package/docs/components/number-range/example.svelte +15 -0
  209. package/docs/components/options-multi-select/README.md +161 -0
  210. package/docs/components/options-multi-select/example.svelte +20 -0
  211. package/docs/components/options-select/README.md +160 -0
  212. package/docs/components/options-select/example.svelte +21 -0
  213. package/docs/components/page/README.md +77 -0
  214. package/docs/components/page/example.svelte +10 -0
  215. package/docs/components/pagination/README.md +88 -0
  216. package/docs/components/pagination/example.svelte +10 -0
  217. package/docs/components/pagination-panel/README.md +120 -0
  218. package/docs/components/pagination-panel/example.svelte +16 -0
  219. package/docs/components/popup-hint/README.md +44 -0
  220. package/docs/components/popup-hint/example.svelte +5 -0
  221. package/docs/components/progress-bar/README.md +64 -0
  222. package/docs/components/progress-bar/example.svelte +8 -0
  223. package/docs/components/progress-step-bar/README.md +93 -0
  224. package/docs/components/progress-step-bar/example.svelte +7 -0
  225. package/docs/components/prompts-text-editor/README.md +146 -0
  226. package/docs/components/prompts-text-editor/example.svelte +17 -0
  227. package/docs/components/radio-button/README.md +121 -0
  228. package/docs/components/radio-button/example.svelte +18 -0
  229. package/docs/components/rows-layout/README.md +70 -0
  230. package/docs/components/rows-layout/example.svelte +12 -0
  231. package/docs/components/search-box/README.md +136 -0
  232. package/docs/components/search-box/example.svelte +16 -0
  233. package/docs/components/separator/README.md +44 -0
  234. package/docs/components/separator/example.svelte +7 -0
  235. package/docs/components/sidebar-layout/README.md +65 -0
  236. package/docs/components/sidebar-layout/example.svelte +12 -0
  237. package/docs/components/split/README.md +48 -0
  238. package/docs/components/split/example.svelte +8 -0
  239. package/docs/components/switch/README.md +77 -0
  240. package/docs/components/switch/example.svelte +11 -0
  241. package/docs/components/tabs/README.md +127 -0
  242. package/docs/components/tabs/example.svelte +22 -0
  243. package/docs/components/tag/README.md +63 -0
  244. package/docs/components/tag/example.svelte +8 -0
  245. package/docs/components/text-button/README.md +90 -0
  246. package/docs/components/text-button/example.svelte +9 -0
  247. package/docs/components/text-editor/README.md +147 -0
  248. package/docs/components/text-editor/example.svelte +16 -0
  249. package/docs/components/time-editor/README.md +130 -0
  250. package/docs/components/time-editor/example.svelte +16 -0
  251. package/docs/components/toast/README.md +52 -0
  252. package/docs/components/toast/example.svelte +9 -0
  253. package/docs/components/tooltip/README.md +44 -0
  254. package/docs/components/tooltip/example.svelte +5 -0
  255. package/docs/components/transfer/README.md +109 -0
  256. package/docs/components/transfer/example.svelte +13 -0
  257. package/docs/components/tree-view/README.md +163 -0
  258. package/docs/components/tree-view/example.svelte +26 -0
  259. package/docs/components/unit-number-editor/README.md +192 -0
  260. package/docs/components/unit-number-editor/example.svelte +18 -0
  261. package/package.json +15 -7
@@ -1,5 +1,6 @@
1
1
  <script lang="ts">
2
2
  import {type Snippet} from "svelte";
3
+ import {outsideClick} from "./outsideClick";
3
4
 
4
5
  interface Props {
5
6
  /** 锚定目标元素;面板会出现在目标下方 */
@@ -33,14 +34,6 @@
33
34
  };
34
35
  };
35
36
 
36
- const handleClickOutside = (e: MouseEvent) => {
37
- if (!panelEl) return;
38
- const t = e.target as Node;
39
- if (panelEl.contains(t)) return;
40
- if (target && target.contains(t)) return;
41
- onclose?.();
42
- };
43
-
44
37
  // 把面板挂到 body 下,跳出任何父容器的 overflow / stacking context。
45
38
  // 不依赖 Portal 库,直接 DOM API。
46
39
  const mountToBody = (node: HTMLElement) => {
@@ -55,12 +48,10 @@
55
48
  $effect(() => {
56
49
  if (!panelEl) return;
57
50
  updatePosition();
58
- document.addEventListener('click', handleClickOutside, true);
59
51
  window.addEventListener('resize', updatePosition);
60
52
  // 捕获阶段监听所有 scroll(包括内部容器),保证面板跟随目标
61
53
  window.addEventListener('scroll', updatePosition, true);
62
54
  return () => {
63
- document.removeEventListener('click', handleClickOutside, true);
64
55
  window.removeEventListener('resize', updatePosition);
65
56
  window.removeEventListener('scroll', updatePosition, true);
66
57
  };
@@ -70,8 +61,9 @@
70
61
  <div
71
62
  bind:this={panelEl}
72
63
  class="floating-panel {className}"
73
- style="position: fixed; top: {coords.top}px; left: {coords.left}px; z-index: 1000;"
64
+ style="position: fixed; top: {coords.top}px; left: {coords.left}px; z-index: var(--uniface-z-floating-panel, 1010);"
74
65
  use:mountToBody
66
+ use:outsideClick={{ callback: () => onclose?.(), exclude: [target] }}
75
67
  >
76
68
  {@render children?.()}
77
69
  </div>
@@ -1,17 +1,41 @@
1
1
  <script lang="ts">
2
2
 
3
3
  import Portal from "svelte-portal";
4
+ import {onMount} from "svelte";
5
+ import type {Snippet} from "svelte";
6
+ import {acquireScrollLock} from "./scrollLock";
7
+ import {registerEscapeHandler} from "./escapeClose";
4
8
 
5
- export let visible: boolean = false;
9
+ interface Props {
10
+ visible?: boolean;
11
+ children?: Snippet;
12
+ }
6
13
 
14
+ let {visible = $bindable(false), children}: Props = $props();
15
+
16
+ // Regression: this modal surface previously never locked body scroll or
17
+ // closed on Escape.
18
+ onMount(() => {
19
+ const release = acquireScrollLock();
20
+ const unregisterEscape = registerEscapeHandler(() => { visible = false; });
21
+ return () => {
22
+ release();
23
+ unregisterEscape();
24
+ };
25
+ });
7
26
  </script>
8
27
 
9
28
  {#if visible}
10
29
  <Portal target="body">
11
30
  <div class="uniface-mask-overlay">
31
+ <!-- Backdrop click-to-close convenience only; aria-hidden must not be used
32
+ here since it would hide the real, arbitrary interactive content
33
+ rendered via `children` from assistive tech. -->
34
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
35
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
12
36
  <div style="position: relative; width: 100%; height: 100%; overflow: hidden"
13
- on:click={()=>{visible = false}} aria-hidden="true">
14
- <slot></slot>
37
+ onclick={()=>{visible = false}}>
38
+ {@render children?.()}
15
39
  </div>
16
40
  </div>
17
41
  </Portal>
@@ -1,29 +1,8 @@
1
- interface $$__sveltets_2_IsomorphicComponent<Props extends Record<string, any> = any, Events extends Record<string, any> = any, Slots extends Record<string, any> = any, Exports = {}, Bindings = string> {
2
- new (options: import('svelte').ComponentConstructorOptions<Props>): import('svelte').SvelteComponent<Props, Events, Slots> & {
3
- $$bindings?: Bindings;
4
- } & Exports;
5
- (internal: unknown, props: Props & {
6
- $$events?: Events;
7
- $$slots?: Slots;
8
- }): Exports & {
9
- $set?: any;
10
- $on?: any;
11
- };
12
- z_$$bindings?: Bindings;
13
- }
14
- type $$__sveltets_2_PropsWithChildren<Props, Slots> = Props & (Slots extends {
15
- default: any;
16
- } ? Props extends Record<string, never> ? any : {
17
- children?: any;
18
- } : {});
19
- declare const FullscreenOverlay: $$__sveltets_2_IsomorphicComponent<$$__sveltets_2_PropsWithChildren<{
1
+ import type { Snippet } from "svelte";
2
+ interface Props {
20
3
  visible?: boolean;
21
- }, {
22
- default: {};
23
- }>, {
24
- [evt: string]: CustomEvent<any>;
25
- }, {
26
- default: {};
27
- }, {}, string>;
28
- type FullscreenOverlay = InstanceType<typeof FullscreenOverlay>;
4
+ children?: Snippet;
5
+ }
6
+ declare const FullscreenOverlay: import("svelte").Component<Props, {}, "visible">;
7
+ type FullscreenOverlay = ReturnType<typeof FullscreenOverlay>;
29
8
  export default FullscreenOverlay;
@@ -1,6 +1,8 @@
1
1
  <script lang="ts">
2
2
  import {type Snippet, onMount} from "svelte";
3
3
  import Portal from "svelte-portal";
4
+ import {acquireScrollLock} from "./scrollLock";
5
+ import {registerEscapeHandler} from "./escapeClose";
4
6
 
5
7
  interface Props {
6
8
  visible?: boolean;
@@ -20,15 +22,29 @@
20
22
 
21
23
  onMount(() => {
22
24
  setTimeout(() => hidden = false, 100);
25
+ // Regression: this modal surface previously never locked body scroll
26
+ // or closed on Escape.
27
+ const release = acquireScrollLock();
28
+ const unregisterEscape = registerEscapeHandler(() => {
29
+ if (canClose) visible = false;
30
+ });
31
+ return () => {
32
+ release();
33
+ unregisterEscape();
34
+ };
23
35
  });
24
36
  </script>
25
37
 
26
38
  {#if visible}
27
39
  <Portal target="body">
28
40
  <div class="uniface-mask-overlay" class:hidden data-theme={theme}>
41
+ <!-- Backdrop click-to-close convenience only; aria-hidden must not be used
42
+ here since it would hide the real, arbitrary interactive content
43
+ rendered via `children` from assistive tech. -->
44
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
45
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
29
46
  <div class="uniface-mask-center"
30
- onclick={() => { if (canClose) visible = false; }}
31
- aria-hidden="true">
47
+ onclick={() => { if (canClose) visible = false; }}>
32
48
  <div style="flex: 0 0 auto">
33
49
  {@render children()}
34
50
  </div>
@@ -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,45 +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);
104
+ const releaseTimeout = setTimeout(release, 100);
105
+ window.addEventListener("resize", scheduleUpdate);
113
106
  // capture:true — scroll events don't bubble, so a listener on the bubble phase
114
107
  // only ever sees window/document-level scroll. Capturing catches scroll on any
115
108
  // nested scrollable ancestor (a table body, a dialog's scroll area, etc.) too,
116
109
  // which is what keeps the popover pinned to its anchor while that container
117
110
  // scrolls. Matches the pattern in FloatingPanel.svelte.
118
- window.addEventListener("scroll", updatePosition, true);
119
- const interval = setInterval(checkPosition, 10); // 10ms 检测一次
120
- const resizeObserver = new ResizeObserver(entries => {
121
- for (let entry of entries) {
122
- updatePosition();
123
- }
124
- });
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);
125
116
  resizeObserver.observe(contentPanel);
117
+ resizeObserver.observe(target);
126
118
  return () => {
127
- window.removeEventListener("resize", updatePosition);
128
- window.removeEventListener("scroll", updatePosition, true);
119
+ clearTimeout(showTimeout);
120
+ clearTimeout(releaseTimeout);
121
+ release();
122
+ unregisterEscape();
123
+ window.removeEventListener("resize", scheduleUpdate);
124
+ window.removeEventListener("scroll", scheduleUpdate, true);
129
125
  resizeObserver.disconnect();
130
- clearInterval(interval);
126
+ if (rafId != null) {
127
+ cancelAnimationFrame(rafId);
128
+ rafId = null;
129
+ }
131
130
  };
132
131
  });
133
132
 
@@ -140,7 +139,7 @@
140
139
  <Portal target="body">
141
140
  <div class="uniface-popover-wrapper" class:hidden={hidden}
142
141
  style="{vertPos}; height: auto; left: {position.left-margin}px; width: {position.width}px; ">
143
- <div class="uniface-popover" use:outerClick={close}>
142
+ <div class="uniface-popover" use:outsideClick={close}>
144
143
  <div bind:this={contentPanel} style="{horzPos}">
145
144
  <div class="contents" style="{style}">
146
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;