@workbench-kit/react 0.0.2-prototype.0.2.4 → 0.0.2-prototype.0.2.40
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 +172 -0
- package/package.json +38 -21
- package/src/index.ts +74 -34
- package/src/jdw/JsonCodeEditorPane.tsx +7 -6
- package/src/jdw/builtins/renderBuiltinWidgetLeaf.tsx +1 -3
- package/src/jdw/createBuiltinJdwRegistry.ts +1 -4
- package/src/jdw/cssRenderBackend.tsx +1 -4
- package/src/jdw/fixtures/jdw-fixtures.ts +0 -15
- package/src/json-config/JsonConfigWorkbench.tsx +1 -1
- package/src/layout/index.ts +11 -2
- package/src/layout/panel/Panel.tsx +0 -3
- package/src/layout/panel/index.ts +0 -2
- package/src/layout/sidebar/SideBarTree.tsx +301 -0
- package/src/layout/sidebar/SidebarActionIconBar.tsx +3 -3
- package/src/layout/sidebar/WorkbenchSidebarActions.tsx +1 -1
- package/src/layout/sidebar/index.ts +13 -0
- package/src/modal/Modal.tsx +55 -25
- package/src/modal/ModalTitlebar.tsx +8 -8
- package/src/modal/modalPosition.ts +3 -6
- package/src/modal/useModalFocusTrap.ts +142 -0
- package/src/modal/useModalWindowFrame.ts +2 -2
- package/src/overlay/ContextMenu.tsx +133 -21
- package/src/overlay/context-menu-density.css +22 -0
- package/src/overlay/context-menu-item.ts +17 -0
- package/src/overlay/context-menu.css +6 -2
- package/src/overlay/index.ts +6 -1
- package/src/overlay/measureAnchoredOverlayPanel.ts +4 -6
- package/src/overlay/pointerPassthroughRegion.ts +17 -1
- package/src/overlay/usePointerPassthroughRegion.ts +35 -6
- package/src/primitives/absolute-box/AbsoluteBox.tsx +4 -4
- package/src/primitives/app-icon/AppIcon.tsx +33 -0
- package/src/primitives/app-icon/app-icon.css +28 -0
- package/src/primitives/button/Button.tsx +4 -4
- package/src/primitives/catalog-browse-card/CatalogBrowseCard.tsx +6 -6
- package/src/primitives/catalog-browse-pane/CatalogBrowsePane.tsx +39 -32
- package/src/primitives/catalog-browse-pane/catalog-browse-pane.css +4 -0
- package/src/primitives/catalog-filter-overlay/CatalogFilterOverlay.tsx +2 -2
- package/src/primitives/checkbox/Checkbox.tsx +1 -1
- package/src/primitives/chip/Chip.tsx +3 -12
- package/src/primitives/chip/index.ts +2 -2
- package/src/primitives/codicon/Codicon.tsx +1 -1
- package/src/primitives/file-icon/FileIcon.tsx +1 -1
- package/src/primitives/icon-button/IconButton.tsx +1 -1
- package/src/primitives/index.ts +7 -2
- package/src/primitives/library-detail-layout/LibraryDetailLayout.tsx +123 -9
- package/src/primitives/library-detail-layout/library-detail-layout.css +57 -0
- package/src/primitives/library-detail-layout/resolve-hero-cover-media.ts +80 -0
- package/src/primitives/library-facet-filter-panel/LibraryFacetFilterPanel.tsx +12 -12
- package/src/primitives/library-facet-filter-strip/LibraryFacetFilterStrip.tsx +4 -4
- package/src/primitives/number-input/NumberInput.tsx +5 -5
- package/src/primitives/primitives.css +1 -0
- package/src/primitives/record-media-hero/RecordMediaHero.tsx +10 -6
- package/src/primitives/scroll-area/ScrollArea.tsx +5 -5
- package/src/primitives/scroll-area-infinite-load/useScrollAreaInfiniteLoad.ts +5 -5
- package/src/primitives/searchable-multi-select/SearchableMultiSelect.tsx +7 -7
- package/src/primitives/select/Select.tsx +1 -13
- package/src/primitives/text-area/TextArea.tsx +1 -1
- package/src/primitives/text-input/TextInput.tsx +1 -1
- package/src/primitives/workbench-media-slot/WorkbenchMediaPlaceholder.tsx +2 -2
- package/src/primitives/workbench-media-slot/WorkbenchMediaSlot.tsx +16 -10
- package/src/primitives/workbench-thumbnail/WorkbenchThumbnail.tsx +4 -4
- package/src/scrollbars.css +2 -6
- package/src/styles/core.css +16 -0
- package/src/styles/foundation.css +4 -0
- package/src/styles/overlay.css +2 -0
- package/src/styles.css +1 -15
- package/src/utils/clamp.ts +4 -0
- package/src/utils/normalizeKeyToken.ts +13 -0
- package/src/utils/readNumber.ts +4 -0
- package/src/widget-tree/WidgetSourceEditor.tsx +0 -14
- package/src/widget-tree/WidgetTreeCanvasPreview.tsx +3 -10
- package/src/widget-tree/WidgetTreeLab.tsx +2 -23
- package/src/widget-tree/WidgetTreeSidePanel.tsx +0 -5
- package/src/widget-tree/WidgetTreeWorkspaceShell.tsx +2 -2
- package/src/widget-tree/index.ts +1 -5
- package/src/workbench/auth/WorkbenchLoginView.tsx +1 -2
- package/src/workbench/auth/auth-flows.css +14 -0
- package/src/workbench/chat/ChatCommandProposalCard.tsx +4 -4
- package/src/workbench/chat/ChatConversationBar.tsx +3 -3
- package/src/workbench/chat/ChatHistoryMenu.tsx +1 -1
- package/src/workbench/chat/ChatMessageCollapsible.tsx +4 -0
- package/src/workbench/chat/ChatMessageItem.tsx +68 -10
- package/src/workbench/chat/ChatMessageList.tsx +1 -1
- package/src/workbench/chat/ChatPanel.tsx +32 -23
- package/src/workbench/chat/ChatPhasedRunProgress.tsx +86 -13
- package/src/workbench/chat/ChatRenameDialog.tsx +1 -1
- package/src/workbench/chat/chat-conversation.css +2 -2
- package/src/workbench/chat/chat-ui.css +8 -0
- package/src/workbench/chat/chatConversation.ts +2 -2
- package/src/workbench/chat/chatMessageMeta.tsx +4 -4
- package/src/workbench/chat/index.ts +1 -0
- package/src/workbench/chat/types.ts +7 -7
- package/src/workbench/chat/useCancelRuntimeOnUnmount.ts +4 -4
- package/src/workbench/chat/useChatPrependPagination.ts +10 -10
- package/src/workbench/chat/useSlashCommandSuggest.ts +1 -1
- package/src/workbench/{WorkbenchModalPortal.tsx → chrome/WorkbenchModalPortal.tsx} +1 -1
- package/src/workbench/chrome/workbench-chrome.css +4 -0
- package/src/workbench/chrome/workbench-layout-regions.css +13 -0
- package/src/workbench/chrome/workbench-shell-titlebar.css +0 -8
- package/src/workbench/{CommandGroupShell.tsx → commands/CommandGroupShell.tsx} +2 -2
- package/src/workbench/{CommandList.tsx → commands/CommandList.tsx} +6 -5
- package/src/workbench/{CommandPalette.tsx → commands/CommandPalette.tsx} +16 -42
- package/src/workbench/{ConfirmationFlow.tsx → commands/ConfirmationFlow.tsx} +5 -5
- package/src/workbench/{ShortcutCommandBridge.tsx → commands/ShortcutCommandBridge.tsx} +17 -13
- package/src/workbench/commands/WorkbenchQuickOpen.tsx +462 -0
- package/src/workbench/{command-model.ts → commands/command-model.ts} +5 -1
- package/src/workbench/commands/command-ui.ts +3 -0
- package/src/workbench/{commands.ts → commands/commands.ts} +45 -1
- package/src/workbench/commands/createWorkspaceFilesQuickOpenProvider.ts +86 -0
- package/src/workbench/commands/quick-open-model.ts +66 -0
- package/src/workbench/{WorkbenchEditorTabs.tsx → editor/WorkbenchEditorTabs.tsx} +13 -3
- package/src/workbench/{editor-tabs-dnd.ts → editor/editor-tabs-dnd.ts} +1 -1
- package/src/workbench/{editorTabContextMenu.ts → editor/editorTabContextMenu.ts} +43 -4
- package/src/workbench/{useEditorTabsStripDnd.ts → editor/useEditorTabsStripDnd.ts} +1 -1
- package/src/workbench/{useWorkbenchEditorTabContextMenu.tsx → editor/useWorkbenchEditorTabContextMenu.tsx} +16 -2
- package/src/workbench/editor-tabs/index.ts +10 -9
- package/src/workbench/index.ts +114 -74
- package/src/workbench/management/CommandManagementSidebar.tsx +16 -30
- package/src/workbench/management/ExtensionManagementPanel.tsx +84 -46
- package/src/workbench/management/ExtensionManagementSidebar.tsx +79 -45
- package/src/workbench/management/IntegrationCommandActions.tsx +8 -1
- package/src/workbench/management/IntegrationSettingsSurface.tsx +2 -5
- package/src/workbench/management/IntegrationsShell.tsx +1 -1
- package/src/workbench/management/KeybindingCaptureField.tsx +1 -9
- package/src/workbench/management/WorkbenchDialogFrame.tsx +2 -2
- package/src/workbench/management/WorkbenchNotice.tsx +17 -12
- package/src/workbench/management/createWorkbenchNotify.tsx +88 -0
- package/src/workbench/management/extension-install-approval.ts +72 -0
- package/src/workbench/management/extension-management-filters.ts +38 -0
- package/src/workbench/management/index.ts +18 -3
- package/src/workbench/management/management.css +28 -0
- package/src/workbench/management/types.ts +23 -1
- package/src/workbench/management/useWorkbenchNotify.ts +10 -0
- package/src/workbench/{MarkdownPreview.tsx → markdown/MarkdownPreview.tsx} +22 -7
- package/src/workbench/markdown/markdownRemarkPlugins.ts +12 -0
- package/src/workbench/markdown/sanitizeMarkdownHref.ts +29 -0
- package/src/workbench/schema/workbenchDocumentRenderer.tsx +48 -121
- package/src/workbench/settings/StructuredDataForm.tsx +1 -4
- package/src/workbench/settings/StructuredDataSchemaPanel.tsx +39 -44
- package/src/workbench/settings/sectionedPanelScrollSpy.ts +0 -81
- package/src/workbench/settings/structuredDataFormModel.ts +2 -1
- package/src/workbench/settings/structuredDataSchema.ts +24 -13
- package/src/workbench/settings/structuredDataSchemaSection.ts +4 -0
- package/src/workbench/settings/structuredDataSchemaTable.ts +5 -2
- package/src/workbench/{ActivityBar.tsx → shell/ActivityBar.tsx} +8 -12
- package/src/workbench/{ArtifactShell.tsx → shell/ArtifactShell.tsx} +25 -25
- package/src/workbench/{SplitView.tsx → shell/SplitView.tsx} +3 -3
- package/src/workbench/{StatusBar.tsx → shell/StatusBar.tsx} +8 -8
- package/src/workbench/{StructuredArtifactEditor.tsx → shell/StructuredArtifactEditor.tsx} +9 -9
- package/src/workbench/{Timeline.tsx → shell/Timeline.tsx} +77 -20
- package/src/workbench/{WorkbenchCanvasShell.defaults.ts → shell/WorkbenchCanvasShell.defaults.ts} +1 -1
- package/src/workbench/{WorkbenchCanvasShell.model.ts → shell/WorkbenchCanvasShell.model.ts} +5 -5
- package/src/workbench/{WorkbenchCanvasShell.tsx → shell/WorkbenchCanvasShell.tsx} +6 -6
- package/src/workbench/{WorkbenchDesktopTitleBar.tsx → shell/WorkbenchDesktopTitleBar.tsx} +15 -15
- package/src/workbench/{WorkbenchShell.tsx → shell/WorkbenchShell.tsx} +45 -6
- package/src/workbench/{WorkbenchShellTitleBarLayoutControls.tsx → shell/WorkbenchShellTitleBarLayoutControls.tsx} +16 -19
- package/src/workbench/{WorkbenchStandaloneShell.tsx → shell/WorkbenchStandaloneShell.tsx} +15 -22
- package/src/workbench/{WorkbenchViewEditor.tsx → shell/WorkbenchViewEditor.tsx} +7 -7
- package/src/workbench/{WorkbenchViewSidebar.tsx → shell/WorkbenchViewSidebar.tsx} +7 -7
- package/src/workbench/{WorkbenchWindowChromeControls.tsx → shell/WorkbenchWindowChromeControls.tsx} +18 -15
- package/src/workbench/shell/activity-bar.css +47 -0
- package/src/workbench/{activityBarOrder.ts → shell/activityBarOrder.ts} +2 -1
- package/src/workbench/{shell.ts → shell/shell.ts} +20 -13
- package/src/workbench/{shellViewModel.ts → shell/shellViewModel.ts} +3 -3
- package/src/workbench/{standalone.ts → shell/standalone.ts} +14 -2
- package/src/workbench/{useDeferredWorkbenchMount.ts → shell/useDeferredWorkbenchMount.ts} +3 -3
- package/src/workbench/{useWorkbenchAppearanceDocumentSync.tsx → shell/useWorkbenchAppearanceDocumentSync.tsx} +1 -1
- package/src/workbench/{useWorkbenchSidebarActionBarDnd.ts → shell/useWorkbenchSidebarActionBarDnd.ts} +1 -1
- package/src/workbench/{useWorkbenchSidebarViewPlacementDropZone.ts → shell/useWorkbenchSidebarViewPlacementDropZone.ts} +2 -2
- package/src/workbench/{workbenchSidebarLayoutContext.ts → shell/workbenchSidebarLayoutContext.ts} +0 -6
- package/src/workbench/{workbenchViewRouteState.ts → shell/workbenchViewRouteState.ts} +4 -4
- package/src/workbench/{WorkbenchThemeProvider.tsx → theme/WorkbenchThemeProvider.tsx} +4 -4
- package/src/workbench/{theme.ts → theme/theme.ts} +1 -1
- package/src/workbench/{themePresets.ts → theme/themePresets.ts} +3 -2
- package/src/workbench/views/command-list-density.css +36 -0
- package/src/workbench/views/command-list.css +2 -0
- package/src/workbench/views/extensions-sidebar.css +2 -0
- package/src/workbench/views/view-hosts.css +3 -3
- package/src/workbench/workspace/WorkspaceEditorPanel.tsx +1 -1
- package/src/workbench/workspace/WorkspaceExplorer.tsx +105 -4
- package/src/workbench/workspace/createVirtualWorkspaceExplorerPort.ts +1 -14
- package/src/workbench/workspace/workspaceExplorerKeyboard.ts +109 -0
- package/src/workbench/workspace/workspaceJsonDiagnostics.ts +6 -5
- package/src/workbench-menu-surfaces.css +3 -57
- package/src/brand/TilepaperAppIcon.tsx +0 -181
- package/src/brand/index.ts +0 -2
- package/src/brand/tilepaper-app-icon-geometry.ts +0 -201
- package/src/primitives/absolute-box/index.ts +0 -1
- package/src/primitives/catalog-browse-card/index.ts +0 -1
- package/src/primitives/catalog-browse-pane/index.ts +0 -3
- package/src/primitives/catalog-filter-overlay/index.ts +0 -1
- package/src/primitives/codicon/index.ts +0 -1
- package/src/primitives/external-link-button/index.ts +0 -1
- package/src/primitives/library-detail-layout/index.ts +0 -1
- package/src/primitives/library-facet-filter-panel/index.ts +0 -1
- package/src/primitives/library-facet-filter-strip/index.ts +0 -1
- package/src/primitives/panel-loading/index.ts +0 -1
- package/src/primitives/scroll-area-infinite-load/index.ts +0 -2
- package/src/primitives/workbench-thumbnail/index.ts +0 -1
- package/src/test-utils/workbenchMonacoMock.tsx +0 -37
- package/src/workbench/jdw/jdw-editors.css +0 -4
- package/src/workbench/jdw/jdw.css +0 -2
- package/src/workbench/management/integration-command-action.ts +0 -25
- package/src/workbench/markdownRemarkPlugins.ts +0 -4
- package/src/workbench/workbench-chrome.css +0 -4
- /package/src/workbench/{WorkbenchPlatformContext.tsx → chrome/WorkbenchPlatformContext.tsx} +0 -0
- /package/src/workbench/{workbenchModalViewState.ts → chrome/workbenchModalViewState.ts} +0 -0
- /package/src/workbench/{workbenchOverlaysContext.tsx → chrome/workbenchOverlaysContext.tsx} +0 -0
- /package/src/workbench/{workbenchPlatformChrome.ts → chrome/workbenchPlatformChrome.ts} +0 -0
- /package/src/workbench/{command-execution-policy.ts → commands/command-execution-policy.ts} +0 -0
- /package/src/workbench/{keyboard.ts → commands/keyboard.ts} +0 -0
- /package/src/workbench/{workbenchContextMenu.ts → commands/workbenchContextMenu.ts} +0 -0
- /package/src/workbench/{appearanceLabels.ts → shell/appearanceLabels.ts} +0 -0
- /package/src/workbench/{hardReset.ts → shell/hardReset.ts} +0 -0
- /package/src/workbench/{shellPresets.ts → shell/shellPresets.ts} +0 -0
- /package/src/workbench/{shellState.ts → shell/shellState.ts} +0 -0
- /package/src/workbench/{sidebarViewPlacementDnd.ts → shell/sidebarViewPlacementDnd.ts} +0 -0
- /package/src/workbench/{status.ts → shell/status.ts} +0 -0
- /package/src/workbench/{useWorkbenchSidebarLayout.tsx → shell/useWorkbenchSidebarLayout.tsx} +0 -0
- /package/src/workbench/{useWorkbenchStandaloneShellStateSync.tsx → shell/useWorkbenchStandaloneShellStateSync.tsx} +0 -0
- /package/src/workbench/{workbenchStandaloneShellReactContext.tsx → shell/workbenchStandaloneShellReactContext.tsx} +0 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { useEffect, type RefObject } from 'react';
|
|
2
|
+
|
|
3
|
+
const FOCUSABLE_SELECTOR = [
|
|
4
|
+
'a[href]',
|
|
5
|
+
'button:not([disabled])',
|
|
6
|
+
'textarea:not([disabled])',
|
|
7
|
+
'input:not([disabled]):not([type="hidden"])',
|
|
8
|
+
'select:not([disabled])',
|
|
9
|
+
'[tabindex]:not([tabindex="-1"])',
|
|
10
|
+
].join(',');
|
|
11
|
+
|
|
12
|
+
function isFocusable(element: HTMLElement): boolean {
|
|
13
|
+
if (element.hasAttribute('disabled') || element.getAttribute('aria-disabled') === 'true') {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
if (element.getAttribute('aria-hidden') === 'true') {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
// Explicit tabindex="-1" removes the node from the tab order.
|
|
20
|
+
// Do not use element.tabIndex >= 0: jsdom reports -1 for native buttons.
|
|
21
|
+
if (element.getAttribute('tabindex') === '-1') {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const style = window.getComputedStyle(element);
|
|
25
|
+
return style.display !== 'none' && style.visibility !== 'hidden';
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function listFocusable(container: HTMLElement): HTMLElement[] {
|
|
29
|
+
return Array.from(container.querySelectorAll<HTMLElement>(FOCUSABLE_SELECTOR)).filter(
|
|
30
|
+
isFocusable,
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface UseModalFocusTrapOptions {
|
|
35
|
+
readonly closeOnEscape?: boolean;
|
|
36
|
+
readonly containerRef: RefObject<HTMLElement | null>;
|
|
37
|
+
/** When false, skip trap/restore (for dialogs that stay mounted while closed). Default true. */
|
|
38
|
+
readonly enabled?: boolean;
|
|
39
|
+
readonly initialFocusRef?: RefObject<HTMLElement | null>;
|
|
40
|
+
readonly onClose: () => void;
|
|
41
|
+
readonly restoreFocusOnClose?: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Trap Tab focus inside a mounted modal dialog and restore focus on unmount.
|
|
46
|
+
* Escape dismisses when `closeOnEscape` is true (default).
|
|
47
|
+
*/
|
|
48
|
+
export function useModalFocusTrap({
|
|
49
|
+
closeOnEscape = true,
|
|
50
|
+
containerRef,
|
|
51
|
+
enabled = true,
|
|
52
|
+
initialFocusRef,
|
|
53
|
+
onClose,
|
|
54
|
+
restoreFocusOnClose = true,
|
|
55
|
+
}: UseModalFocusTrapOptions): void {
|
|
56
|
+
useEffect(() => {
|
|
57
|
+
if (!enabled) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const previousFocus =
|
|
62
|
+
document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
63
|
+
|
|
64
|
+
const focusInitial = () => {
|
|
65
|
+
const container = containerRef.current;
|
|
66
|
+
if (!container) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const preferred = initialFocusRef?.current;
|
|
71
|
+
if (preferred && container.contains(preferred)) {
|
|
72
|
+
preferred.focus();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const focusables = listFocusable(container);
|
|
77
|
+
if (focusables[0]) {
|
|
78
|
+
focusables[0].focus();
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!container.hasAttribute('tabindex')) {
|
|
83
|
+
container.tabIndex = -1;
|
|
84
|
+
}
|
|
85
|
+
container.focus();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const frame = window.requestAnimationFrame(focusInitial);
|
|
89
|
+
|
|
90
|
+
const handleKeyDown = (event: KeyboardEvent) => {
|
|
91
|
+
if (event.key === 'Escape' && closeOnEscape) {
|
|
92
|
+
event.preventDefault();
|
|
93
|
+
event.stopPropagation();
|
|
94
|
+
onClose();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (event.key !== 'Tab') {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const container = containerRef.current;
|
|
103
|
+
if (!container) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const focusables = listFocusable(container);
|
|
108
|
+
if (focusables.length === 0) {
|
|
109
|
+
event.preventDefault();
|
|
110
|
+
container.focus();
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const first = focusables[0]!;
|
|
115
|
+
const last = focusables[focusables.length - 1]!;
|
|
116
|
+
const active = document.activeElement;
|
|
117
|
+
|
|
118
|
+
if (event.shiftKey) {
|
|
119
|
+
if (active === first || !container.contains(active)) {
|
|
120
|
+
event.preventDefault();
|
|
121
|
+
last.focus();
|
|
122
|
+
}
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (active === last || !container.contains(active)) {
|
|
127
|
+
event.preventDefault();
|
|
128
|
+
first.focus();
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
window.addEventListener('keydown', handleKeyDown, true);
|
|
133
|
+
|
|
134
|
+
return () => {
|
|
135
|
+
window.cancelAnimationFrame(frame);
|
|
136
|
+
window.removeEventListener('keydown', handleKeyDown, true);
|
|
137
|
+
if (restoreFocusOnClose && previousFocus?.isConnected) {
|
|
138
|
+
previousFocus.focus();
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
}, [closeOnEscape, containerRef, enabled, initialFocusRef, onClose, restoreFocusOnClose]);
|
|
142
|
+
}
|
|
@@ -9,8 +9,8 @@ import { useModalResize } from './useModalResize';
|
|
|
9
9
|
|
|
10
10
|
export interface UseModalWindowFrameOptions {
|
|
11
11
|
defaultMaximized: boolean;
|
|
12
|
-
defaultHeight?: number
|
|
13
|
-
defaultWidth?: number
|
|
12
|
+
defaultHeight?: number;
|
|
13
|
+
defaultWidth?: number;
|
|
14
14
|
minHeight: number;
|
|
15
15
|
minWidth: number;
|
|
16
16
|
}
|
|
@@ -1,30 +1,20 @@
|
|
|
1
|
-
import
|
|
1
|
+
import './context-menu.css';
|
|
2
|
+
import { useCallback, useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react';
|
|
2
3
|
import { Button } from '../primitives/button';
|
|
3
4
|
import { cxCodicon } from '../utils/codicon';
|
|
4
5
|
import { cx } from '../utils/cx';
|
|
6
|
+
import type { ContextMenuItem } from './context-menu-item';
|
|
5
7
|
import { useClampedFixedOverlayPosition } from './useClampedFixedOverlayPosition';
|
|
6
8
|
import { useFixedOverlayDismiss } from './useFixedOverlayDismiss';
|
|
7
9
|
|
|
8
|
-
export type ContextMenuItem
|
|
9
|
-
| {
|
|
10
|
-
type: 'separator';
|
|
11
|
-
id?: string | undefined;
|
|
12
|
-
}
|
|
13
|
-
| {
|
|
14
|
-
type?: 'item';
|
|
15
|
-
id?: string | undefined;
|
|
16
|
-
label: ReactNode;
|
|
17
|
-
icon?: string | undefined;
|
|
18
|
-
shortcut?: ReactNode | undefined;
|
|
19
|
-
disabled?: boolean | undefined;
|
|
20
|
-
danger?: boolean | undefined;
|
|
21
|
-
onSelect: () => void;
|
|
22
|
-
};
|
|
10
|
+
export type { ContextMenuItem } from './context-menu-item';
|
|
23
11
|
|
|
24
12
|
export interface ContextMenuProps {
|
|
25
13
|
ariaLabel?: string | undefined;
|
|
26
14
|
className?: string | undefined;
|
|
27
15
|
items: ContextMenuItem[];
|
|
16
|
+
/** Explicit focus target restored only when Escape dismisses the menu. */
|
|
17
|
+
returnFocusTarget?: HTMLElement | null | undefined;
|
|
28
18
|
x: number;
|
|
29
19
|
y: number;
|
|
30
20
|
onClose: () => void;
|
|
@@ -44,20 +34,78 @@ function menuHasShortcuts(items: ContextMenuItem[]): boolean {
|
|
|
44
34
|
);
|
|
45
35
|
}
|
|
46
36
|
|
|
37
|
+
function isEnabledMenuItem(
|
|
38
|
+
item: ContextMenuItem,
|
|
39
|
+
): item is Extract<ContextMenuItem, { onSelect: () => void }> {
|
|
40
|
+
return item.type !== 'separator' && !item.disabled;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getEnabledItemIndexes(items: readonly ContextMenuItem[]): number[] {
|
|
44
|
+
return items.reduce<number[]>((indexes, item, index) => {
|
|
45
|
+
if (isEnabledMenuItem(item)) {
|
|
46
|
+
indexes.push(index);
|
|
47
|
+
}
|
|
48
|
+
return indexes;
|
|
49
|
+
}, []);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function stepEnabledIndex(
|
|
53
|
+
enabledIndexes: readonly number[],
|
|
54
|
+
currentIndex: number,
|
|
55
|
+
direction: 1 | -1,
|
|
56
|
+
): number {
|
|
57
|
+
if (enabledIndexes.length === 0) {
|
|
58
|
+
return -1;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const position = enabledIndexes.indexOf(currentIndex);
|
|
62
|
+
if (position < 0) {
|
|
63
|
+
return direction === 1 ? enabledIndexes[0]! : enabledIndexes[enabledIndexes.length - 1]!;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const nextPosition = (position + direction + enabledIndexes.length) % enabledIndexes.length;
|
|
67
|
+
return enabledIndexes[nextPosition]!;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* WAI-ARIA menu keyboard model (no nested submenus in this slice):
|
|
72
|
+
* - ArrowUp/ArrowDown move highlight (skip disabled + separators)
|
|
73
|
+
* - Home/End jump to first/last enabled item
|
|
74
|
+
* - Enter/Space activate the highlighted item
|
|
75
|
+
* - Escape closes via {@link useFixedOverlayDismiss}
|
|
76
|
+
* - Roving tabindex: only the highlighted enabled item is tabIndex=0
|
|
77
|
+
*/
|
|
47
78
|
export function ContextMenu({
|
|
48
79
|
ariaLabel = 'Context menu',
|
|
49
80
|
className,
|
|
50
81
|
items,
|
|
82
|
+
returnFocusTarget,
|
|
51
83
|
x,
|
|
52
84
|
y,
|
|
53
85
|
onClose,
|
|
54
86
|
}: ContextMenuProps) {
|
|
55
87
|
const ref = useRef<HTMLDivElement>(null);
|
|
88
|
+
const [fallbackReturnFocusTarget] = useState<HTMLElement | null>(() => {
|
|
89
|
+
if (typeof document === 'undefined') {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
return document.activeElement instanceof HTMLElement ? document.activeElement : null;
|
|
93
|
+
});
|
|
56
94
|
const position = useClampedFixedOverlayPosition(ref, { x, y }, items.length);
|
|
57
95
|
const hasIcons = menuHasIcons(items);
|
|
58
96
|
const hasShortcuts = menuHasShortcuts(items);
|
|
97
|
+
const enabledIndexes = useMemo(() => getEnabledItemIndexes(items), [items]);
|
|
98
|
+
const [highlightedIndex, setHighlightedIndex] = useState(() => enabledIndexes[0] ?? -1);
|
|
59
99
|
|
|
60
|
-
|
|
100
|
+
const restoreFocusOnEscape = useCallback(() => {
|
|
101
|
+
const target = returnFocusTarget ?? fallbackReturnFocusTarget;
|
|
102
|
+
if (target?.isConnected) {
|
|
103
|
+
target.focus();
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
}, [fallbackReturnFocusTarget, returnFocusTarget]);
|
|
107
|
+
|
|
108
|
+
useFixedOverlayDismiss({ containerRef: ref, onClose, onEscape: restoreFocusOnEscape });
|
|
61
109
|
|
|
62
110
|
useEffect(() => {
|
|
63
111
|
const handleContextMenu = (event: MouseEvent) => {
|
|
@@ -73,8 +121,64 @@ export function ContextMenu({
|
|
|
73
121
|
}, [onClose]);
|
|
74
122
|
|
|
75
123
|
useEffect(() => {
|
|
76
|
-
|
|
77
|
-
|
|
124
|
+
setHighlightedIndex((current) =>
|
|
125
|
+
enabledIndexes.includes(current) ? current : (enabledIndexes[0] ?? -1),
|
|
126
|
+
);
|
|
127
|
+
}, [enabledIndexes]);
|
|
128
|
+
|
|
129
|
+
useEffect(() => {
|
|
130
|
+
if (highlightedIndex < 0) {
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const item = ref.current?.querySelector<HTMLButtonElement>(
|
|
135
|
+
`[data-menu-index="${highlightedIndex}"]`,
|
|
136
|
+
);
|
|
137
|
+
item?.focus();
|
|
138
|
+
}, [highlightedIndex]);
|
|
139
|
+
|
|
140
|
+
const activateIndex = (index: number) => {
|
|
141
|
+
const item = items[index];
|
|
142
|
+
if (!item || !isEnabledMenuItem(item)) {
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
item.onSelect();
|
|
146
|
+
onClose();
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const handleMenuKeyDown = (event: KeyboardEvent<HTMLDivElement>) => {
|
|
150
|
+
if (event.key === 'ArrowDown') {
|
|
151
|
+
event.preventDefault();
|
|
152
|
+
setHighlightedIndex((current) => stepEnabledIndex(enabledIndexes, current, 1));
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (event.key === 'ArrowUp') {
|
|
157
|
+
event.preventDefault();
|
|
158
|
+
setHighlightedIndex((current) => stepEnabledIndex(enabledIndexes, current, -1));
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (event.key === 'Home') {
|
|
163
|
+
event.preventDefault();
|
|
164
|
+
setHighlightedIndex(enabledIndexes[0] ?? -1);
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (event.key === 'End') {
|
|
169
|
+
event.preventDefault();
|
|
170
|
+
setHighlightedIndex(enabledIndexes[enabledIndexes.length - 1] ?? -1);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
175
|
+
if (highlightedIndex < 0) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
event.preventDefault();
|
|
179
|
+
activateIndex(highlightedIndex);
|
|
180
|
+
}
|
|
181
|
+
};
|
|
78
182
|
|
|
79
183
|
if (items.length === 0) return null;
|
|
80
184
|
|
|
@@ -91,6 +195,7 @@ export function ContextMenu({
|
|
|
91
195
|
top: position.y,
|
|
92
196
|
}}
|
|
93
197
|
onContextMenu={(event) => event.preventDefault()}
|
|
198
|
+
onKeyDown={handleMenuKeyDown}
|
|
94
199
|
>
|
|
95
200
|
{items.map((item, index) =>
|
|
96
201
|
item.type === 'separator' ? (
|
|
@@ -100,11 +205,18 @@ export function ContextMenu({
|
|
|
100
205
|
key={itemKey(item, index)}
|
|
101
206
|
className="ui-context-menu__item"
|
|
102
207
|
data-danger={item.danger ? 'true' : undefined}
|
|
208
|
+
data-highlighted={highlightedIndex === index ? 'true' : undefined}
|
|
209
|
+
data-menu-index={index}
|
|
103
210
|
disabled={item.disabled}
|
|
104
211
|
role="menuitem"
|
|
212
|
+
tabIndex={highlightedIndex === index ? 0 : -1}
|
|
105
213
|
onClick={() => {
|
|
106
|
-
|
|
107
|
-
|
|
214
|
+
activateIndex(index);
|
|
215
|
+
}}
|
|
216
|
+
onMouseEnter={() => {
|
|
217
|
+
if (!item.disabled) {
|
|
218
|
+
setHighlightedIndex(index);
|
|
219
|
+
}
|
|
108
220
|
}}
|
|
109
221
|
>
|
|
110
222
|
{hasIcons ? (
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/* Compact density overrides for ContextMenu. Keep before context-menu.css. */
|
|
2
|
+
|
|
3
|
+
.ui-context-menu__item,
|
|
4
|
+
.ui-context-menu__item.ui-button {
|
|
5
|
+
--ui-button-min-height: 28px;
|
|
6
|
+
--ui-button-height: auto;
|
|
7
|
+
--ui-button-padding: 4px 10px;
|
|
8
|
+
--ui-button-border-radius: 0;
|
|
9
|
+
border-radius: 0;
|
|
10
|
+
min-height: 28px;
|
|
11
|
+
height: auto;
|
|
12
|
+
padding: 4px 10px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.ui-context-menu__separator {
|
|
16
|
+
margin: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.ui-context-menu__shortcut {
|
|
20
|
+
font-size: var(--font-size-xs);
|
|
21
|
+
color: var(--color-text-subtle);
|
|
22
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ReactNode } from 'react';
|
|
2
|
+
|
|
3
|
+
export type ContextMenuItem =
|
|
4
|
+
| {
|
|
5
|
+
type: 'separator';
|
|
6
|
+
id?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
| {
|
|
9
|
+
type?: 'item';
|
|
10
|
+
id?: string | undefined;
|
|
11
|
+
label: ReactNode;
|
|
12
|
+
icon?: string | undefined;
|
|
13
|
+
shortcut?: ReactNode | undefined;
|
|
14
|
+
disabled?: boolean | undefined;
|
|
15
|
+
danger?: boolean | undefined;
|
|
16
|
+
onSelect: () => void;
|
|
17
|
+
};
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
@import './context-menu-density.css';
|
|
2
|
+
|
|
1
3
|
.ui-context-menu {
|
|
2
4
|
position: fixed;
|
|
3
5
|
z-index: var(--ui-context-menu-z-index, 260);
|
|
@@ -72,7 +74,8 @@
|
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
.ui-context-menu__item:hover,
|
|
75
|
-
.ui-context-menu__item:focus-visible
|
|
77
|
+
.ui-context-menu__item:focus-visible,
|
|
78
|
+
.ui-context-menu__item[data-highlighted='true'] {
|
|
76
79
|
color: var(--color-text);
|
|
77
80
|
background: var(--color-surface-hover);
|
|
78
81
|
outline: none;
|
|
@@ -92,7 +95,8 @@
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
.ui-context-menu__item[data-danger='true']:hover,
|
|
95
|
-
.ui-context-menu__item[data-danger='true']:focus-visible
|
|
98
|
+
.ui-context-menu__item[data-danger='true']:focus-visible,
|
|
99
|
+
.ui-context-menu__item[data-danger='true'][data-highlighted='true'] {
|
|
96
100
|
color: #fff;
|
|
97
101
|
background: var(--color-danger);
|
|
98
102
|
}
|
package/src/overlay/index.ts
CHANGED
|
@@ -20,12 +20,17 @@ export type {
|
|
|
20
20
|
export {
|
|
21
21
|
createPointerPassthroughController,
|
|
22
22
|
isPointerOverHitRegion,
|
|
23
|
+
resolvePointerHitTarget,
|
|
23
24
|
} from './pointerPassthroughRegion';
|
|
24
25
|
export type {
|
|
26
|
+
PointerHitTestDocument,
|
|
25
27
|
PointerOverHitRegionOptions,
|
|
26
28
|
PointerPassthroughController,
|
|
27
29
|
PointerPassthroughControllerOptions,
|
|
28
30
|
PointerPassthroughPort,
|
|
29
31
|
} from './pointerPassthroughRegion';
|
|
30
32
|
export { usePointerPassthroughRegion } from './usePointerPassthroughRegion';
|
|
31
|
-
export type {
|
|
33
|
+
export type {
|
|
34
|
+
PointerHitTargetResolver,
|
|
35
|
+
UsePointerPassthroughRegionOptions,
|
|
36
|
+
} from './usePointerPassthroughRegion';
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { clampNumber } from '../utils/clamp';
|
|
2
|
+
|
|
1
3
|
export type AnchoredOverlayPlacement = 'side' | 'below' | 'above';
|
|
2
4
|
|
|
3
5
|
export interface MeasureAnchoredOverlayPanelOptions {
|
|
@@ -26,10 +28,6 @@ const SIDE_MIN_VISIBLE_HEIGHT_PX = 240;
|
|
|
26
28
|
/** Prefer below when at least this many pixels are available under the trigger. */
|
|
27
29
|
const BELOW_MIN_SPACE_PX = 12;
|
|
28
30
|
|
|
29
|
-
function clamp(value: number, min: number, max: number): number {
|
|
30
|
-
return Math.min(Math.max(value, min), max);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
31
|
/**
|
|
34
32
|
* Measure a panel-sized overlay anchored to a trigger element.
|
|
35
33
|
* Prefers a side flyout when the trigger sits in the left portion of the
|
|
@@ -65,7 +63,7 @@ export function measureAnchoredOverlayPanel(
|
|
|
65
63
|
|
|
66
64
|
if (preferSide) {
|
|
67
65
|
const minVisibleHeight = Math.min(SIDE_MIN_VISIBLE_HEIGHT_PX, maxHeight);
|
|
68
|
-
const top =
|
|
66
|
+
const top = clampNumber(
|
|
69
67
|
triggerRect.top,
|
|
70
68
|
viewportPaddingPx,
|
|
71
69
|
viewportHeight - viewportPaddingPx - minVisibleHeight,
|
|
@@ -80,7 +78,7 @@ export function measureAnchoredOverlayPanel(
|
|
|
80
78
|
}
|
|
81
79
|
|
|
82
80
|
// Align the panel's right edge toward the trigger's right, then clamp.
|
|
83
|
-
const left =
|
|
81
|
+
const left = clampNumber(
|
|
84
82
|
triggerRect.right - width,
|
|
85
83
|
viewportPaddingPx,
|
|
86
84
|
viewportWidth - viewportPaddingPx - width,
|
|
@@ -2,6 +2,22 @@ export interface PointerPassthroughPort {
|
|
|
2
2
|
setPointerPassthrough(enabled: boolean): void | Promise<void>;
|
|
3
3
|
}
|
|
4
4
|
|
|
5
|
+
export interface PointerHitTestDocument {
|
|
6
|
+
elementFromPoint(x: number, y: number): Element | null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
/** Resolve the painted element at finite viewport coordinates. */
|
|
10
|
+
export function resolvePointerHitTarget(
|
|
11
|
+
clientX: number,
|
|
12
|
+
clientY: number,
|
|
13
|
+
documentSurface: PointerHitTestDocument = document,
|
|
14
|
+
): Element | null {
|
|
15
|
+
if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
return documentSurface.elementFromPoint(clientX, clientY);
|
|
19
|
+
}
|
|
20
|
+
|
|
5
21
|
export interface PointerOverHitRegionOptions {
|
|
6
22
|
readonly hitSelectors: readonly string[];
|
|
7
23
|
readonly controlSelectors?: readonly string[];
|
|
@@ -51,7 +67,7 @@ export interface PointerPassthroughController {
|
|
|
51
67
|
/**
|
|
52
68
|
* Pure controller for overlay click-through: when the pointer is outside
|
|
53
69
|
* host-provided selectors, ask the port to enable passthrough.
|
|
54
|
-
* Pair with platform `
|
|
70
|
+
* Pair with platform `applyWindowResidencyPolicy` on the main side.
|
|
55
71
|
*/
|
|
56
72
|
export function createPointerPassthroughController(
|
|
57
73
|
options: PointerPassthroughControllerOptions,
|
|
@@ -2,9 +2,12 @@ import { useEffect, useRef, type RefObject } from 'react';
|
|
|
2
2
|
|
|
3
3
|
import {
|
|
4
4
|
createPointerPassthroughController,
|
|
5
|
+
resolvePointerHitTarget,
|
|
5
6
|
type PointerPassthroughPort,
|
|
6
7
|
} from './pointerPassthroughRegion';
|
|
7
8
|
|
|
9
|
+
export type PointerHitTargetResolver = (clientX: number, clientY: number) => EventTarget | null;
|
|
10
|
+
|
|
8
11
|
export interface UsePointerPassthroughRegionOptions {
|
|
9
12
|
readonly enabled: boolean;
|
|
10
13
|
readonly port: PointerPassthroughPort;
|
|
@@ -14,11 +17,15 @@ export interface UsePointerPassthroughRegionOptions {
|
|
|
14
17
|
readonly controlSelectors?: readonly string[];
|
|
15
18
|
/** Limit hit-testing to a subtree; defaults to the document. */
|
|
16
19
|
readonly rootRef?: RefObject<HTMLElement | null>;
|
|
20
|
+
/** Override painted-element resolution; defaults to `document.elementFromPoint`. */
|
|
21
|
+
readonly resolveHitTarget?: PointerHitTargetResolver;
|
|
17
22
|
}
|
|
18
23
|
|
|
19
24
|
/**
|
|
20
25
|
* Toggles host pointer passthrough from renderer hit-testing.
|
|
21
|
-
* Uses `requestAnimationFrame` coalescing
|
|
26
|
+
* Uses `requestAnimationFrame` coalescing and painted-element hit-testing; no
|
|
27
|
+
* product selectors in kit. Electron can forward move events while ignoring
|
|
28
|
+
* mouse input, in which case `event.target` is not necessarily under the cursor.
|
|
22
29
|
*
|
|
23
30
|
* Host responsibilities:
|
|
24
31
|
* - Provide selector lists
|
|
@@ -26,7 +33,7 @@ export interface UsePointerPassthroughRegionOptions {
|
|
|
26
33
|
* - Pair with platform residency applicator for the main-process half
|
|
27
34
|
*/
|
|
28
35
|
export function usePointerPassthroughRegion(options: UsePointerPassthroughRegionOptions): void {
|
|
29
|
-
const { enabled, port, hitSelectors, controlSelectors, rootRef } = options;
|
|
36
|
+
const { enabled, port, hitSelectors, controlSelectors, rootRef, resolveHitTarget } = options;
|
|
30
37
|
|
|
31
38
|
const portRef = useRef(port);
|
|
32
39
|
portRef.current = port;
|
|
@@ -34,6 +41,8 @@ export function usePointerPassthroughRegion(options: UsePointerPassthroughRegion
|
|
|
34
41
|
hitSelectorsRef.current = hitSelectors;
|
|
35
42
|
const controlSelectorsRef = useRef(controlSelectors);
|
|
36
43
|
controlSelectorsRef.current = controlSelectors;
|
|
44
|
+
const resolveHitTargetRef = useRef(resolveHitTarget ?? resolvePointerHitTarget);
|
|
45
|
+
resolveHitTargetRef.current = resolveHitTarget ?? resolvePointerHitTarget;
|
|
37
46
|
|
|
38
47
|
useEffect(() => {
|
|
39
48
|
const controller = createPointerPassthroughController({
|
|
@@ -58,25 +67,45 @@ export function usePointerPassthroughRegion(options: UsePointerPassthroughRegion
|
|
|
58
67
|
}
|
|
59
68
|
|
|
60
69
|
let frame = 0;
|
|
61
|
-
let
|
|
70
|
+
let pendingX = 0;
|
|
71
|
+
let pendingY = 0;
|
|
62
72
|
|
|
63
73
|
const flush = () => {
|
|
64
74
|
frame = 0;
|
|
65
|
-
|
|
75
|
+
// Re-assert on every frame. Host state may also change outside this hook;
|
|
76
|
+
// the main-process port should make same-value updates a no-op.
|
|
77
|
+
controller.reset();
|
|
78
|
+
controller.handlePointerTarget(resolveHitTargetRef.current(pendingX, pendingY));
|
|
66
79
|
};
|
|
67
80
|
|
|
68
|
-
const onPointerMove = (event: PointerEvent) => {
|
|
69
|
-
|
|
81
|
+
const onPointerMove = (event: PointerEvent | MouseEvent) => {
|
|
82
|
+
pendingX = event.clientX;
|
|
83
|
+
pendingY = event.clientY;
|
|
70
84
|
if (frame !== 0) {
|
|
71
85
|
return;
|
|
72
86
|
}
|
|
73
87
|
frame = window.requestAnimationFrame(flush);
|
|
74
88
|
};
|
|
75
89
|
|
|
90
|
+
const onMouseLeave = () => {
|
|
91
|
+
if (frame !== 0) {
|
|
92
|
+
window.cancelAnimationFrame(frame);
|
|
93
|
+
frame = 0;
|
|
94
|
+
}
|
|
95
|
+
controller.reset();
|
|
96
|
+
controller.handlePointerTarget(null);
|
|
97
|
+
};
|
|
98
|
+
|
|
76
99
|
window.addEventListener('pointermove', onPointerMove, { passive: true });
|
|
100
|
+
// Some Electron builds forward mousemove rather than pointermove while the
|
|
101
|
+
// native window ignores input.
|
|
102
|
+
window.addEventListener('mousemove', onPointerMove, { passive: true });
|
|
103
|
+
document.documentElement.addEventListener('mouseleave', onMouseLeave);
|
|
77
104
|
|
|
78
105
|
return () => {
|
|
79
106
|
window.removeEventListener('pointermove', onPointerMove);
|
|
107
|
+
window.removeEventListener('mousemove', onPointerMove);
|
|
108
|
+
document.documentElement.removeEventListener('mouseleave', onMouseLeave);
|
|
80
109
|
if (frame !== 0) {
|
|
81
110
|
window.cancelAnimationFrame(frame);
|
|
82
111
|
}
|
|
@@ -9,12 +9,12 @@ export interface WorkbenchRect {
|
|
|
9
9
|
}
|
|
10
10
|
|
|
11
11
|
export interface AbsoluteBoxProps {
|
|
12
|
-
background?: string
|
|
12
|
+
background?: string;
|
|
13
13
|
children?: ReactNode;
|
|
14
|
-
className?: string
|
|
15
|
-
overflow?: CSSProperties['overflow']
|
|
14
|
+
className?: string;
|
|
15
|
+
overflow?: CSSProperties['overflow'];
|
|
16
16
|
rect: WorkbenchRect;
|
|
17
|
-
style?: CSSProperties
|
|
17
|
+
style?: CSSProperties;
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
export function AbsoluteBox({
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import './app-icon.css';
|
|
2
|
+
import type { ComponentPropsWithRef, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
import { cx } from '../../utils/cx';
|
|
5
|
+
|
|
6
|
+
export type AppIconSize = 'sm' | 'md' | 'lg';
|
|
7
|
+
|
|
8
|
+
type AppIconBaseProps = Omit<ComponentPropsWithRef<'span'>, 'children'> & {
|
|
9
|
+
size?: AppIconSize;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
type AppIconImageProps = {
|
|
13
|
+
/** Accessible image text. Use an empty string when the surrounding surface already names the app. */
|
|
14
|
+
alt: string;
|
|
15
|
+
children?: never;
|
|
16
|
+
src: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
type AppIconCustomProps = {
|
|
20
|
+
alt?: never;
|
|
21
|
+
children: ReactNode;
|
|
22
|
+
src?: never;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type AppIconProps = AppIconBaseProps & (AppIconImageProps | AppIconCustomProps);
|
|
26
|
+
|
|
27
|
+
export function AppIcon({ alt, children, className, size = 'md', src, ...props }: AppIconProps) {
|
|
28
|
+
return (
|
|
29
|
+
<span className={cx('ui-app-icon', className)} data-size={size} {...props}>
|
|
30
|
+
{src === undefined ? children : <img alt={alt} className="ui-app-icon__image" src={src} />}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|