@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.
- package/README.md +29 -54
- package/README_CN.md +37 -57
- package/dist/app_layout/ClassicLayout.svelte +8 -40
- package/dist/app_layout/ColumnsLayout.svelte +3 -3
- package/dist/app_layout/HeaderLayout.svelte +4 -20
- package/dist/app_layout/RowsLayout.svelte +3 -3
- package/dist/app_layout/SidebarLayout.svelte +4 -20
- package/dist/base-calendar/CalendarBase.svelte +29 -6
- package/dist/base-calendar/CalendarBase.svelte.d.ts +4 -0
- package/dist/base-calendar/MonthPickupPanel.svelte +19 -6
- package/dist/base-calendar/MonthPickupPanel.svelte.d.ts +1 -1
- package/dist/base-calendar/MonthlyCalendar.svelte +18 -2
- package/dist/base-calendar/YearPickupPanel.svelte +24 -6
- package/dist/base-calendar/YearPickupPanel.svelte.d.ts +1 -1
- package/dist/composite/pagination_panel/PaginationPanel.svelte +2 -0
- package/dist/composite/transfer/Transfer.svelte +12 -6
- package/dist/data_display/accordion/Accordion.svelte +1 -4
- package/dist/data_display/card/CommonCardActionBar.svelte +31 -37
- package/dist/data_display/concise_data_table/Column.d.ts +6 -0
- package/dist/data_display/concise_data_table/ConciseListTable.svelte +4 -3
- package/dist/data_display/concise_data_table/DataCell.svelte +3 -3
- package/dist/data_display/list_box/ListBox.svelte +96 -23
- package/dist/data_display/list_box/ListBox.svelte.d.ts +4 -0
- package/dist/data_display/tree_view/TreeNodeView.svelte +25 -4
- package/dist/data_table/DataTable.svelte +5 -5
- package/dist/data_table/DataTable.svelte.d.ts +36 -16
- package/dist/data_table/lib/ActionsColumn.d.ts +2 -2
- package/dist/data_table/lib/DataColumn.d.ts +7 -1
- package/dist/data_table/lib/RowAction.d.ts +5 -5
- package/dist/data_table/lib/clickOutSide.js +6 -5
- package/dist/data_table/parts/FixedHeaderPanel.svelte +18 -3
- package/dist/data_table/parts/FixedRow.svelte +10 -1
- package/dist/data_table/parts/HrefCell.svelte +1 -1
- package/dist/data_table/parts/TableHeaderPanel.svelte +18 -2
- package/dist/data_table/parts/TextCell.svelte +2 -2
- package/dist/form/attachment_files/AttachmentEditor.svelte +20 -15
- package/dist/form/attachment_files/FileRender.svelte +1 -1
- package/dist/form/attachment_files/FileUploadBar.svelte +5 -2
- package/dist/form/attachment_files/FileUploadPanel.svelte +11 -73
- package/dist/form/cascade_options_select/CascadeOptionSelect.svelte +10 -2
- package/dist/form/cascade_options_select/OptionsBox.svelte +48 -2
- package/dist/form/checkbox/CheckBox.svelte +2 -7
- package/dist/form/color_picker/ColorPicker.svelte +150 -35
- package/dist/form/color_picker/ColorPicker.svelte.d.ts +7 -25
- package/dist/form/color_picker/ColorPickerPanel.svelte +23 -20
- package/dist/form/color_picker/ColorPickerPanel.svelte.d.ts +5 -18
- package/dist/form/color_picker/README.md +14 -7
- package/dist/form/color_picker/README_CN.md +14 -7
- package/dist/form/common_editor/CommonEditor.svelte +18 -4
- package/dist/form/common_editor/CommonPicker.svelte +14 -10
- package/dist/form/common_editor/CommonPopupEditor.svelte +8 -1
- package/dist/form/common_range_editor/CommonRangeEditor.svelte +11 -2
- package/dist/form/date_picker/DatePicker.svelte +1 -1
- package/dist/form/date_picker/DateTimePicker.svelte +1 -1
- package/dist/form/date_picker/ScrollBar.svelte +20 -12
- package/dist/form/date_picker/ScrollBar.svelte.d.ts +4 -18
- package/dist/form/date_range/DateRangeEditor.svelte +11 -3
- package/dist/form/group_box/group-check-box/GroupCheckBox.svelte +6 -1
- package/dist/form/group_box/group-radio-box/GroupRadioBox.svelte +6 -1
- package/dist/form/image-files/ImageFilesField.svelte +51 -7
- package/dist/form/image-files/ImagePreview.svelte +23 -9
- package/dist/form/image-files/ImageRender.svelte +69 -11
- package/dist/form/input_options_select/InputOptionsSelect.svelte +10 -2
- package/dist/form/lookup_editor/LookupEditor.svelte +9 -2
- package/dist/form/memo_editor/MemoEditor.svelte +2 -2
- package/dist/form/number_editor/NumberEditor.svelte +2 -2
- package/dist/form/options_multi_select/OptionsMultiSelect.svelte +20 -12
- package/dist/form/options_select/OptionsSelect.svelte +15 -6
- package/dist/form/options_select/OptionsSelect.svelte.d.ts +39 -17
- package/dist/form/prompts_text_editor/PromptsTextEditor.svelte +10 -2
- package/dist/form/radio_button/RadioButton.svelte +2 -8
- package/dist/form/switch/Switch.svelte +4 -4
- package/dist/form/text_editor/TextEditor.svelte +2 -2
- package/dist/form/unit-number-editor/UnitNumberEditor.svelte +10 -2
- package/dist/general/button/Button.svelte +5 -0
- package/dist/general/button/Button.svelte.d.ts +1 -0
- package/dist/general/button/IconButton.svelte +3 -1
- package/dist/general/button/IconButton.svelte.d.ts +1 -0
- package/dist/general/button/TextButton.svelte +3 -0
- package/dist/general/button/TextButton.svelte.d.ts +1 -0
- package/dist/general/icon/Icon.svelte +18 -1
- package/dist/general/split/Split.svelte +24 -4
- package/dist/general/tag/Tag.svelte +9 -1
- package/dist/navigation/nav_menu/MenuItem.d.ts +4 -2
- package/dist/navigation/nav_menu/NavigatorMenu.svelte +157 -34
- package/dist/navigation/nav_menu/NavigatorMenu.svelte.d.ts +2 -3
- package/dist/navigation/nav_menu/NavigatorMenuItem.svelte +25 -13
- package/dist/navigation/nav_menu/NavigatorMenuItem.svelte.d.ts +3 -3
- package/dist/navigation/navigator/Navigator.svelte +17 -17
- package/dist/navigation/page-stack/PageStack.svelte +160 -0
- package/dist/navigation/page-stack/PageStack.svelte.d.ts +24 -0
- package/dist/navigation/page-stack/PageStackContext.d.ts +25 -0
- package/dist/navigation/page-stack/PageStackContext.js +12 -0
- package/dist/navigation/page-stack/PageStackFrame.svelte +24 -0
- package/dist/navigation/page-stack/PageStackFrame.svelte.d.ts +11 -0
- package/dist/navigation/page-stack/README.md +154 -0
- package/dist/navigation/page-stack/README_CN.md +154 -0
- package/dist/navigation/page-stack/index.d.ts +6 -0
- package/dist/navigation/page-stack/index.js +4 -0
- package/dist/navigation/pagination/Pagination.svelte +114 -14
- package/dist/navigation/progress_step_bar/ProgressStepBlock.svelte +1 -5
- package/dist/navigation/tabs/Tabs.svelte +42 -10
- package/dist/navigation/tabs/Tabs.svelte.d.ts +2 -2
- package/dist/overlay/common/FloatingPanel.svelte +3 -17
- package/dist/overlay/common/FullscreenOverlay.svelte +27 -3
- package/dist/overlay/common/FullscreenOverlay.svelte.d.ts +6 -27
- package/dist/overlay/common/ModalPopover.svelte +18 -2
- package/dist/overlay/common/Popover.svelte +61 -57
- package/dist/overlay/common/Popover.svelte.d.ts +1 -5
- package/dist/overlay/common/escapeClose.d.ts +28 -0
- package/dist/overlay/common/escapeClose.js +47 -0
- package/dist/overlay/common/focusTrap.d.ts +8 -0
- package/dist/overlay/common/focusTrap.js +83 -0
- package/dist/overlay/common/mountSingleton.d.ts +27 -0
- package/dist/overlay/common/mountSingleton.js +45 -0
- package/dist/overlay/common/outsideClick.d.ts +28 -0
- package/dist/overlay/common/outsideClick.js +20 -0
- package/dist/overlay/common/scrollLock.d.ts +8 -0
- package/dist/overlay/common/scrollLock.js +49 -0
- package/dist/overlay/common/uniface-utils.js +0 -1
- package/dist/overlay/context-menu/ContextMenu.svelte.js +2 -15
- package/dist/overlay/context-menu/ContextMenuPanel.svelte +27 -3
- package/dist/overlay/dialog/Dialog.svelte +48 -32
- package/dist/overlay/dialog/DialogManager.svelte.js +7 -9
- package/dist/overlay/dialog/DialogWrapper.svelte +5 -2
- package/dist/overlay/drawer/Drawer.svelte +30 -3
- package/dist/overlay/indicator/Indicator.svelte.js +2 -15
- package/dist/overlay/indicator/IndicatorPanel.svelte +4 -1
- package/dist/overlay/message-box/MessageBox.svelte.js +2 -15
- package/dist/overlay/message-box/MessageBoxPanel.svelte +19 -3
- package/dist/overlay/toast/Toast.svelte.js +2 -15
- package/dist/overlay/toast/ToastPanel.svelte +1 -1
- package/dist/overlay/tooltip/Tooltip.svelte +1 -1
- package/dist/ticatec-uniface-web.css +128 -13
- package/dist/types.d.ts +4 -3
- package/dist/utils/prefixFilter.d.ts +3 -3
- package/dist/utils/prefixFilter.js +3 -5
- package/docs/components/README.md +133 -0
- package/docs/components/accordion/README.md +60 -0
- package/docs/components/accordion/example.svelte +7 -0
- package/docs/components/action-bar/README.md +114 -0
- package/docs/components/action-bar/example.svelte +7 -0
- package/docs/components/app-top-bar/README.md +60 -0
- package/docs/components/app-top-bar/example.svelte +9 -0
- package/docs/components/attachment-files/README.md +332 -0
- package/docs/components/attachment-files/example.svelte +16 -0
- package/docs/components/box/README.md +63 -0
- package/docs/components/box/example.svelte +10 -0
- package/docs/components/breadcrumbs/README.md +86 -0
- package/docs/components/breadcrumbs/example.svelte +11 -0
- package/docs/components/button/README.md +103 -0
- package/docs/components/button/example.svelte +10 -0
- package/docs/components/card/README.md +113 -0
- package/docs/components/card/example.svelte +10 -0
- package/docs/components/cascade-options-select/README.md +162 -0
- package/docs/components/cascade-options-select/example.svelte +18 -0
- package/docs/components/checkbox/README.md +103 -0
- package/docs/components/checkbox/example.svelte +15 -0
- package/docs/components/classic-layout/README.md +76 -0
- package/docs/components/classic-layout/example.svelte +16 -0
- package/docs/components/color-picker/README.md +132 -0
- package/docs/components/color-picker/example.svelte +16 -0
- package/docs/components/columns-layout/README.md +70 -0
- package/docs/components/columns-layout/example.svelte +12 -0
- package/docs/components/common-dialog/README.md +109 -0
- package/docs/components/common-dialog/example.svelte +12 -0
- package/docs/components/concise-data-table/README.md +307 -0
- package/docs/components/concise-data-table/example.svelte +16 -0
- package/docs/components/context-menu/README.md +99 -0
- package/docs/components/context-menu/example.svelte +10 -0
- package/docs/components/criteria-field/README.md +69 -0
- package/docs/components/criteria-field/example.svelte +9 -0
- package/docs/components/data-table/README.md +365 -0
- package/docs/components/data-table/example.svelte +23 -0
- package/docs/components/date-picker/README.md +136 -0
- package/docs/components/date-picker/example.svelte +16 -0
- package/docs/components/date-range/README.md +122 -0
- package/docs/components/date-range/example.svelte +15 -0
- package/docs/components/date-time-picker/README.md +142 -0
- package/docs/components/date-time-picker/example.svelte +16 -0
- package/docs/components/dialog/README.md +174 -0
- package/docs/components/dialog/example.svelte +13 -0
- package/docs/components/dialog-board/README.md +41 -0
- package/docs/components/dialog-board/example.svelte +5 -0
- package/docs/components/drawer/README.md +70 -0
- package/docs/components/drawer/example.svelte +12 -0
- package/docs/components/flex-form/README.md +55 -0
- package/docs/components/flex-form/example.svelte +7 -0
- package/docs/components/flex-row-form/README.md +56 -0
- package/docs/components/flex-row-form/example.svelte +7 -0
- package/docs/components/form-field/README.md +84 -0
- package/docs/components/form-field/example.svelte +10 -0
- package/docs/components/full-screen-overlay/README.md +66 -0
- package/docs/components/full-screen-overlay/example.svelte +11 -0
- package/docs/components/grid-form/README.md +75 -0
- package/docs/components/grid-form/example.svelte +9 -0
- package/docs/components/group-check-box/README.md +147 -0
- package/docs/components/group-check-box/example.svelte +21 -0
- package/docs/components/group-radio-box/README.md +144 -0
- package/docs/components/group-radio-box/example.svelte +21 -0
- package/docs/components/header-layout/README.md +65 -0
- package/docs/components/header-layout/example.svelte +12 -0
- package/docs/components/icon/README.md +87 -0
- package/docs/components/icon/example.svelte +9 -0
- package/docs/components/icon-button/README.md +91 -0
- package/docs/components/icon-button/example.svelte +10 -0
- package/docs/components/image-files/README.md +174 -0
- package/docs/components/image-files/example.svelte +19 -0
- package/docs/components/indicator/README.md +53 -0
- package/docs/components/indicator/example.svelte +14 -0
- package/docs/components/input-options-select/README.md +163 -0
- package/docs/components/input-options-select/example.svelte +16 -0
- package/docs/components/list-box/ListItem.svelte +14 -0
- package/docs/components/list-box/README.md +221 -0
- package/docs/components/list-box/example.svelte +61 -0
- package/docs/components/lookup-editor/README.md +137 -0
- package/docs/components/lookup-editor/example.svelte +16 -0
- package/docs/components/memo-editor/README.md +143 -0
- package/docs/components/memo-editor/example.svelte +16 -0
- package/docs/components/message-box/README.md +68 -0
- package/docs/components/message-box/example.svelte +10 -0
- package/docs/components/navigator/README.md +75 -0
- package/docs/components/navigator/example.svelte +13 -0
- package/docs/components/navigator-menu/README.md +113 -0
- package/docs/components/navigator-menu/example.svelte +10 -0
- package/docs/components/number-editor/README.md +144 -0
- package/docs/components/number-editor/example.svelte +16 -0
- package/docs/components/number-range/README.md +116 -0
- package/docs/components/number-range/example.svelte +15 -0
- package/docs/components/options-multi-select/README.md +161 -0
- package/docs/components/options-multi-select/example.svelte +20 -0
- package/docs/components/options-select/README.md +160 -0
- package/docs/components/options-select/example.svelte +21 -0
- package/docs/components/page/README.md +77 -0
- package/docs/components/page/example.svelte +10 -0
- package/docs/components/pagination/README.md +88 -0
- package/docs/components/pagination/example.svelte +10 -0
- package/docs/components/pagination-panel/README.md +120 -0
- package/docs/components/pagination-panel/example.svelte +16 -0
- package/docs/components/popup-hint/README.md +44 -0
- package/docs/components/popup-hint/example.svelte +5 -0
- package/docs/components/progress-bar/README.md +64 -0
- package/docs/components/progress-bar/example.svelte +8 -0
- package/docs/components/progress-step-bar/README.md +93 -0
- package/docs/components/progress-step-bar/example.svelte +7 -0
- package/docs/components/prompts-text-editor/README.md +146 -0
- package/docs/components/prompts-text-editor/example.svelte +17 -0
- package/docs/components/radio-button/README.md +121 -0
- package/docs/components/radio-button/example.svelte +18 -0
- package/docs/components/rows-layout/README.md +70 -0
- package/docs/components/rows-layout/example.svelte +12 -0
- package/docs/components/search-box/README.md +136 -0
- package/docs/components/search-box/example.svelte +16 -0
- package/docs/components/separator/README.md +44 -0
- package/docs/components/separator/example.svelte +7 -0
- package/docs/components/sidebar-layout/README.md +65 -0
- package/docs/components/sidebar-layout/example.svelte +12 -0
- package/docs/components/split/README.md +48 -0
- package/docs/components/split/example.svelte +8 -0
- package/docs/components/switch/README.md +77 -0
- package/docs/components/switch/example.svelte +11 -0
- package/docs/components/tabs/README.md +127 -0
- package/docs/components/tabs/example.svelte +22 -0
- package/docs/components/tag/README.md +63 -0
- package/docs/components/tag/example.svelte +8 -0
- package/docs/components/text-button/README.md +90 -0
- package/docs/components/text-button/example.svelte +9 -0
- package/docs/components/text-editor/README.md +147 -0
- package/docs/components/text-editor/example.svelte +16 -0
- package/docs/components/time-editor/README.md +130 -0
- package/docs/components/time-editor/example.svelte +16 -0
- package/docs/components/toast/README.md +52 -0
- package/docs/components/toast/example.svelte +9 -0
- package/docs/components/tooltip/README.md +44 -0
- package/docs/components/tooltip/example.svelte +5 -0
- package/docs/components/transfer/README.md +109 -0
- package/docs/components/transfer/example.svelte +13 -0
- package/docs/components/tree-view/README.md +163 -0
- package/docs/components/tree-view/example.svelte +26 -0
- package/docs/components/unit-number-editor/README.md +192 -0
- package/docs/components/unit-number-editor/example.svelte +18 -0
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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
|
-
|
|
109
|
-
document.body.style.overflowY = overflowY;
|
|
110
|
-
}, 50);
|
|
102
|
+
scheduleUpdate();
|
|
111
103
|
}, 50); // 延迟显示 portal
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
123
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
3
|
+
import { createSingletonMounter } from '../common/mountSingleton';
|
|
5
4
|
/**
|
|
6
5
|
* 懒挂载:第一次 show() 时把面板挂到 document.body,后续复用同一个实例。
|
|
7
6
|
* 不需要用户在 app 根节点手动挂 <ContextMenu />。
|
|
8
7
|
*/
|
|
9
|
-
|
|
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={() =>
|
|
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>
|