@greypan/web-ui 8.1.0 → 9.1.0

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 (163) hide show
  1. package/README.CN.md +309 -135
  2. package/README.md +311 -135
  3. package/dist/assets/glass.js +1 -1
  4. package/dist/assets/glass.js.map +1 -1
  5. package/dist/assets/menu-portal.js +1 -1
  6. package/dist/assets/menu-portal.js.map +1 -1
  7. package/dist/assets/modal-surface.js +6 -0
  8. package/dist/assets/modal-surface.js.map +1 -0
  9. package/dist/assets/selection-control.js +1 -1
  10. package/dist/assets/selection-control.js.map +1 -1
  11. package/dist/assets/toast-containers.js +1 -1
  12. package/dist/assets/toast-containers.js.map +1 -1
  13. package/dist/components/autocomplete/index.d.ts +3 -2
  14. package/dist/components/autocomplete/index.js +40 -44
  15. package/dist/components/autocomplete/index.js.map +1 -1
  16. package/dist/components/autocomplete/style.js +1 -1
  17. package/dist/components/autocomplete/style.js.map +1 -1
  18. package/dist/components/avatar/index.js.map +1 -1
  19. package/dist/components/avatar/style.js +1 -1
  20. package/dist/components/avatar/style.js.map +1 -1
  21. package/dist/components/back-top/style.js +1 -1
  22. package/dist/components/back-top/style.js.map +1 -1
  23. package/dist/components/badge/style.js +1 -1
  24. package/dist/components/badge/style.js.map +1 -1
  25. package/dist/components/button/style.js +1 -1
  26. package/dist/components/button/style.js.map +1 -1
  27. package/dist/components/button-group/index.js +0 -1
  28. package/dist/components/button-group/index.js.map +1 -1
  29. package/dist/components/button-group/style.js +1 -1
  30. package/dist/components/button-group/style.js.map +1 -1
  31. package/dist/components/checkbox/index.js +4 -4
  32. package/dist/components/checkbox/index.js.map +1 -1
  33. package/dist/components/checkbox-group/style.js +1 -1
  34. package/dist/components/checkbox-group/style.js.map +1 -1
  35. package/dist/components/context-menu/index.d.ts +182 -3
  36. package/dist/components/context-menu/index.js +166 -91
  37. package/dist/components/context-menu/index.js.map +1 -1
  38. package/dist/components/context-menu/scrim.js +6 -0
  39. package/dist/components/context-menu/scrim.js.map +1 -0
  40. package/dist/components/context-menu/style.js +1 -1
  41. package/dist/components/context-menu/style.js.map +1 -1
  42. package/dist/components/dialog/index.d.ts +9 -0
  43. package/dist/components/dialog/index.js +68 -46
  44. package/dist/components/dialog/index.js.map +1 -1
  45. package/dist/components/dialog/style.js +1 -1
  46. package/dist/components/dialog/style.js.map +1 -1
  47. package/dist/components/drawer/index.d.ts +45 -0
  48. package/dist/components/drawer/index.js +122 -53
  49. package/dist/components/drawer/index.js.map +1 -1
  50. package/dist/components/drawer/style.js +1 -1
  51. package/dist/components/drawer/style.js.map +1 -1
  52. package/dist/components/dropdown/index.d.ts +12 -0
  53. package/dist/components/dropdown/index.js +13 -10
  54. package/dist/components/dropdown/index.js.map +1 -1
  55. package/dist/components/dropdown-divider/style.js +1 -1
  56. package/dist/components/dropdown-divider/style.js.map +1 -1
  57. package/dist/components/dropdown-header/style.js +1 -1
  58. package/dist/components/dropdown-header/style.js.map +1 -1
  59. package/dist/components/dropdown-item/style.js +1 -1
  60. package/dist/components/dropdown-item/style.js.map +1 -1
  61. package/dist/components/editable-text/index.d.ts +39 -0
  62. package/dist/components/editable-text/index.js +6 -2
  63. package/dist/components/editable-text/index.js.map +1 -1
  64. package/dist/components/empty/index.d.ts +21 -4
  65. package/dist/components/empty/index.js +37 -32
  66. package/dist/components/empty/index.js.map +1 -1
  67. package/dist/components/empty/style.js +1 -1
  68. package/dist/components/empty/style.js.map +1 -1
  69. package/dist/components/image-preview/index.js +75 -63
  70. package/dist/components/image-preview/index.js.map +1 -1
  71. package/dist/components/image-preview/style.js +1 -1
  72. package/dist/components/image-preview/style.js.map +1 -1
  73. package/dist/components/index.d.ts +1 -0
  74. package/dist/components/index.js +38 -37
  75. package/dist/components/input/style.js +1 -1
  76. package/dist/components/input/style.js.map +1 -1
  77. package/dist/components/input-number/style.js +1 -1
  78. package/dist/components/input-number/style.js.map +1 -1
  79. package/dist/components/layout/index.d.ts +21 -6
  80. package/dist/components/layout/index.js +47 -27
  81. package/dist/components/layout/index.js.map +1 -1
  82. package/dist/components/layout/style.js +1 -1
  83. package/dist/components/layout/style.js.map +1 -1
  84. package/dist/components/middle-ellipsis/index.d.ts +100 -0
  85. package/dist/components/middle-ellipsis/index.js +112 -0
  86. package/dist/components/middle-ellipsis/index.js.map +1 -0
  87. package/dist/components/middle-ellipsis/style.js +6 -0
  88. package/dist/components/middle-ellipsis/style.js.map +1 -0
  89. package/dist/components/middle-ellipsis/truncate.d.ts +62 -0
  90. package/dist/components/middle-ellipsis/truncate.js +45 -0
  91. package/dist/components/middle-ellipsis/truncate.js.map +1 -0
  92. package/dist/components/option/style.js +1 -1
  93. package/dist/components/option/style.js.map +1 -1
  94. package/dist/components/popover/index.js.map +1 -1
  95. package/dist/components/popover/style.js +1 -1
  96. package/dist/components/popover/style.js.map +1 -1
  97. package/dist/components/radio/style.js +1 -1
  98. package/dist/components/radio/style.js.map +1 -1
  99. package/dist/components/radio-group/style.js +1 -1
  100. package/dist/components/radio-group/style.js.map +1 -1
  101. package/dist/components/segmented/index.d.ts +5 -0
  102. package/dist/components/segmented/index.js +45 -28
  103. package/dist/components/segmented/index.js.map +1 -1
  104. package/dist/components/segmented/style.js +1 -1
  105. package/dist/components/segmented/style.js.map +1 -1
  106. package/dist/components/segmented-trigger/style.js +1 -1
  107. package/dist/components/segmented-trigger/style.js.map +1 -1
  108. package/dist/components/select/index.js +0 -1
  109. package/dist/components/select/index.js.map +1 -1
  110. package/dist/components/select/style.js +1 -1
  111. package/dist/components/select/style.js.map +1 -1
  112. package/dist/components/slider/style.js +1 -1
  113. package/dist/components/slider/style.js.map +1 -1
  114. package/dist/components/spinner/style.js +1 -1
  115. package/dist/components/spinner/style.js.map +1 -1
  116. package/dist/components/svg-draw-lines/index.js.map +1 -1
  117. package/dist/components/switch/style.js +1 -1
  118. package/dist/components/switch/style.js.map +1 -1
  119. package/dist/components/textarea/index.js +2 -2
  120. package/dist/components/textarea/index.js.map +1 -1
  121. package/dist/components/textarea/style.js +1 -1
  122. package/dist/components/textarea/style.js.map +1 -1
  123. package/dist/components/theme/style.js +1 -1
  124. package/dist/components/theme/style.js.map +1 -1
  125. package/dist/components/toast/style.js +1 -1
  126. package/dist/components/toast/style.js.map +1 -1
  127. package/dist/components/tooltip/index.js.map +1 -1
  128. package/dist/components/tooltip/style.js +1 -1
  129. package/dist/components/tooltip/style.js.map +1 -1
  130. package/dist/icons/generated/akar-icons-circle-check.d.ts +6 -0
  131. package/dist/icons/generated/akar-icons-circle-check.js +10 -0
  132. package/dist/icons/generated/akar-icons-circle-check.js.map +1 -0
  133. package/dist/icons/generated/bi-check-lg.d.ts +6 -0
  134. package/dist/icons/generated/bi-check-lg.js +10 -0
  135. package/dist/icons/generated/bi-check-lg.js.map +1 -0
  136. package/dist/icons/generated/fluent-tag-search-24-regular.d.ts +6 -0
  137. package/dist/icons/generated/fluent-tag-search-24-regular.js +10 -0
  138. package/dist/icons/generated/fluent-tag-search-24-regular.js.map +1 -0
  139. package/dist/icons/generated/mdi-tag-search-outline.d.ts +6 -0
  140. package/dist/icons/generated/mdi-tag-search-outline.js +10 -0
  141. package/dist/icons/generated/mdi-tag-search-outline.js.map +1 -0
  142. package/dist/icons/index.d.ts +4 -0
  143. package/dist/icons/index.js +5 -1
  144. package/dist/scrollbars/index.d.ts +33 -0
  145. package/dist/scrollbars/index.js +18 -0
  146. package/dist/scrollbars/index.js.map +1 -0
  147. package/dist/scrollbars/style.css +10 -0
  148. package/dist/shared/element-height/index.js +47 -0
  149. package/dist/shared/element-height/index.js.map +1 -0
  150. package/dist/shared/element-width/index.js +37 -0
  151. package/dist/shared/element-width/index.js.map +1 -0
  152. package/dist/shared/menu-portal/menu-portal.js +11 -11
  153. package/dist/shared/menu-portal/menu-portal.js.map +1 -1
  154. package/dist/shared/menu-portal/menu-tree.js +2 -2
  155. package/dist/shared/menu-portal/menu-tree.js.map +1 -1
  156. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  157. package/dist/shared/overlay/native-dialog-presence.js +23 -11
  158. package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
  159. package/dist/shared/overlay/nested-drawer-layers.js +30 -23
  160. package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
  161. package/dist/shared/scroll-lock/scroll-lock.js +3 -3
  162. package/dist/shared/scroll-lock/scroll-lock.js.map +1 -1
  163. package/package.json +19 -5
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/context-menu/index.ts"],"sourcesContent":["import { computePosition, shift } from '@floating-ui/dom'\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport '@/components/dropdown-divider'\nimport '@/components/dropdown-header'\nimport '@/components/dropdown-item'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport {\n createClosingSubmenuStack,\n createMenuHoverBinder,\n createMenuOutsideClickGuard,\n getEnabledMenuLevelItems,\n getFocusedMenuItemFromPanels,\n handleMenuKeyboard,\n type MenuHoverDelegate,\n type MenuKeyboardDelegate\n} from '@/shared/menu-behavior'\nimport { createMenuPortalOverlay, type MenuPortalOverlay } from '@/shared/menu-portal/menu-portal'\nimport {\n captureFrameworkAnchors,\n focusMenuItem,\n getMenuChildren,\n getMenuItemFromEvent,\n hideNestedMenuChildren,\n moveMenuChildren,\n orderManagedMenuItems,\n reconcileManagedMenuItems,\n restoreFrameworkAnchors,\n returnManagedMenuItemsToSlot\n} from '@/shared/menu-portal/menu-tree'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineOverlayPositioningGeneration } from '@/shared/overlay/positioning-generation'\nimport { hideOverlayPresence, showOverlayPresence } from '@/shared/overlay/presence'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\nconst MARKER_TEXT = 'wui-context-menu-item'\n\n@customElement('web-ui-context-menu')\nexport class WebUiContextMenu extends LitElement {\n static override styles = unsafeCSS(style)\n\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n\n @state() private _isOpen = false\n @state() private _x = 0\n @state() private _y = 0\n\n private _activeSubmenus: MenuPortalOverlay[] = []\n private _activeSubmenuItems: HTMLElement[] = []\n // 子菜单 dialog 定位的唯一写者代数(按 panel 键控):同帧关闭→重开复用同一 panel\n // 时只允许最新一次定位写入;不同层级子菜单各有 panel,互不作废。\n private readonly _submenuPositionEpochs = new WeakMap<HTMLElement, number>()\n private _menu?: MenuPortalOverlay\n /** 根面板的开启会话句柄;未开启时为 null。查询走它。 */\n private _menuHandle: OpenOverlayHandle | null = null\n /**\n * 子菜单 panel → 会话句柄。子层经 `adopt` 进根句柄子树(默认不成为独立候选),\n * 但仍各自持有句柄:整层关闭或收尾动画结束时按 panel 精确撤销。\n */\n private readonly _submenuHandles = new Map<HTMLElement, OpenOverlayHandle>()\n // 行为层(hover / outside-click / 键盘 / submenu 收尾)由 shared/menu-behavior 驱动\n private readonly _outsideClickGuard = createMenuOutsideClickGuard(\n this,\n // 根句柄的 contains 递归覆盖全部已 adopt 的子层,一次查询替代逐 panel 判定。\n node => this._menuHandle?.contains(node) ?? false\n )\n private readonly _closingSubmenus = createClosingSubmenuStack<MenuPortalOverlay>({\n getPanel: container => container.panel,\n restoreItems: (container, parentItem) => this._restoreSubmenuItems(container, parentItem),\n dispose: container => {\n this._releaseSubmenu(container.panel)\n container.panel.remove()\n }\n })\n private readonly _hoverDelegate: MenuHoverDelegate = {\n getOpenDepth: () => this._activeSubmenus.length,\n getLevelItems: level => this._getLevelItems(level),\n getActiveItem: level => this._activeSubmenuItems[level],\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenu: item => this._openSubmenu(item),\n closeFrom: level => {\n this._closeSubmenusFrom(level)\n this._hoverBinder.bind()\n }\n }\n private readonly _hoverBinder = createMenuHoverBinder(this._hoverDelegate, 'web-ui-dropdown-item')\n private readonly _keyboardDelegate: MenuKeyboardDelegate = {\n getFocusedItem: event =>\n getFocusedMenuItemFromPanels(\n event,\n [this._menu?.content, ...this._activeSubmenus.map(submenu => submenu.content)],\n 'web-ui-dropdown-item'\n ),\n getLevelOf: item => {\n const level = this._getItemLevel(item)\n return level === -1 ? undefined : level\n },\n getEnabledItems: level => getEnabledMenuLevelItems(this._getLevelContainer(level)?.content),\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenuInstant: item => this._openSubmenu(item, true),\n closeToParent: level => {\n const parent = this._activeSubmenuItems[level - 1]\n this._closeSubmenusFrom(level - 1)\n this._hoverBinder.bind()\n return parent\n },\n closeDeepestOrAll: () => this._closeLastSubmenuOrMenu()\n }\n /*\n * 开启态浮层(issue #120 Block 1)。根面板是登记对象:子菜单经 adopt 成为它的逻辑\n * 后代;关闭动作仍走 closeDeepestOrAll(最深子菜单优先)。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._keyboardDelegate.closeDeepestOrAll(),\n isConnected: () => this.isConnected\n })\n private readonly _menuItemAnchors = new Map<HTMLElement, Comment>()\n private readonly _scrollLock = defineScrollLockLease().make()\n private readonly _userOpenChange = new UserChangeController()\n private _restoreFocusTarget?: HTMLElement\n private _shouldOpenInstantly = true\n private _refreshScheduled = false\n // 主菜单 dialog 路径与子菜单一样使用代数令牌;快速 openAt/refresh 或 close 后,\n // 迟到的 positioning promise 只能被丢弃,不能覆盖最新坐标。\n private readonly _menuPositionGeneration = defineOverlayPositioningGeneration().make()\n // 菜单打开期间宿主可能不经重定位直接改写子内容(网络推送、定时器等),\n // 新节点缺隐藏 slot 会可见叠加到菜单上;观察 portal 内容并在下一帧重新同步。\n private readonly _contentObserver = new MutationObserver(() => {\n if (this._isOpen) this._scheduleRefresh()\n })\n\n get isOpen(): boolean {\n return this._isOpen\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._hideMenuItems()\n this.addEventListener('contextmenu', this._onContextMenu)\n this.addEventListener('keydown', this._onKeydown)\n this.addEventListener('click', this._onMenuClick)\n document.addEventListener('click', this._onClickOutside)\n document.addEventListener('contextmenu', this._onContextMenuOutside)\n document.addEventListener('wheel', this._onWheel, { capture: true, passive: false })\n document.addEventListener('touchmove', this._onTouchMove, { capture: true, passive: false })\n document.addEventListener('keydown', this._onDocumentKeydown)\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this.removeEventListener('contextmenu', this._onContextMenu)\n this.removeEventListener('keydown', this._onKeydown)\n this.removeEventListener('click', this._onMenuClick)\n document.removeEventListener('click', this._onClickOutside)\n document.removeEventListener('contextmenu', this._onContextMenuOutside)\n document.removeEventListener('wheel', this._onWheel, true)\n document.removeEventListener('touchmove', this._onTouchMove, true)\n document.removeEventListener('keydown', this._onDocumentKeydown)\n this._contentObserver.disconnect()\n this._menuPositionGeneration.invalidate()\n this._outsideClickGuard.dispose()\n this._hoverBinder.dispose()\n this._scrollLock.release()\n this._menuHandle?.release()\n this._menuHandle = null\n this._returnItemsToSlot()\n this._menu?.panel.remove()\n this._menu = undefined\n this._closeSubmenusFrom(0, true)\n this._closingSubmenus.restoreAll()\n // 脱离文档即视为关闭:否则重连后 _isOpen 仍为 true 而 _menu 已清空,\n // 下次 openAt 会走已打开分支静默失败,菜单无法再打开。\n this._isOpen = false\n this._restoreFocusTarget = undefined\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // Lit 的已排队更新可在卸载后执行,不能让已失效实例重新获取全局滚动锁。\n if (!this.isConnected) {\n this._syncScrollLock(false)\n return\n }\n\n if (changed.has('_isOpen')) {\n if (this._isOpen) {\n this._syncScrollLock()\n if (!this._menu) {\n this._menu = createMenuPortalOverlay('context-menu', this)\n this._menu.panel.setAttribute('role', 'menu')\n this._menu.panel.setAttribute('aria-label', '上下文菜单')\n this._menu.panel.addEventListener('click', this._onMenuClick)\n }\n /*\n * 登记与面板建立解耦。退场动画被打断时(`_closeMenuAfterPresence` 在\n * `hideOverlayPresence` 返回 false 后提前退出)`_menu` 还在,而关闭分支已经把句柄\n * 撤了;此时重开必须补 claim。否则菜单开着且可见却没有登记:Escape 关不掉它\n * (仲裁看不到它),而 `_menuHandle === null` 会让每次 document click——包括面板\n * 内部与子菜单父项——都被 `_outsideClickGuard` 判成外部点击而关闭菜单。\n */\n if (!this._menuHandle) {\n this._menuHandle = this._overlay.claim(this._menu.panel)\n // 重新 claim = 新会话:根层的子层随之作废,仍在场的子菜单面板要重新 adopt 回\n // 新句柄的子树,否则根句柄的 contains 看不到它们。\n for (const submenu of this._activeSubmenus) {\n this._submenuHandles.delete(submenu.panel)\n this._submenuHandles.set(submenu.panel, this._menuHandle.adopt(submenu.panel))\n }\n // 收尾栈里的子菜单面板同样「可见但已脱离快照」:`_closeSubmenusFrom` 只把它们\n // 移出 `_activeSubmenus`,面板仍在 DOM 里退场。不补 adopt 的话它们会被判成\n // 面板外,点它内部就关掉整张菜单(base 的登记树直到 dispose 才注销,故属回归)。\n // 写进 `_submenuHandles` 是因为收尾结束时 `_releaseSubmenu` 按 panel 取句柄释放。\n for (const container of this._closingSubmenus.closing()) {\n this._submenuHandles.set(container.panel, this._menuHandle.adopt(container.panel))\n }\n }\n // 父项始终留在 menu.content 内,观察它即可覆盖各级子菜单在打开期间的内容重建。\n this._contentObserver.observe(this._menu.content, { childList: true, subtree: true })\n requestAnimationFrame(() => {\n if (!this._isOpen || !this._menu) return\n this._refreshMenu()\n showOverlayPresence(this._menu.panel, { isInstant: this._shouldOpenInstantly })\n this._shouldOpenInstantly = true\n this._focusFirstItem()\n })\n } else {\n this._syncScrollLock(false)\n // release ⟺ 关闭:登记与仲裁归属同时撤销,退场动画只是视觉收尾。\n this._menuHandle?.release()\n this._menuHandle = null\n this._contentObserver.disconnect()\n this._menuPositionGeneration.invalidate()\n this._refreshScheduled = false\n void this._closeMenuAfterPresence()\n }\n if (this._userOpenChange.consume()) this._dispatchChange(this._isOpen)\n }\n if (changed.has('noScrollLock')) this._syncScrollLock()\n }\n\n /**\n * 在指定视口坐标打开菜单。\n * @param x 水平坐标(px)。\n * @param y 垂直坐标(px)。\n * @returns 无返回值;命令式打开不派发 `open-change` 事件。\n */\n openAt(x: number, y: number) {\n this._openAt(x, y, true)\n }\n\n private _openAt(x: number, y: number, isInstant: boolean): boolean {\n if (this.disabled) return false\n this._x = x\n this._y = y\n this._shouldOpenInstantly = isInstant\n this._outsideClickGuard.arm()\n if (this._isOpen) {\n this._closeSubmenusFrom(0, true)\n this._closingSubmenus.restoreAll()\n // 先失效尚未完成的旧定位,再调度下一帧新代;避免新请求前的旧 promise 胜出。\n this._menuPositionGeneration.invalidate()\n this._scheduleRefresh()\n return false\n }\n this._restoreFocusTarget ??= document.activeElement instanceof HTMLElement ? document.activeElement : undefined\n this._isOpen = true\n return true\n }\n\n /**\n * 关闭菜单。\n * @returns 无返回值;命令式关闭不派发 `open-change` 事件。\n */\n close() {\n if (!this._isOpen) return\n this._isOpen = false\n this._menuPositionGeneration.invalidate()\n }\n\n private readonly _closeFromUser = () => {\n if (!this._isOpen) return\n this._userOpenChange.mark()\n this.close()\n }\n\n // 同一帧内多次内容变化只刷新一次,避免观察者与刷新自身 append 形成循环。\n private _scheduleRefresh() {\n if (this._refreshScheduled || !this._isOpen) return\n this._refreshScheduled = true\n requestAnimationFrame(() => {\n this._refreshScheduled = false\n if (!this._isOpen || !this._menu) return\n this._refreshMenu()\n })\n }\n\n // 重新同步 portal 内容并重定位;fresh-open、重定位与观察者触发的刷新共用。\n private _refreshMenu() {\n if (!this._menu) return\n this._setupMenuItems()\n this._positionMenu()\n this._hoverBinder.bind()\n }\n\n private _positionMenu() {\n const panel = this._menu?.panel\n if (!panel) return\n\n panel.style.visibility = 'hidden'\n panel.style.display = ''\n\n // 普通 overlay root 不经过 transformed containing block,保留轻量的同步定位。\n // 只有 panel 已进入 open native dialog 时才需要 Floating UI 解析坐标。\n if (!(panel.parentElement instanceof HTMLDialogElement && panel.parentElement.open)) {\n this._positionMenuInViewport(panel)\n return\n }\n\n const generation = this._menuPositionGeneration.next()\n void computePosition({ getBoundingClientRect: () => new DOMRect(this._x, this._y, 0, 0) }, panel, {\n strategy: 'fixed',\n placement: 'bottom-start',\n // crossAxis 必须显式开启:bottom-start 的 sideAxis 为 y,默认只钳制 x,\n // 视口下缘打开时菜单底部会溢出且无法滚动进入视野。\n middleware: [shift({ padding: 8, crossAxis: true })]\n }).then(({ x, y, middlewareData }) => {\n if (!this._isOpen || this._menu?.panel !== panel || !this._menuPositionGeneration.isCurrent(generation)) return\n // dialog 相对坐标不能与 viewport 的 _x/_y 比较推导 origin(原点非零时几乎恒判\n // right/bottom);shift 数据是该坐标系内的钳制位移增量,负值即被推向该轴起点侧。\n const shiftX = middlewareData.shift?.x ?? 0\n const shiftY = middlewareData.shift?.y ?? 0\n this._applyMenuPosition(panel, x, y, shiftX < 0 ? 'right' : 'left', shiftY < 0 ? 'bottom' : 'top')\n })\n }\n\n private _positionMenuInViewport(panel: HTMLElement) {\n const { width, height } = panel.getBoundingClientRect()\n const vw = window.innerWidth\n const vh = window.innerHeight\n\n let x = this._x\n let y = this._y\n\n if (x + width > vw) x = vw - width - 8\n if (y + height > vh) y = vh - height - 8\n if (x < 0) x = 8\n if (y < 0) y = 8\n\n this._applyMenuPosition(panel, x, y)\n }\n\n private _applyMenuPosition(\n panel: HTMLElement,\n x: number,\n y: number,\n horizontalOrigin?: 'left' | 'right',\n verticalOrigin?: 'top' | 'bottom'\n ) {\n panel.style.left = `${x}px`\n panel.style.top = `${y}px`\n // 非 dialog 路径保持 viewport 坐标比较;dialog 路径由调用方传入 shift 推导值。\n const horizontal = horizontalOrigin ?? (x < this._x ? 'right' : 'left')\n const vertical = verticalOrigin ?? (y < this._y ? 'bottom' : 'top')\n panel.style.setProperty('--wui-internal-overlay-transform-origin', `${vertical} ${horizontal}`)\n panel.style.visibility = ''\n }\n\n private _focusFirstItem() {\n const items = this._menu?.content.querySelectorAll<HTMLElement>('web-ui-dropdown-item:not([disabled])')\n const firstItem = items?.[0]\n if (firstItem) {\n focusMenuItem(firstItem)\n }\n }\n\n private _setupMenuItems() {\n const menu = this._menu\n if (!menu) return\n const content = menu.content\n\n reconcileManagedMenuItems(this, content, this._menuItemAnchors, MARKER_TEXT)\n this._hideMenuItems()\n // 宿主可能在菜单打开期间动态改写 slot 子内容(如切换上下文后重建嵌套子项),\n // 新节点没有隐藏 slot 会落入父项默认 slot 可见渲染并叠到一级菜单上;\n // 因此对已移入 content 的嵌套子项也要重新隐藏。\n hideNestedMenuChildren(content, 'context-menu-hidden')\n\n // 框架 v-if 注释锚点的复位独立于元素重排:无论元素序是否已变都要执行。\n // 若挂在「元素序已变才重排」之下,顺序恰好正确时会跳过复位,锚点漂移无法收敛,\n // 而 Vue 下次翻转正依赖锚点引导新分支项的插入点。\n const anchors = captureFrameworkAnchors(content, MARKER_TEXT)\n orderManagedMenuItems(this, content, this._menuItemAnchors, MARKER_TEXT)\n restoreFrameworkAnchors(content, anchors)\n }\n\n private _returnItemsToSlot() {\n const menu = this._menu\n if (!menu) return\n\n this._closeSubmenusFrom(0, true)\n this._closingSubmenus.restoreAll()\n\n returnManagedMenuItemsToSlot(this, menu.content, this._menuItemAnchors)\n }\n\n private async _closeMenuAfterPresence() {\n const menu = this._menu\n if (menu && !(await hideOverlayPresence(menu.panel))) return\n if (this._isOpen || !this.isConnected || this._menu !== menu) return\n\n // 登记已在关闭分支撤销(release ⟺ 关闭),这里只做内容归还与 DOM 收尾。\n this._returnItemsToSlot()\n menu?.panel.remove()\n this._menu = undefined\n this._restoreFocusTarget?.focus()\n this._restoreFocusTarget = undefined\n }\n\n private _hideMenuItems() {\n getMenuChildren(this).forEach(child => child.setAttribute('slot', 'context-menu-hidden'))\n hideNestedMenuChildren(this, 'context-menu-hidden')\n }\n\n private _openSubmenu(item: HTMLElement, isInstant = false) {\n if (!this._isOpen || !item.hasAttribute('submenu') || item.hasAttribute('disabled')) return\n\n const level = this._getItemLevel(item)\n if (level === -1 || this._activeSubmenuItems[level] === item) return\n\n this._closeSubmenusFrom(level)\n const closingSubmenu = this._closingSubmenus.take(item)\n if (closingSubmenu) {\n this._activeSubmenus[level] = closingSubmenu\n this._activeSubmenuItems[level] = item\n /*\n * 取回的面板写回 `_submenuHandles`,让收尾结束时的 `_releaseSubmenu` 释放到正确的句柄。\n * 注:「面板在根句柄子树里」这一条已由上面的 claim 分支保证 —— 该分支会把仍在\n * closing 栈里的面板一并重挂(`_closingSubmenus.closing()`)。实测单独摘掉这一行\n * 全部用例仍绿,所以它维护的是释放账本,不是树的成员关系。\n */\n if (this._menuHandle) this._submenuHandles.set(closingSubmenu.panel, this._menuHandle.adopt(closingSubmenu.panel))\n item.setAttribute('active', '')\n this._positionSubmenu(item, closingSubmenu)\n showOverlayPresence(closingSubmenu.panel, { isInstant })\n this._hoverBinder.bind()\n return\n }\n if (getMenuChildren(item).length === 0) return\n\n const submenu = createMenuPortalOverlay('context-submenu', this)\n submenu.panel.dataset.level = String(level)\n submenu.panel.setAttribute('role', 'menu')\n submenu.panel.setAttribute('aria-label', '子菜单')\n submenu.panel.style.visibility = 'hidden'\n submenu.panel.addEventListener('click', this._onMenuClick)\n moveMenuChildren(item, submenu.content)\n\n this._activeSubmenus[level] = submenu\n this._activeSubmenuItems[level] = item\n /*\n * 子层进根句柄的子树,默认不是独立候选 —— 「它在树里、但不参与这一层裁决」由此\n * 表达。原先按 level===0 选择根面板还是上一层子面板的父级判断随之消失:contains\n * 沿子树递归,拍平与嵌套对任何现有查询结果等价。\n */\n if (this._menuHandle) this._submenuHandles.set(submenu.panel, this._menuHandle.adopt(submenu.panel))\n item.setAttribute('active', '')\n this._positionSubmenu(item, submenu)\n showOverlayPresence(submenu.panel, { isInstant })\n this._hoverBinder.bind()\n }\n\n private _closeSubmenusFrom(level: number, isInstant = false) {\n for (let index = this._activeSubmenus.length - 1; index >= level; index--) {\n const submenu = this._activeSubmenus[index]\n const item = this._activeSubmenuItems[index]\n item?.removeAttribute('active')\n if (!item || isInstant) {\n if (submenu) this._releaseSubmenu(submenu.panel)\n this._restoreSubmenuItems(submenu, item)\n submenu.panel.remove()\n } else {\n this._closingSubmenus.closeAsync(item, submenu)\n }\n }\n this._activeSubmenus.length = level\n this._activeSubmenuItems.length = level\n }\n\n private _restoreSubmenuItems(submenu: MenuPortalOverlay, item?: HTMLElement) {\n if (!item) return\n moveMenuChildren(submenu.content, item)\n // 子菜单打开期间宿主可能重建了嵌套子项,归还时补隐藏,避免可见叠加。\n hideNestedMenuChildren(item, 'context-menu-hidden')\n }\n\n private _getItemLevel(item: HTMLElement): number {\n const menu = this._menu\n if (menu?.panel.contains(item)) return 0\n const parentLevel = this._activeSubmenus.findIndex(submenu => submenu.panel.contains(item))\n return parentLevel === -1 ? -1 : parentLevel + 1\n }\n\n private _positionSubmenu(item: HTMLElement, submenu: MenuPortalOverlay) {\n // 与主菜单同因:panel 进入 open native dialog 后处于 transformed containing\n // block,viewport 坐标的 left/top 会相对 dialog padding box 解析而整体偏移,\n // 需改走 Floating UI 换算为 dialog 相对坐标;开合方向仍按视口坐标度量预判。\n if (submenu.panel.parentElement instanceof HTMLDialogElement && submenu.panel.parentElement.open) {\n const itemRect = item.getBoundingClientRect()\n const padding = 8\n const canOpenRight = itemRect.right + submenu.panel.getBoundingClientRect().width + padding <= window.innerWidth\n // 同帧关闭→重开会复用同一 panel 产生两个 in-flight 定位 promise,完成序不保证\n // 后者胜出;按 panel 键控的代数 token 只允许最新一次调用写入,迟到旧 promise 丢弃。\n const epoch = (this._submenuPositionEpochs.get(submenu.panel) ?? 0) + 1\n this._submenuPositionEpochs.set(submenu.panel, epoch)\n void computePosition(item, submenu.panel, {\n strategy: 'fixed',\n placement: canOpenRight ? 'right-start' : 'left-start',\n middleware: [shift({ padding, crossAxis: true })]\n }).then(({ x, y, middlewareData }) => {\n if (!this._activeSubmenus.includes(submenu) || epoch !== this._submenuPositionEpochs.get(submenu.panel)) return\n // 与主菜单 dialog 路径一致:origin 由 shift 增量推导,视口预判只决定 placement。\n const shiftX = middlewareData.shift?.x ?? 0\n const shiftY = middlewareData.shift?.y ?? 0\n const horizontalOrigin = shiftX < 0 ? 'right' : 'left'\n const verticalOrigin = shiftY < 0 ? 'bottom' : 'top'\n submenu.panel.style.left = `${x}px`\n submenu.panel.style.top = `${y}px`\n submenu.panel.style.setProperty(\n '--wui-internal-overlay-transform-origin',\n `${verticalOrigin} ${horizontalOrigin}`\n )\n submenu.panel.style.visibility = ''\n })\n return\n }\n\n const itemRect = item.getBoundingClientRect()\n const submenuRect = submenu.panel.getBoundingClientRect()\n const padding = 8\n const canOpenRight = itemRect.right + submenuRect.width + padding <= window.innerWidth\n const left = canOpenRight ? itemRect.right : Math.max(padding, itemRect.left - submenuRect.width)\n const top = Math.min(Math.max(padding, itemRect.top), window.innerHeight - submenuRect.height - padding)\n\n submenu.panel.style.left = `${left}px`\n submenu.panel.style.top = `${top}px`\n submenu.panel.style.setProperty('--wui-internal-overlay-transform-origin', canOpenRight ? 'top left' : 'top right')\n submenu.panel.style.visibility = ''\n }\n\n private _dispatchChange(open: boolean) {\n dispatchOpenChangeEvent(this, open)\n }\n\n private _onContextMenu = (e: MouseEvent) => {\n if (this.disabled) return\n e.preventDefault()\n this._restoreFocusTarget = e.target instanceof HTMLElement ? e.target : undefined\n if (this._openAt(e.clientX, e.clientY, false)) this._userOpenChange.mark()\n }\n\n private _onContextMenuOutside = (e: MouseEvent) => {\n if (this._isOpen && !this._outsideClickGuard.isInside(e)) {\n this._closeFromUser()\n }\n }\n\n private _onKeydown = (e: KeyboardEvent) => {\n if (this.disabled) return\n\n if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) {\n e.preventDefault()\n const focused = document.activeElement\n if (focused && focused !== document.body) {\n const rect = focused.getBoundingClientRect()\n if (this._openAt(rect.left, rect.bottom, true)) this._userOpenChange.mark()\n } else {\n if (this._openAt(window.innerWidth / 2, window.innerHeight / 2, true)) this._userOpenChange.mark()\n }\n return\n }\n }\n\n private _onClickOutside = (e: MouseEvent) => {\n if (!this._isOpen || this._outsideClickGuard.isArmed()) return\n if (this._outsideClickGuard.isInside(e)) return\n this._closeFromUser()\n }\n\n private _onMenuClick = (e: MouseEvent) => {\n const item = getMenuItemFromEvent(e)\n if (!item || item.hasAttribute('disabled')) return\n if (item.hasAttribute('submenu')) {\n this._openSubmenu(item)\n return\n }\n this._closeFromUser()\n }\n\n private _getLevelContainer(level: number) {\n return level === 0 ? this._menu : this._activeSubmenus[level - 1]\n }\n\n private _getLevelItems(level: number): HTMLElement[] {\n const container = this._getLevelContainer(level)\n if (!container) return []\n return getMenuChildren(container.content)\n }\n\n private _onWheel = (e: WheelEvent) => {\n this._preventBackgroundScroll(e)\n }\n\n private _onTouchMove = (e: TouchEvent) => {\n this._preventBackgroundScroll(e)\n }\n\n private _onDocumentKeydown = (e: KeyboardEvent) => {\n if (!this._isOpen) return\n // 鼠标右键打开的菜单焦点不在菜单内,e.target !== this 时也必须能 Escape 关闭;\n // 菜单为模态浮层,按 Escape 即关闭,无需限定焦点位置\n handleMenuKeyboard(this._keyboardDelegate, e)\n }\n\n private _closeLastSubmenuOrMenu() {\n if (this._activeSubmenus.length > 0) {\n const level = this._activeSubmenus.length - 1\n const parent = this._activeSubmenuItems[level]\n this._closeSubmenusFrom(level)\n focusMenuItem(parent)\n this._hoverBinder.bind()\n } else {\n this._closeFromUser()\n }\n }\n\n private _preventBackgroundScroll(e: Event) {\n if (!this._isOpen || this.noScrollLock || this._isMenuPanelEvent(e)) return\n e.preventDefault()\n }\n\n private _syncScrollLock(isOpen = this._isOpen) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n /** 按 panel 撤销一层子菜单的登记;句柄已随根层递归撤销时幂等无操作。 */\n private _releaseSubmenu(panel: HTMLElement) {\n this._submenuHandles.get(panel)?.release()\n this._submenuHandles.delete(panel)\n }\n\n private _isMenuPanelEvent(e: Event): boolean {\n // 在监听器内部判定:composedPath() 在派发结束后会被清空。\n return this._menuHandle?.containsEvent(e) ?? false\n }\n\n override render() {\n return html`\n <div class=\"context-menu-anchor\">\n <slot @slotchange=${this._onSlotChange}></slot>\n </div>\n `\n }\n\n private _onSlotChange() {\n this._hideMenuItems()\n // 打开期间宿主重建顶层项时,新成员要移入 portal 才对用户可见;\n // 关闭状态下无需移动,等下次打开时由 _setupMenuItems 统一处理。\n if (this._isOpen) this._scheduleRefresh()\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-context-menu': WebUiContextMenu\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/context-menu/index.ts"],"sourcesContent":["import { computePosition, flip, shift } from '@floating-ui/dom'\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { UserChangeController } from '@/shared/events/user-change'\nimport {\n createClosingSubmenuStack,\n createMenuHoverBinder,\n createMenuOutsideClickGuard,\n getEnabledMenuLevelItems,\n getFocusedMenuItemFromPanels,\n handleMenuKeyboard,\n type MenuHoverDelegate,\n type MenuKeyboardDelegate\n} from '@/shared/menu-behavior'\nimport { createMenuPortalOverlay, type MenuPortalOverlay } from '@/shared/menu-portal/menu-portal'\nimport {\n captureFrameworkAnchors,\n findFocusedMenuItem,\n focusMenuItem,\n getMenuChildren,\n getMenuItemFromEvent,\n hideNestedMenuChildren,\n moveMenuChildren,\n orderManagedMenuItems,\n reconcileManagedMenuItems,\n restoreFrameworkAnchors,\n returnManagedMenuItemsToSlot\n} from '@/shared/menu-portal/menu-tree'\nimport { normalizeNumber } from '@/shared/normalize'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineOverlayPositioningGeneration } from '@/shared/overlay/positioning-generation'\nimport { hideOverlayPresence, showOverlayPresence } from '@/shared/overlay/presence'\nimport { findNearestTheme } from '@/shared/overlay/theme-overlay-scope'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport scrimStyle from './scrim.css?inline'\nimport style from './style.css?inline'\n\nconst MARKER_TEXT = 'wui-context-menu-item'\n\n/** 与 iOS / Android 原生长按一致的默认时长。 */\nconst DEFAULT_LONG_PRESS_DELAY = 500\n/** long-press-delay 的上界:超过 5s 的长按不再是「长按」,等同没有该属性。 */\nconst MAX_LONG_PRESS_DELAY = 5000\n/** 按住期间的容差位移;超出即视为滚动/拖拽意图,取消长按。 */\nconst LONG_PRESS_MOVE_TOLERANCE = 10\n/**\n * 长按开菜单后吸收浏览器补发事件的窗口上限。\n *\n * 这不是时序保证:实测补发几乎紧跟抬手(touchend(1019ms) → click(1022ms),间隔 3ms),\n * 1000ms 远大于实际需要,只是一个陈旧兜底上限。真正的边界是下一次 `pointerdown`\n * (`_onPointerDown` 无条件清零,含鼠标)与菜单关闭,两条硬边界都在窗口判定之外,\n * 所以窗口只负责兜住「补发事件既没等到下一次按下、菜单也没关闭」这种不该发生的路径。\n */\nconst LONG_PRESS_FOLLOW_UP_WINDOW_MS = 1000\n\n/**\n * 菜单打开期间铺在整屏上的 scrim `<dialog>`。\n *\n * 用 `<dialog>` + `showModal()` 而不是 plain `div`:模态 dialog 进 top layer,\n * 于是「z-index 谁高谁低」这道约束整体消失 —— 面板与 scrim 同在 top layer,\n * 而 top layer 之下的任何 z-index 都碰不到它们。plain `div` 也能修好 light-dismiss\n * 穿透,但那样 scrim 必须靠 z-index 压过 `--wui-layer-menu`(100),是另一套易碎的约定。\n */\nconst SCRIM_ATTR = 'data-wui-menu-scrim'\n\ntype CloseSource = 'menu-item' | 'default'\n\n/** 首项聚焦的最大尝试次数(含首次)。见 `_focusFirstItem`。 */\nconst FOCUS_FIRST_ITEM_ATTEMPTS = 5\n\n/** 面板与视口边缘的最小间距,与既有夹取算术同源。 */\nconst VIEWPORT_PADDING = 8\n\n@customElement('web-ui-context-menu')\nexport class WebUiContextMenu extends LitElement {\n static override styles = unsafeCSS(style)\n\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n /** opt-in:触屏长按打开菜单。默认关闭,鼠标路径靠原生 contextmenu,无需此属性。 */\n @property({ type: Boolean, reflect: true, attribute: 'long-press' }) longPress = false\n\n private _longPressDelay = DEFAULT_LONG_PRESS_DELAY\n\n /**\n * 走包内既有的 `normalizeNumber` 通道(与 `dropdown.offset`、`popover.offset` 同形)。\n * 不能只靠 `Math.max(0, …)` 挡负数:`Math.max(0, NaN) === NaN`,而属性写错\n * (`long-press-delay=\"abc\"`)时 Lit 的 Number converter 给的正是 NaN,\n * `setTimeout(fn, NaN)` 会立刻触发,长按就在 pointerdown 同一帧开菜单、绕过全部时长语义。\n */\n @property({ type: Number, attribute: 'long-press-delay' })\n get longPressDelay(): number {\n return this._longPressDelay\n }\n set longPressDelay(v: number) {\n const old = this._longPressDelay\n this._longPressDelay = normalizeNumber(v, 0, MAX_LONG_PRESS_DELAY, DEFAULT_LONG_PRESS_DELAY)\n this.requestUpdate('longPressDelay', old)\n }\n\n @state() private _isOpen = false\n @state() private _x = 0\n @state() private _y = 0\n\n private _activeSubmenus: MenuPortalOverlay[] = []\n private _activeSubmenuItems: HTMLElement[] = []\n private _longPressTimer: ReturnType<typeof setTimeout> | undefined\n private _longPressOrigin: { x: number; y: number } | undefined\n /**\n * 长按自行开菜单的时刻。浏览器会为**同一次触摸**补发两个事件,二者都属于\n * 那一次意图,不是两次独立操作:\n *\n * - 原生 `contextmenu`:长按被识别时立刻派发,会重复打开(`_openAt` 走已开分支)。\n * - 合成 `click`:`touchend` 之后由引擎补发,会把刚打开的菜单立刻 light-dismiss。\n * 实测真实触控管线下的顺序为 pointerdown(9ms) → 长按开菜单(≈509ms) →\n * touchend(1019ms) → click(1022ms),缺这一步菜单开了就被自己的补发事件关掉。\n *\n * 因此在窗口内吸收这两个补发事件;窗口由下一次 `pointerdown` 关闭。\n */\n private _longPressOpenedAt: number | null = null\n // 子菜单 dialog 定位的唯一写者代数(按 panel 键控):同帧关闭→重开复用同一 panel\n // 时只允许最新一次定位写入;不同层级子菜单各有 panel,互不作废。\n private readonly _submenuPositionEpochs = new WeakMap<HTMLElement, number>()\n private _menu?: MenuPortalOverlay\n /**\n * 模态 scrim。菜单打开期间存在并处于 `showModal()` 状态,关闭时随面板一起移除。\n *\n * 面板必须**显式**挂进它(`createMenuPortalOverlay` 的第三参):`resolveOverlayContainer`\n * 查的是 target 的**祖先**里有无已打开的 dialog,而 scrim 是面板的反方向,祖先发现查不到。\n */\n private _scrim?: HTMLDialogElement\n /**\n * 本次开启是否**水平居中于触发点**(触屏长按路径)。鼠标 / 键盘路径仍左上角对齐光标。\n *\n * 只切换 placement 的对齐量,不切换定位算法 —— 两条入口共用 `_positionMenu` 里的\n * 同一套 `computePosition` + `flip` / `shift`,垂直契约(上缘对齐触发点、向下展开、\n * 放不下翻到上方)完全一致。\n *\n * 触屏要居中:手指按下去的位置是「这块内容在这里」的意思,菜单在它正下方居中展开,\n * 正是 iOS / Android 原生长按菜单的形态;左对齐会让菜单整体偏到手指右侧,看起来像\n * 弹在了别的东西上面。\n *\n * 鼠标不居中:桌面右键的肌肉记忆是菜单**左上角**出现在光标处。更要紧的是居中会让光标\n * 落在面板上缘正中 —— 也就是落在第一项的命中区里。后果是菜单一开就有项处于 hover 高亮,\n * 并且紧接着再右键一次(想开子菜单)会打在菜单项上而不是空白处。`overlay-in-dialog`\n * 里 `openAt(100, 400)` 那条用例钉的 `'top left'` 正是这条契约。\n *\n * 判据是**开启来源**(调用点显式传入),不靠运行时嗅探 pointerType 或视口宽度:\n * 后两者会让同一块屏幕上的两种输入拿到两套布局,且与本组件 opt-in 的 `long-press`\n * 属性不同源。\n */\n private _centerOnPressPoint = false\n\n /**\n * 本次关闭的来源,决定焦点归还走哪条路。语义见 `close()` 里的说明。\n *\n * 只有菜单项激活这一条需要抢在退场之前出顶层;点外面 / Escape / 程序式关闭一律\n * 保留 dialog 默认效果 —— 那正是用户「回到原处」的期望。\n */\n private _closeSource: CloseSource = 'default'\n /** 根面板的开启会话句柄;未开启时为 null。查询走它。 */\n private _menuHandle: OpenOverlayHandle | null = null\n /**\n * 子菜单 panel → 会话句柄。子层经 `adopt` 进根句柄子树(默认不成为独立候选),\n * 但仍各自持有句柄:整层关闭或收尾动画结束时按 panel 精确撤销。\n */\n private readonly _submenuHandles = new Map<HTMLElement, OpenOverlayHandle>()\n // 行为层(hover / outside-click / 键盘 / submenu 收尾)由 shared/menu-behavior 驱动\n private readonly _outsideClickGuard = createMenuOutsideClickGuard(\n this,\n // 根句柄的 contains 递归覆盖全部已 adopt 的子层,一次查询替代逐 panel 判定。\n node => this._menuHandle?.contains(node) ?? false\n )\n private readonly _closingSubmenus = createClosingSubmenuStack<MenuPortalOverlay>({\n getPanel: container => container.panel,\n restoreItems: (container, parentItem) => this._restoreSubmenuItems(container, parentItem),\n dispose: container => {\n this._releaseSubmenu(container.panel)\n container.panel.remove()\n }\n })\n private readonly _hoverDelegate: MenuHoverDelegate = {\n getOpenDepth: () => this._activeSubmenus.length,\n getLevelItems: level => this._getLevelItems(level),\n getActiveItem: level => this._activeSubmenuItems[level],\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenu: item => this._openSubmenu(item),\n closeFrom: level => {\n this._closeSubmenusFrom(level)\n this._hoverBinder.bind()\n }\n }\n private readonly _hoverBinder = createMenuHoverBinder(this._hoverDelegate, 'web-ui-dropdown-item')\n private readonly _keyboardDelegate: MenuKeyboardDelegate = {\n getFocusedItem: event =>\n getFocusedMenuItemFromPanels(\n event,\n [this._menu?.content, ...this._activeSubmenus.map(submenu => submenu.content)],\n 'web-ui-dropdown-item'\n ),\n getLevelOf: item => {\n const level = this._getItemLevel(item)\n return level === -1 ? undefined : level\n },\n getEnabledItems: level => getEnabledMenuLevelItems(this._getLevelContainer(level)?.content),\n isSubmenuItem: item => item.hasAttribute('submenu'),\n openSubmenuInstant: item => this._openSubmenu(item, true),\n closeToParent: level => {\n const parent = this._activeSubmenuItems[level - 1]\n this._closeSubmenusFrom(level - 1)\n this._hoverBinder.bind()\n return parent\n },\n closeDeepestOrAll: () => this._closeLastSubmenuOrMenu()\n }\n /*\n * 开启态浮层(issue #120 Block 1)。根面板是登记对象:子菜单经 adopt 成为它的逻辑\n * 后代;关闭动作仍走 closeDeepestOrAll(最深子菜单优先)。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._keyboardDelegate.closeDeepestOrAll(),\n isConnected: () => this.isConnected\n })\n private readonly _menuItemAnchors = new Map<HTMLElement, Comment>()\n private readonly _scrollLock = defineScrollLockLease().make()\n private readonly _userOpenChange = new UserChangeController()\n private _shouldOpenInstantly = true\n private _suppressInitialFocusVisible = true\n private _refreshScheduled = false\n // 主菜单 dialog 路径与子菜单一样使用代数令牌;快速 openAt/refresh 或 close 后,\n // 迟到的 positioning promise 只能被丢弃,不能覆盖最新坐标。\n private readonly _menuPositionGeneration = defineOverlayPositioningGeneration().make()\n // 菜单打开期间宿主可能不经重定位直接改写子内容(网络推送、定时器等),\n // 新节点缺隐藏 slot 会可见叠加到菜单上;观察 portal 内容并在下一帧重新同步。\n private readonly _contentObserver = new MutationObserver(() => {\n if (this._isOpen) this._scheduleRefresh()\n })\n\n get isOpen(): boolean {\n return this._isOpen\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._hideMenuItems()\n this.addEventListener('contextmenu', this._onContextMenu)\n this.addEventListener('keydown', this._onKeydown)\n this.addEventListener('click', this._onMenuClick)\n this.addEventListener('pointerdown', this._onPointerDown)\n this.addEventListener('pointermove', this._onPointerMove)\n this.addEventListener('pointerup', this._onPointerUp)\n this.addEventListener('pointercancel', this._onPointerCancel)\n document.addEventListener('contextmenu', this._onContextMenuOutside)\n document.addEventListener('wheel', this._onWheel, { capture: true, passive: false })\n document.addEventListener('touchmove', this._onTouchMove, { capture: true, passive: false })\n document.addEventListener('keydown', this._onDocumentKeydown)\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this.removeEventListener('contextmenu', this._onContextMenu)\n this.removeEventListener('keydown', this._onKeydown)\n this.removeEventListener('click', this._onMenuClick)\n this.removeEventListener('pointerdown', this._onPointerDown)\n this.removeEventListener('pointermove', this._onPointerMove)\n this.removeEventListener('pointerup', this._onPointerUp)\n this.removeEventListener('pointercancel', this._onPointerCancel)\n document.removeEventListener('contextmenu', this._onContextMenuOutside)\n document.removeEventListener('wheel', this._onWheel, true)\n document.removeEventListener('touchmove', this._onTouchMove, true)\n document.removeEventListener('keydown', this._onDocumentKeydown)\n this._contentObserver.disconnect()\n this._menuPositionGeneration.invalidate()\n this._outsideClickGuard.dispose()\n this._hoverBinder.dispose()\n this._scrollLock.release()\n this._menuHandle?.release()\n this._menuHandle = null\n this._returnItemsToSlot()\n this._menu?.panel.remove()\n this._menu = undefined\n this._closeSubmenusFrom(0, true)\n this._closingSubmenus.restoreAll()\n /*\n * scrim 挂在宿主以外的容器里(theme overlay root,无主题时 document.body),不随宿主一起\n * 脱离文档。不在这里收掉就会留下一张仍然 `showModal()` 的全屏 dialog:top layer 不受宿主\n * 移除影响,整个页面就此锁死。\n */\n this._closeScrim()\n // 脱离文档即视为关闭:否则重连后 _isOpen 仍为 true 而 _menu 已清空,\n // 下次 openAt 会走已打开分支静默失败,菜单无法再打开。\n this._isOpen = false\n this._cancelLongPress()\n this._longPressOpenedAt = null\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // Lit 的已排队更新可在卸载后执行,不能让已失效实例重新获取全局滚动锁。\n if (!this.isConnected) {\n this._syncScrollLock(false)\n return\n }\n\n if (changed.has('_isOpen')) {\n if (this._isOpen) {\n this._syncScrollLock()\n if (!this._menu) {\n /*\n * 顺序:先建 scrim 并 `showModal()`,再把面板挂进去。\n *\n * `showModal()` 会把此前聚焦的元素记为还原目标;此刻焦点仍在 opener 上,\n * 正是归还目标。面板随后 `_focusFirstItem()` 把焦点带进菜单,关闭时 UA 还回去。\n *\n * 面板挂进 scrim 是承重的,不是「顺手放一起」:top layer 只对**自身子树内的\n * 元素**做命中拦截,面板若留在 overlay root(scrim 之外),模态期间它自己就不可点。\n */\n const scrim = this._openScrim()\n this._menu = createMenuPortalOverlay('context-menu', this, scrim)\n this._menu.panel.setAttribute('role', 'menu')\n this._menu.panel.setAttribute('aria-label', '上下文菜单')\n this._menu.panel.addEventListener('click', this._onMenuClick)\n }\n /*\n * 登记与面板建立解耦。退场动画被打断时(`_closeMenuAfterPresence` 在\n * `hideOverlayPresence` 返回 false 后提前退出)`_menu` 还在,而关闭分支已经把句柄\n * 撤了;此时重开必须补 claim。否则菜单开着且可见却没有登记:Escape 关不掉它\n * (仲裁看不到它),而 `_menuHandle === null` 会让每次 document click——包括面板\n * 内部与子菜单父项——都被 `_outsideClickGuard` 判成外部点击而关闭菜单。\n */\n if (!this._menuHandle) {\n this._menuHandle = this._overlay.claim(this._menu.panel)\n // 重新 claim = 新会话:根层的子层随之作废,仍在场的子菜单面板要重新 adopt 回\n // 新句柄的子树,否则根句柄的 contains 看不到它们。\n for (const submenu of this._activeSubmenus) {\n this._submenuHandles.delete(submenu.panel)\n this._submenuHandles.set(submenu.panel, this._menuHandle.adopt(submenu.panel))\n }\n // 收尾栈里的子菜单面板同样「可见但已脱离快照」:`_closeSubmenusFrom` 只把它们\n // 移出 `_activeSubmenus`,面板仍在 DOM 里退场。**这个循环承重**:不补 adopt,\n // 它们就会落在句柄的登记树之外,被判成「面板外」,点它内部会关掉整张菜单。\n // 用例 `退场窗口内重开后,收尾中的子菜单面板在取回前后都不可被当成面板外` 守着\n // 这一条 —— 把循环体整段删掉,那条会红(实测 1 failed | 95 passed)。\n // 写进 `_submenuHandles` 还因为收尾结束时 `_releaseSubmenu` 按 panel 取句柄释放。\n for (const container of this._closingSubmenus.closing()) {\n this._submenuHandles.set(container.panel, this._menuHandle.adopt(container.panel))\n }\n }\n // 父项始终留在 menu.content 内,观察它即可覆盖各级子菜单在打开期间的内容重建。\n this._contentObserver.observe(this._menu.content, { childList: true, subtree: true })\n requestAnimationFrame(() => {\n if (!this._isOpen || !this._menu) return\n this._refreshMenu()\n showOverlayPresence(this._menu.panel, { isInstant: this._shouldOpenInstantly })\n this._shouldOpenInstantly = true\n /*\n * 首项聚焦必须再等一帧,不能和 `_refreshMenu()` 同帧。\n *\n * 两个原因都指向「太早」:\n *\n * 1. `showModal()` 的 dialog focusing steps 由 UA 排队执行,会在我们已经聚焦首项\n * **之后**再把焦点挪到 dialog 上 —— 模态化之前没有这一步,也就没有这场竞争。\n * 2. `_setupMenuItems()` 刚创建的 `web-ui-dropdown-item` 尚未完成 Lit 的 shadow\n * 渲染,`focusItem()` 此刻还找不到 `.item-inner`,静默无操作。\n *\n * 实测:同帧聚焦后 `document.activeElement` 是 `<dialog>` 而非首项;再等一帧聚焦,\n * 焦点稳定落在菜单项内。\n */\n requestAnimationFrame(() => {\n if (!this._isOpen || !this._menu) return\n this._focusFirstItem()\n })\n })\n } else {\n this._syncScrollLock(false)\n // 吸收窗口只覆盖「长按之后浏览器补发的那几个事件」。菜单已关,\n // 之后任何一次真实右键或点击都必须走正常路径,不能被这个窗口吞掉。\n this._longPressOpenedAt = null\n // release ⟺ 关闭:登记与仲裁归属同时撤销,退场动画只是视觉收尾。\n this._menuHandle?.release()\n this._menuHandle = null\n this._contentObserver.disconnect()\n this._menuPositionGeneration.invalidate()\n this._refreshScheduled = false\n void this._closeMenuAfterPresence()\n }\n if (this._userOpenChange.consume()) this._dispatchChange(this._isOpen)\n }\n if (changed.has('noScrollLock')) this._syncScrollLock()\n }\n\n /**\n * 在指定视口坐标打开菜单。\n * @param x 水平坐标(px)。\n * @param y 垂直坐标(px)。\n * @returns 无返回值;命令式打开不派发 `open-change` 事件。\n */\n openAt(x: number, y: number) {\n this._openAt(x, y, true, true)\n }\n\n private _openAt(\n x: number,\n y: number,\n isInstant: boolean,\n suppressFocusVisible: boolean,\n centerOnPressPoint = false\n ): boolean {\n if (this.disabled) return false\n this._x = x\n this._y = y\n this._shouldOpenInstantly = isInstant\n this._suppressInitialFocusVisible = suppressFocusVisible\n this._centerOnPressPoint = centerOnPressPoint\n this._outsideClickGuard.arm()\n if (this._isOpen) {\n this._closeSubmenusFrom(0, true)\n this._closingSubmenus.restoreAll()\n // 先失效尚未完成的旧定位,再调度下一帧新代;避免新请求前的旧 promise 胜出。\n this._menuPositionGeneration.invalidate()\n this._scheduleRefresh()\n return false\n }\n this._isOpen = true\n return true\n }\n\n /**\n * 关闭菜单。\n * @returns 无返回值;命令式关闭不派发 `open-change` 事件。\n */\n close() {\n if (!this._isOpen) return\n this._isOpen = false\n this._menuPositionGeneration.invalidate()\n /*\n * 菜单项激活触发的关闭要**现在**就让 scrim 出顶层,不能等退场结束。\n *\n * 焦点归还由 UA 负责(`close()` 把焦点还给 `showModal()` 之前的 activeElement),\n * 而菜单项的回调常紧接着把焦点送到别处 —— 典型是右键重命名:回调把行换成编辑态、\n * 下一帧 `select()` 进 textarea。等到退场结束才关,UA 的归还会把焦点从 textarea\n * 拽回那一行,blur 掉编辑器,而 editable-text 的 blur 契约会**提交未改动的标题**。\n * 那是 interweave 上真实存在的 bug,不能放回来。\n *\n * 此刻焦点还在菜单里,UA 的归还正好落在 opener 上——那本来就是用户期望的落点,\n * 应用随后的焦点转移不会被覆盖。元素本身要留到面板真正移除(见 `_releaseScrimModality`)。\n */\n if (this._closeSource === 'menu-item') this._releaseScrimModality()\n this._closeSource = 'default'\n }\n\n private readonly _closeFromUser = (source: CloseSource = 'default') => {\n if (!this._isOpen) return\n this._userOpenChange.mark()\n this._closeSource = source\n this.close()\n }\n\n // 同一帧内多次内容变化只刷新一次,避免观察者与刷新自身 append 形成循环。\n private _scheduleRefresh() {\n if (this._refreshScheduled || !this._isOpen) return\n this._refreshScheduled = true\n requestAnimationFrame(() => {\n this._refreshScheduled = false\n if (!this._isOpen || !this._menu) return\n this._refreshMenu()\n })\n }\n\n // 重新同步 portal 内容并重定位;fresh-open、重定位与观察者触发的刷新共用。\n private _refreshMenu() {\n if (!this._menu) return\n this._setupMenuItems()\n this._positionMenu()\n this._hoverBinder.bind()\n }\n\n /**\n * 定位:**触屏长按与鼠标右键共用这一条 Floating UI 路径**。\n *\n * 两条入口现在只差「触发点的 x/y 从哪来」,布局契约完全一致:\n * `bottom-start` 表达「面板上缘 == 触发点 y、向下展开」,水平以触发点 x 为基准。\n * 之前有一条手写的贴底分支(先按点、再按视口下缘算 `top`),那是把「贴视口底」硬编码\n * 成算术;真机反馈证明它错(菜单跑到屏幕最底部,与手指按下的位置完全脱节)。现在\n * 落点交给 `flip` / `shift` 按**实际空间**决定,`placement` 只表达意图。\n *\n * 不再按「是否触屏」分流:窄视口的鼠标右键同样期望菜单贴着光标出现,按输入类型或视口\n * 宽度分流会让同一块屏幕上的两种输入拿到两套布局。分流所需的 `_anchorAtPressPoint`\n * 连同 `_positionMenuInViewport` / `_positionMenuAtPressPoint` 两个方法一起删掉 ——\n * 面板恒为 open scrim `<dialog>` 的直接子节点(`createMenuPortalOverlay` 第三参传入\n * scrim,`menu-portal.ts` 里 `container = scrim ?? …`),所以旧代码那条\n * 「非 dialog 容器」的分支对 context-menu 本来就不可达。\n *\n * 算出来的 left/top 是**视口坐标**。面板是 `position: fixed`,而 scrim 刻意不带\n * transform / filter / contain(见 style.css),因此不构成 fixed 的包含块 ——\n * 坐标直接相对视口解析。这条等价关系由 style.css 的满屏声明保证,并由 R6 的\n * 「scrim 无 transform/filter/contain」+ viewport-fit 的几何断言兜底。\n */\n private _positionMenu() {\n const panel = this._menu?.panel\n if (!panel) return\n\n /*\n * 先读安全区,再碰面板:`getComputedStyle` 会强制一次同步布局,让它发生在面板进入\n * 「可测」状态之前,而不是夹在「面板刚可测」与「Floating UI 量它」之间。\n *\n * 需要说清楚的是,**这不是** viewport-fit 那条横向断言里 2px 抖动的成因。把这一行移到\n * 最前面实测并没有让那个偏差消失(改前改后都是 204 对期望 206)。那个 2px 来自 flip 的\n * reset 会多跑一轮测量:那一刻面板宽 202px、落定后 200px。相关用例已经不再钉与视口\n * 边界的精确距离。\n */\n const safeAreaBottom = this._safeAreaBottom()\n\n panel.style.visibility = 'hidden'\n panel.style.display = ''\n\n const generation = this._menuPositionGeneration.next()\n /*\n * 可用矩形 = 视口 − 安全区 − 最小间距。flip 与 shift 必须吃**同一份** padding:\n *\n * - flip 用它判断「这一侧放不放得下」,决定翻不翻;\n * - shift 用它做实际钳制,且 `crossAxis: true` 会**同时**钳主轴与交叉轴\n * (core 的 shift 默认 mainAxis=true / crossAxis=false,开了 crossAxis 才两边都管)。\n *\n * 安全区必须进 shift 而不只是 flip:flip 只挑「哪一侧」,挑完就把面板贴在触发点上,\n * 不再管这一侧内部的位置。触发点本身落在安全区里时(按在屏幕最底部),挑出来的\n * 「上方」那一侧仍然把面板下缘按在安全区里 —— 只有 shift 的主轴钳制会把它推上来。\n * 这条正是 R7 用例在钉的行为。\n */\n const boundaryPadding = {\n top: VIEWPORT_PADDING,\n bottom: VIEWPORT_PADDING + safeAreaBottom,\n left: VIEWPORT_PADDING,\n right: VIEWPORT_PADDING\n }\n /*\n * 限制 flip 的候选集为「下方 / 上方」两个,**刻意不含 `-start` / `-end` 的另一侧**。\n *\n * 为什么必须限制:`flip` 对对齐过的 placement(`bottom-start`)默认展开成\n * `getExpandedPlacements()`,候选里带着 `bottom-end` / `top-end`。于是鼠标贴视口右缘、\n * `bottom-start` 的对齐侧溢出时,flip 会认定「`bottom-end` 才放得下」并把面板整个挪到\n * 光标左侧。落点仍在视口内,但那是一次没人要求的契约变更 —— 水平夹取本来就归 shift,\n * 而且对齐量一变,transform-origin 的推导前提(对齐量不变 ⇒ 水平 origin 只由 shift 位移\n * 决定)就不成立了。\n *\n * 用的是 `fallbackPlacements` 而不是 `allowedPlacements`:后者是 `autoPlacement` 的选项,\n * `flip` 根本不读,传进去只会被静默忽略(TS 会报错,运行时会当成 padding 之外的字段丢掉)。\n * `fallbackPlacements` 正是 flip 用来**替换**上面那个默认展开列表的字段,指定后\n * `placements = [initialPlacement, ...fallbackPlacements]`,连 bestFit 兜底也只在这份\n * 候选里挑,`-end` 因此永远不可能出现。\n *\n * 用例「鼠标贴视口右缘时对齐量不被 flip 改成 -end」钉的就是这一条。\n */\n const placements = this._centerOnPressPoint\n ? (['bottom', 'top'] as const)\n : (['bottom-start', 'top-start'] as const)\n\n void computePosition({ getBoundingClientRect: () => new DOMRect(this._x, this._y, 0, 0) }, panel, {\n strategy: 'fixed',\n /*\n * `bottom`(触屏):面板上缘对齐触发点 y、水平居中于触发点 x。\n * `bottom-start`(鼠标):同样上缘对齐 y,但左缘对齐光标 x。理由见\n * `_centerOnPressPoint` —— 居中会把光标落进面板上缘的命中区。\n *\n * 居中在贴边时**必然**被 shift 的交叉轴钳制打破(按点 x 太靠边,居中的面板会溢出视口)。\n * 这不是缺陷而是取舍:完整可见优先于几何居中。shift 推完面板仍在视口内,\n * 只是不再居中 —— 用例「按点贴视口右缘时仍完整可见」钉的就是这一条。\n */\n placement: placements[0],\n middleware: [\n /*\n * flip 负责「向下放不下就翻到按点上方」—— 面板下缘对齐按点、向上长。\n * 两侧都放不下(按点极低且面板极高)时它按 bestFit 选空间多的一侧并夹住主轴,\n * 所以不需要自己再写一遍兜底夹取。\n */\n flip({ fallbackPlacements: [...placements], padding: boundaryPadding }),\n shift({ padding: boundaryPadding, crossAxis: true })\n ]\n }).then(({ x, y, placement, middlewareData }) => {\n if (!this._isOpen || this._menu?.panel !== panel || !this._menuPositionGeneration.isCurrent(generation)) return\n const shiftX = middlewareData.shift?.x ?? 0\n /*\n * 垂直 origin 取 flip 后的实际 placement,不看 shift 位移:placement 直接说明面板\n * 落在触发点的哪一侧,而「离触发点最近的那条边」就是 scale 应该长出来的地方。\n *\n * 水平:默认跟 placement 的对齐量;被 shift 推动时改用推动方向那一侧,菜单才是朝\n * 光标「长过来」而不是从视口边缘滑进去。居中(placement 不带 `-` 对齐后缀)且未被\n * 推动时保持 `center`。\n */\n const horizontalOrigin = !placement.includes('-')\n ? shiftX === 0\n ? 'center'\n : shiftX < 0\n ? 'right'\n : 'left'\n : shiftX < 0\n ? 'right'\n : 'left'\n this._applyMenuPosition(panel, x, y, horizontalOrigin, placement.startsWith('bottom') ? 'top' : 'bottom')\n })\n }\n\n /**\n * 视口下缘的安全区高度(px)。\n *\n * 改按压点展开之后它**仍然必需**,而且不再只服务触屏:菜单默认从触发点向下长,触发点\n * 落在屏幕下半部时面板下缘会进入 home indicator 区域。它现在折进 flip 与 shift 共用的\n * 边界 padding(见 `_positionMenu`),不再需要单独一条 `maxBottom` 算术。\n *\n * 读宿主的自定义属性而不是 `env()`:自定义属性读回的是**计算后**的解析值,\n * 而 `env()` 在 JS 里没有对应的求值入口,只能间接经由声明它的属性取。\n * 属性未解析时 `parseFloat` 得 NaN,落到 0 —— 与 `env(..., 0px)` 的缺省一致。\n */\n private _safeAreaBottom(): number {\n const value = Number.parseFloat(getComputedStyle(this).getPropertyValue('--wui-context-menu-safe-area-bottom'))\n return Number.isFinite(value) ? value : 0\n }\n\n /**\n * 写下面板坐标与进场 transform 的原点。\n *\n * 两个 origin 必传且**不**再回退成坐标比较:flip 会改变垂直方向,回退表达式\n * `y < this._y ? 'bottom' : 'top'` 在「面板顶边与触发点重合」时两边都成立,等于随机取。\n * 唯一调用方从 flip 后的 placement 与 shift 位移推出真实侧边,见 `_positionMenu`。\n */\n private _applyMenuPosition(\n panel: HTMLElement,\n x: number,\n y: number,\n horizontalOrigin: 'left' | 'right' | 'center',\n verticalOrigin: 'top' | 'bottom'\n ) {\n panel.style.left = `${x}px`\n panel.style.top = `${y}px`\n panel.style.setProperty('--wui-internal-overlay-transform-origin', `${verticalOrigin} ${horizontalOrigin}`)\n panel.style.visibility = ''\n }\n\n /**\n * 把焦点带进菜单的第一项。\n *\n * `attempt` 是重试计数:菜单项是刚被 `reconcileManagedMenuItems` 搬进来的自定义元素,\n * 它们的 shadow(`.item-inner`)由各自的 Lit 更新**异步**渲染。`focusMenuItem()` 委托给\n * 项的 `focusItem()`,那一刻 `.item-inner` 还不存在,于是**静默无操作** —— 不抛错、\n * 也不留痕,只表现为「菜单开了但焦点没进去」。\n *\n * 所以这里不假设一次成功:调用后用 `findFocusedMenuItem` 复核,没落上就在下一帧再试,\n * 最多 `FOCUS_FIRST_ITEM_ATTEMPTS` 次。上界是必要的 —— 菜单项若始终渲染不出来\n * (空菜单、内容被外部换掉),不能让重试链一直跑下去。\n */\n private _focusFirstItem(attempt = 0) {\n const items = this._menu?.content.querySelectorAll<HTMLElement>('web-ui-dropdown-item:not([disabled])')\n const firstItem = items?.[0]\n if (!firstItem) return\n focusMenuItem(firstItem, { suppressFocusVisible: this._suppressInitialFocusVisible })\n if (attempt + 1 >= FOCUS_FIRST_ITEM_ATTEMPTS) return\n if (findFocusedMenuItem([this._menu?.panel])) return\n requestAnimationFrame(() => {\n if (!this._isOpen || !this._menu) return\n this._focusFirstItem(attempt + 1)\n })\n }\n\n private _setupMenuItems() {\n const menu = this._menu\n if (!menu) return\n const content = menu.content\n\n reconcileManagedMenuItems(this, content, this._menuItemAnchors, MARKER_TEXT)\n this._hideMenuItems()\n // 宿主可能在菜单打开期间动态改写 slot 子内容(如切换上下文后重建嵌套子项),\n // 新节点没有隐藏 slot 会落入父项默认 slot 可见渲染并叠到一级菜单上;\n // 因此对已移入 content 的嵌套子项也要重新隐藏。\n hideNestedMenuChildren(content, 'context-menu-hidden')\n\n // 框架 v-if 注释锚点的复位独立于元素重排:无论元素序是否已变都要执行。\n // 若挂在「元素序已变才重排」之下,顺序恰好正确时会跳过复位,锚点漂移无法收敛,\n // 而 Vue 下次翻转正依赖锚点引导新分支项的插入点。\n const anchors = captureFrameworkAnchors(content, MARKER_TEXT)\n orderManagedMenuItems(this, content, this._menuItemAnchors, MARKER_TEXT)\n restoreFrameworkAnchors(content, anchors)\n }\n\n private _returnItemsToSlot() {\n const menu = this._menu\n if (!menu) return\n\n this._closeSubmenusFrom(0, true)\n this._closingSubmenus.restoreAll()\n\n returnManagedMenuItemsToSlot(this, menu.content, this._menuItemAnchors)\n }\n\n /**\n * 建 scrim 并让它进入模态态,返回容器供面板挂载。\n *\n * 每次开启都新建而不是复用:scrim 的生命周期必须与「这一次开启」严格同进同出。\n * 复用一个 dialog 意味着上一轮的 `close()` 事件、`open` 属性与 UA 记下的焦点还原目标\n * 会跨会话残留,而这些都不是本组件能重置的。\n */\n private _openScrim(): HTMLDialogElement {\n const scrim = document.createElement('dialog')\n scrim.className = 'wui-context-menu-scrim'\n /*\n * scrim 的样式必须**显式挂到 scrim 元素上**。\n *\n * `static styles`(style.css)只进宿主的 shadow root,scrim 又是本组件自己造的节点、不进\n * 宿主 shadow root,拿不到那份样式;`scrim.css` 若只躺在文件里而不注入,就是死代码。\n * 实测未注入时 scrim 完全是 UA 默认 `dialog:modal` 的样子:`margin: auto` +\n * `width/height: fit-content` 让它在 414x896 视口里缩成 **38x38 的白盒子**正中,\n * `background` 还是不透明白 —— 菜单一开就在屏幕中间糊一块白色方块。\n *\n * 用子 `<style>` 而不是行内 `cssText`:与 `menu-portal.ts` 对 dialog 容器注入共享样式\n * 是同一手法(那边已在 dialog 容器上验证过),随元素一起移除,不留全局残留。\n */\n const scrimStyles = document.createElement('style')\n scrimStyles.textContent = scrimStyle\n scrim.append(scrimStyles)\n scrim.setAttribute(SCRIM_ATTR, '')\n scrim.addEventListener('click', this._onScrimClick)\n scrim.addEventListener('cancel', this._onScrimCancel)\n // showModal() 要求元素已在文档中。\n this._scrimParent().append(scrim)\n scrim.showModal()\n this._scrim = scrim\n return scrim\n }\n\n /**\n * scrim 的挂载点:**与面板在没有 scrim 时会落到的容器同源** —— 最近主题的 theme-owned\n * overlay root。scrim 就是那个容器的替身(面板必须挂进 scrim,见 `updated()` 的顺序注释),\n * 所以它得站在容器该站的位置上。\n *\n * 为什么不能在整棵 DOM 里随便挑位置:`--wui-color-*` 由 `<web-ui-theme>` 写在自己的 `:host`\n * 上,只沿 DOM 树向下继承。scrim 若挂到 `document.body`,而 `<web-ui-theme>` 是 body 的**子节点**,\n * 这条继承链就断了 —— 面板与 scrim 双双落回 `var(--wui-color-*, <浅色字面量>)` 的 fallback,\n * 深色外观下菜单是一块浅色玻璃(app 侧的 `dark:` 变体也够不到它)。挂进 theme overlay root\n * 之后,面板与 scrim 跟 dropdown / select 一样吃同一份 token。\n *\n * 无主题时保持历史行为挂在 `document.body`:那条路径本来就没有 token 作用域可继承,改用\n * fallback overlay root 只会改变既有测试与消费者的观察面,不换来任何东西。\n */\n private _scrimParent(): HTMLElement {\n return findNearestTheme(this)?.getOverlayRoot() ?? document.body\n }\n\n /**\n * 关掉并摘除 scrim。幂等:关闭分支与 `disconnectedCallback` 都会调。\n *\n * `close()` 触发 UA 的焦点归还,**必须先于面板 `remove()`** —— 理由见 `_closeMenuAfterPresence`。\n */\n private _closeScrim() {\n const scrim = this._scrim\n if (!scrim) return\n this._scrim = undefined\n if (scrim.open) scrim.close()\n scrim.remove()\n }\n\n /**\n * 只让 scrim 出顶层,不摘除元素。面板挂在 scrim 里,摘了就把退场动画一起摘掉。\n *\n * 提前出顶层是为了焦点(理由见 `close()`)。代价是这个尾巴上 scrim 已经在 top layer\n * 之外:下层恢复可命中,且面板的层级退回普通流 —— 在「菜单开在 drawer/dialog 之上」\n * 的场景里,退场动画会被下层的 top layer 盖住。\n *\n * 这条尾巴只有一次退场过渡的时长(默认 160ms),且只发生在菜单项激活时。\n * 摘除仍在 `_closeScrim()`,那时面板已经走完。\n */\n private _releaseScrimModality() {\n const scrim = this._scrim\n if (scrim?.open) scrim.close()\n }\n\n /**\n * 点 scrim = 关菜单(Q12:必关,不给 prop 选项),与 dialog / drawer 的实现同形:\n * 只有落点就是 scrim 本身才算「点外面」,落在面板(scrim 的后代)上不算。\n *\n * 落点判据之所以只能是这两个,是模态给的:菜单打开期间下层收不到命中,事件只可能\n * 落在 scrim 或面板上,两者都由本组件判定。\n *\n * 更早的实现在 document 捕获阶段挂 click 监听,把「点外面」理解成「命中测试落在菜单\n * 面板之外」—— 宿主 light DOM 的每一行都满足这个条件,于是判定与行激活成了同一个\n * click 的两个后果,中间没有任何仲裁,下层目标先被激活。那条监听器连同它的实现已一并\n * 删除:模态化之后它没有任何还能生效的场景,别再挂回来。\n */\n private _onScrimClick = (e: MouseEvent) => {\n if (e.target !== this._scrim) return\n // 长按抬手后浏览器补发的 click 属于那一次触摸意图,不能把刚开的菜单关掉。\n if (this._isLongPressFollowUp()) return\n this._closeFromUser()\n }\n\n /**\n * Escape 的**唯一**关闭路径仍是共享仲裁者(`_overlay.requestClose` → `closeDeepestOrAll`),\n * 它按层深决定是关最深子菜单还是关整张菜单。原生 dialog 自己也监听 Escape 并派发\n * `cancel`,若放任默认行为,UA 会直接关掉 scrim —— 那条路径不经过组件状态机,\n * 既可能漏派 `open-change`,也会把「关最深一层」的语义降级成「整张菜单没了」。\n *\n * 因此这里只吞掉默认行为,不重复关闭。仲裁者那条路径已经会走 `_closeFromUser()`。\n */\n private _onScrimCancel = (e: Event) => {\n e.preventDefault()\n }\n\n private async _closeMenuAfterPresence() {\n const menu = this._menu\n if (menu && !(await hideOverlayPresence(menu.panel))) return\n if (this._isOpen || !this.isConnected || this._menu !== menu) return\n\n /*\n * 归还判定必须在移除面板之前做完:面板一脱离文档,「焦点仍在菜单内」就再也读不到了。\n * 子菜单层此刻仍可能持有焦点,一并计入。\n */\n /*\n * 先关 scrim 再摘面板,顺序不能反。\n *\n * 焦点归还是 `showModal()` / `close()` 的 UA 行为,不再由组件自己实现:UA 记的是\n * `showModal()` 之前的 `document.activeElement`,`close()` 时还回去。反过来做就成了\n * 实测里那条「open 中直接移除 dialog → 焦点丢到 body,不归还」——面板先脱离文档,\n * 焦点链断了,UA 的还原目标也就落空。\n *\n * 菜单项激活那条路径的 scrim 早在 `close()` 里就出了顶层(`_releaseScrimModality`),\n * 这里的 `close()` 对它是幂等空转,摘除照常发生。\n *\n * 「关闭期间焦点被外部接管时不抢回」那道防护不是被删除,是被**移到了更早的位置**:\n * UA 自己没有这个分支(实测:外部接管后 close,焦点必被抢回 opener,`close()` /\n * `remove()` / `open = false` / 摘 `open` / 置 `display:none` / opener 移除或不可聚焦,\n * 十种写法全都照旧夺焦点)。所以只能在焦点**还没被送走**的时候就把顶层让掉 ——\n * 也就是菜单项激活那一刻。点外面 / Escape / 程序式关闭不抢焦点是用户期望的语义,\n * 那三条保留 dialog 默认效果。\n */\n this._closeScrim()\n\n // 登记已在关闭分支撤销(release ⟺ 关闭),这里只做内容归还与 DOM 收尾。\n this._returnItemsToSlot()\n menu?.panel.remove()\n this._menu = undefined\n }\n\n private _hideMenuItems() {\n getMenuChildren(this).forEach(child => child.setAttribute('slot', 'context-menu-hidden'))\n hideNestedMenuChildren(this, 'context-menu-hidden')\n }\n\n private _openSubmenu(item: HTMLElement, isInstant = false) {\n if (!this._isOpen || !item.hasAttribute('submenu') || item.hasAttribute('disabled')) return\n\n const level = this._getItemLevel(item)\n if (level === -1 || this._activeSubmenuItems[level] === item) return\n\n this._closeSubmenusFrom(level)\n const closingSubmenu = this._closingSubmenus.take(item)\n if (closingSubmenu) {\n this._activeSubmenus[level] = closingSubmenu\n this._activeSubmenuItems[level] = item\n /*\n * 取回的面板写回 `_submenuHandles`,让收尾结束时的 `_releaseSubmenu` 释放到正确的句柄。\n * 注:「面板在根句柄子树里」这一条已由上面的 claim 分支保证 —— 该分支会把仍在\n * closing 栈里的面板一并重挂(`_closingSubmenus.closing()`)。实测单独摘掉这一行\n * 全部用例仍绿,所以它维护的是释放账本,不是树的成员关系。\n *\n * **这一行没有任何测试守护**:把 `adopt` 与 `set` 一起摘掉,context-menu 的 96 条\n * 用例仍然全绿(实测 96 passed)。所以下面写的是「当前如此」—— 这一行维护的是\n * 释放账本(收尾结束时 `_releaseSubmenu` 按 panel 取句柄),不是树的成员关系;\n * 成员关系由上面 claim 分支的 `_closingSubmenus.closing()` 循环负责,那一条**是**\n * 有测试的。改动这一行前请自己判断要不要补一条能红的用例,别把这里的沉默当成覆盖。\n */\n if (this._menuHandle) this._submenuHandles.set(closingSubmenu.panel, this._menuHandle.adopt(closingSubmenu.panel))\n item.setAttribute('active', '')\n this._positionSubmenu(item, closingSubmenu)\n showOverlayPresence(closingSubmenu.panel, { isInstant })\n this._hoverBinder.bind()\n return\n }\n if (getMenuChildren(item).length === 0) return\n\n /*\n * 子菜单与根菜单挂进**同一个** scrim。只改根菜单会让两层行为分裂:根层被 scrim\n * 罩住时,子菜单若仍留在 overlay root,它自己就成了 scrim 子树外的元素 —— 模态期间\n * 不可点,且点它内部会被判成「点外面」而关掉整张菜单。\n */\n const submenu = createMenuPortalOverlay('context-submenu', this, this._scrim)\n submenu.panel.dataset.level = String(level)\n submenu.panel.setAttribute('role', 'menu')\n submenu.panel.setAttribute('aria-label', '子菜单')\n submenu.panel.style.visibility = 'hidden'\n submenu.panel.addEventListener('click', this._onMenuClick)\n moveMenuChildren(item, submenu.content)\n\n this._activeSubmenus[level] = submenu\n this._activeSubmenuItems[level] = item\n /*\n * 子层进根句柄的子树,默认不是独立候选 —— 「它在树里、但不参与这一层裁决」由此\n * 表达。原先按 level===0 选择根面板还是上一层子面板的父级判断随之消失:contains\n * 沿子树递归,拍平与嵌套对任何现有查询结果等价。\n */\n if (this._menuHandle) this._submenuHandles.set(submenu.panel, this._menuHandle.adopt(submenu.panel))\n item.setAttribute('active', '')\n this._positionSubmenu(item, submenu)\n showOverlayPresence(submenu.panel, { isInstant })\n this._hoverBinder.bind()\n }\n\n private _closeSubmenusFrom(level: number, isInstant = false) {\n for (let index = this._activeSubmenus.length - 1; index >= level; index--) {\n const submenu = this._activeSubmenus[index]\n const item = this._activeSubmenuItems[index]\n item?.removeAttribute('active')\n if (!item || isInstant) {\n if (submenu) this._releaseSubmenu(submenu.panel)\n this._restoreSubmenuItems(submenu, item)\n submenu.panel.remove()\n } else {\n this._closingSubmenus.closeAsync(item, submenu)\n }\n }\n this._activeSubmenus.length = level\n this._activeSubmenuItems.length = level\n }\n\n private _restoreSubmenuItems(submenu: MenuPortalOverlay, item?: HTMLElement) {\n if (!item) return\n moveMenuChildren(submenu.content, item)\n // 子菜单打开期间宿主可能重建了嵌套子项,归还时补隐藏,避免可见叠加。\n hideNestedMenuChildren(item, 'context-menu-hidden')\n }\n\n private _getItemLevel(item: HTMLElement): number {\n const menu = this._menu\n if (menu?.panel.contains(item)) return 0\n const parentLevel = this._activeSubmenus.findIndex(submenu => submenu.panel.contains(item))\n return parentLevel === -1 ? -1 : parentLevel + 1\n }\n\n private _positionSubmenu(item: HTMLElement, submenu: MenuPortalOverlay) {\n // 与主菜单同因:panel 进入 open native dialog 后处于 transformed containing\n // block,viewport 坐标的 left/top 会相对 dialog padding box 解析而整体偏移,\n // 需改走 Floating UI 换算为 dialog 相对坐标;开合方向仍按视口坐标度量预判。\n if (submenu.panel.parentElement instanceof HTMLDialogElement && submenu.panel.parentElement.open) {\n const itemRect = item.getBoundingClientRect()\n const padding = 8\n const canOpenRight = itemRect.right + submenu.panel.getBoundingClientRect().width + padding <= window.innerWidth\n // 同帧关闭→重开会复用同一 panel 产生两个 in-flight 定位 promise,完成序不保证\n // 后者胜出;按 panel 键控的代数 token 只允许最新一次调用写入,迟到旧 promise 丢弃。\n const epoch = (this._submenuPositionEpochs.get(submenu.panel) ?? 0) + 1\n this._submenuPositionEpochs.set(submenu.panel, epoch)\n void computePosition(item, submenu.panel, {\n strategy: 'fixed',\n placement: canOpenRight ? 'right-start' : 'left-start',\n middleware: [shift({ padding, crossAxis: true })]\n }).then(({ x, y, middlewareData }) => {\n if (!this._activeSubmenus.includes(submenu) || epoch !== this._submenuPositionEpochs.get(submenu.panel)) return\n // 与主菜单 dialog 路径一致:origin 由 shift 增量推导,视口预判只决定 placement。\n const shiftX = middlewareData.shift?.x ?? 0\n const shiftY = middlewareData.shift?.y ?? 0\n const horizontalOrigin = shiftX < 0 ? 'right' : 'left'\n const verticalOrigin = shiftY < 0 ? 'bottom' : 'top'\n submenu.panel.style.left = `${x}px`\n submenu.panel.style.top = `${y}px`\n submenu.panel.style.setProperty(\n '--wui-internal-overlay-transform-origin',\n `${verticalOrigin} ${horizontalOrigin}`\n )\n submenu.panel.style.visibility = ''\n })\n return\n }\n\n const itemRect = item.getBoundingClientRect()\n const submenuRect = submenu.panel.getBoundingClientRect()\n const padding = 8\n const canOpenRight = itemRect.right + submenuRect.width + padding <= window.innerWidth\n const left = canOpenRight ? itemRect.right : Math.max(padding, itemRect.left - submenuRect.width)\n const top = Math.min(Math.max(padding, itemRect.top), window.innerHeight - submenuRect.height - padding)\n\n submenu.panel.style.left = `${left}px`\n submenu.panel.style.top = `${top}px`\n submenu.panel.style.setProperty('--wui-internal-overlay-transform-origin', canOpenRight ? 'top left' : 'top right')\n submenu.panel.style.visibility = ''\n }\n\n private _dispatchChange(open: boolean) {\n dispatchOpenChangeEvent(this, open)\n }\n\n private _onPointerDown = (e: PointerEvent) => {\n // 任何一次新的按下都结束「上一次长按的补发事件」窗口:抬手后浏览器合成的\n // click 一定早于下一次 pointerdown,所以这个边界既能吸收补发,又不会误吞\n // 用户之后真正的点击。\n this._longPressOpenedAt = null\n // 鼠标走原生 contextmenu,无需长按;只认触屏指针。\n if (!this.longPress || this.disabled || e.pointerType !== 'touch') return\n this._cancelLongPress()\n this._longPressOrigin = { x: e.clientX, y: e.clientY }\n this._longPressTimer = setTimeout(\n () => {\n this._longPressTimer = undefined\n const origin = this._longPressOrigin\n if (!origin) return\n this._longPressOrigin = undefined\n if (this._isOpen) return\n this._longPressOpenedAt = performance.now()\n // 触屏长按:面板上缘对齐按点、向下展开,水平居中于按点(判据见 `_centerOnPressPoint`)。\n if (this._openAt(origin.x, origin.y, false, true, true)) this._userOpenChange.mark()\n },\n // 属性已被 normalizeNumber 钳到 [0, 5000],这里直接用;不再二次 Math.max。\n this.longPressDelay\n )\n }\n\n private _onPointerMove = (e: PointerEvent) => {\n if (e.pointerType !== 'touch') return\n const origin = this._longPressOrigin\n if (!origin) return\n // 触屏有隐式指针捕获,pointermove 会持续派发到 pointerdown 的落点,\n // 因此位移超阈值即判定为滚动意图,取消长按。\n const distance = Math.hypot(e.clientX - origin.x, e.clientY - origin.y)\n if (distance > LONG_PRESS_MOVE_TOLERANCE) this._cancelLongPress()\n }\n\n private _onPointerUp = (e: PointerEvent) => {\n if (e.pointerType === 'touch') this._cancelLongPress()\n }\n\n private _onPointerCancel = () => {\n this._cancelLongPress()\n }\n\n private _cancelLongPress() {\n if (this._longPressTimer !== undefined) {\n clearTimeout(this._longPressTimer)\n this._longPressTimer = undefined\n }\n this._longPressOrigin = undefined\n }\n\n /** 该事件是否是长按开菜单之后、浏览器为同一次触摸补发的事件。 */\n private _isLongPressFollowUp(): boolean {\n const openedAt = this._longPressOpenedAt\n if (openedAt === null) return false\n // 超期只说明「这次长按的补发没来」,不代表窗口承担时序保证;见常量注释。\n if (performance.now() - openedAt > LONG_PRESS_FOLLOW_UP_WINDOW_MS) {\n this._longPressOpenedAt = null\n return false\n }\n return true\n }\n\n private _onContextMenu = (e: MouseEvent) => {\n if (this.disabled) return\n if (this._isLongPressFollowUp()) {\n e.preventDefault()\n return\n }\n e.preventDefault()\n if (this._openAt(e.clientX, e.clientY, false, true)) this._userOpenChange.mark()\n }\n\n private _onContextMenuOutside = (e: MouseEvent) => {\n if (this._isOpen && !this._outsideClickGuard.isInside(e)) {\n this._closeFromUser()\n }\n }\n\n private _onKeydown = (e: KeyboardEvent) => {\n if (this.disabled) return\n\n if (e.key === 'ContextMenu' || (e.shiftKey && e.key === 'F10')) {\n e.preventDefault()\n const focused = document.activeElement\n if (focused && focused !== document.body) {\n const rect = focused.getBoundingClientRect()\n if (this._openAt(rect.left, rect.bottom, true, false)) this._userOpenChange.mark()\n } else {\n if (this._openAt(window.innerWidth / 2, window.innerHeight / 2, true, false)) this._userOpenChange.mark()\n }\n return\n }\n }\n\n private _onMenuClick = (e: MouseEvent) => {\n const item = getMenuItemFromEvent(e)\n if (!item || item.hasAttribute('disabled')) return\n if (item.hasAttribute('submenu')) {\n this._openSubmenu(item)\n return\n }\n this._closeFromUser('menu-item')\n }\n\n private _getLevelContainer(level: number) {\n return level === 0 ? this._menu : this._activeSubmenus[level - 1]\n }\n\n private _getLevelItems(level: number): HTMLElement[] {\n const container = this._getLevelContainer(level)\n if (!container) return []\n return getMenuChildren(container.content)\n }\n\n private _onWheel = (e: WheelEvent) => {\n this._preventBackgroundScroll(e)\n }\n\n private _onTouchMove = (e: TouchEvent) => {\n this._preventBackgroundScroll(e)\n }\n\n private _onDocumentKeydown = (e: KeyboardEvent) => {\n if (!this._isOpen) return\n // 鼠标右键打开的菜单焦点不在菜单内,e.target !== this 时也必须能 Escape 关闭;\n // 菜单为模态浮层,按 Escape 即关闭,无需限定焦点位置\n handleMenuKeyboard(this._keyboardDelegate, e)\n }\n\n private _closeLastSubmenuOrMenu() {\n if (this._activeSubmenus.length > 0) {\n const level = this._activeSubmenus.length - 1\n const parent = this._activeSubmenuItems[level]\n this._closeSubmenusFrom(level)\n focusMenuItem(parent)\n this._hoverBinder.bind()\n } else {\n this._closeFromUser()\n }\n }\n\n private _preventBackgroundScroll(e: Event) {\n if (!this._isOpen || this.noScrollLock || this._isMenuPanelEvent(e)) return\n e.preventDefault()\n }\n\n private _syncScrollLock(isOpen = this._isOpen) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n /** 按 panel 撤销一层子菜单的登记;句柄已随根层递归撤销时幂等无操作。 */\n private _releaseSubmenu(panel: HTMLElement) {\n this._submenuHandles.get(panel)?.release()\n this._submenuHandles.delete(panel)\n }\n\n private _isMenuPanelEvent(e: Event): boolean {\n // 在监听器内部判定:composedPath() 在派发结束后会被清空。\n return this._menuHandle?.containsEvent(e) ?? false\n }\n\n override render() {\n return html`\n <div class=\"context-menu-anchor\">\n <slot @slotchange=${this._onSlotChange}></slot>\n </div>\n `\n }\n\n private _onSlotChange() {\n this._hideMenuItems()\n // 打开期间宿主重建顶层项时,新成员要移入 portal 才对用户可见;\n // 关闭状态下无需移动,等下次打开时由 _setupMenuItems 统一处理。\n if (this._isOpen) this._scheduleRefresh()\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-context-menu': WebUiContextMenu\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ //#region src/components/context-menu/scrim.css?inline
2
+ var e = "dialog.wui-context-menu-scrim{width:auto;max-width:none;height:auto;max-height:none;color:inherit;background:0 0;border:0;margin:0;padding:0;position:fixed;inset:0;overflow:visible}dialog.wui-context-menu-scrim::backdrop{background:0 0}dialog.wui-context-menu-scrim:not([open]){display:block}dialog.wui-context-menu-scrim:focus,dialog.wui-context-menu-scrim:focus-visible{outline:none}";
3
+ //#endregion
4
+ export { e as default };
5
+
6
+ //# sourceMappingURL=scrim.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scrim.js","names":[],"sources":["../../../src/components/context-menu/scrim.css?inline"],"sourcesContent":["/*\n * context-menu 的模态 scrim。以内联 style 子节点挂在 scrim 元素自身,随 scrim 一起摘除。\n *\n * 三条硬约束,改动前先读:\n *\n * 1. **绝不能**出现 transform / filter / contain / backdrop-filter / will-change /\n * perspective。菜单面板是 `position: fixed` + Floating UI `strategy: 'fixed'`,\n * 上面任意一条都会让 scrim 成为 fixed 的包含块,Floating UI 返回的坐标随即整体偏移。\n * drawer 的 dialog 带着 `transform: translate(0,0)`,`_positionSubmenu` 里的\n * `instanceof HTMLDialogElement && .open` 分支正是为那种情况才存在的。\n *\n * 2. 盒子必须**精确铺满视口**(下面把 margin / max-* / padding / border 全部清零)。\n * Floating UI 的 `strategy: 'fixed'` 会把最近的 `position: fixed` 祖先当作 offsetParent,\n * scrim 正是这样一个祖先 —— 它返回的坐标是**相对 scrim padding box** 的。\n * padding box 与视口逐像素相等时两套坐标系恰好重合,位置才对。默认 UA 的\n * `dialog:modal` 带 `margin: auto` 与 `max-width: calc(100% - 6px - 2em)`,\n * 会让 scrim 居中且比视口小一圈 —— 那是静默偏移,测试里必须用几何断言钉住。\n *\n * 3. 视觉上保持透明。scrim 的职责是**吃掉下层命中**(点外面必关菜单,且下层收不到这次\n * click),不是给页面加一层遮罩变暗。给上下文菜单加全屏压暗会与 dialog / drawer 的\n * 遮罩语义混淆,而它们是有可见边框的实体卡片。\n * **「保持透明」必须同时管住元素与 ::backdrop**,两者是各自独立的绘制层,见下方规则。\n */\ndialog.wui-context-menu-scrim {\n position: fixed;\n inset: 0;\n\n /* 面板是 fixed,不在流内;UA 的 overflow: auto 在此既无内容可滚,也会引入滚动条。 */\n overflow: visible;\n\n width: auto;\n max-width: none;\n height: auto;\n max-height: none;\n margin: 0;\n padding: 0;\n border: 0;\n\n color: inherit;\n\n /* 透明底:不加变暗遮罩,理由见文件头第 3 条。 */\n background: transparent;\n}\n\n/*\n * `::backdrop` 必须单独清,否则整屏蒙一层 10% 黑。\n *\n * 它是独立伪元素,**不继承**元素的 background:上面那条 `background: transparent`\n * 只作用于元素本身,`showModal()` 一开 UA 照样按 `dialog::backdrop` 的默认值画\n * `rgba(0, 0, 0, 0.1)`。真机表现为「菜单一打开整页灰了一层」,而元素级检查\n * (scrim 的 background / filter / backdrop-filter 全是透明或 none)完全查不出来 ——\n * 那层颜色根本不在元素上。\n *\n * drawer / dialog 的 ::backdrop 是**有意**的实体卡片遮罩,不要照抄它们的取值;\n * context-menu 按裁定不压暗,这里是纯透明。\n */\ndialog.wui-context-menu-scrim::backdrop {\n background: transparent;\n}\n\n/*\n * 菜单项激活触发的关闭会**提前**出顶层(见 index.ts 的 `_releaseScrimModality`):\n * 此刻焦点还在菜单里,UA 的焦点归还正好落在 opener 上;等退场结束再让顶层就晚了,\n * 那时焦点已经被应用送走,归还会把它拽回来 blur 掉编辑器。\n *\n * scrim 元素本身必须留到面板真正被摘除 —— 面板挂在它里面,一起摘掉就没有退场动画了。\n * 而 UA 的 `dialog:not([open]) { display: none }` 会把面板一起藏掉,所以这里显式恢复可见。\n *\n * 代价是这个尾巴上 scrim 已经在 top layer 之外:下层恢复可命中,面板的层级退回普通流。\n * 尾巴只有一次退场过渡的时长(默认 160ms),且只发生在菜单项激活时。\n */\ndialog.wui-context-menu-scrim:not([open]) {\n display: block;\n}\n\n/*\n * scrim 是模态表面,不是可聚焦控件:`showModal()` 的 focusing steps 在面板还没挂进去时\n * 会把焦点交给 scrim 本身,键盘路径下 UA 会给它画一枚默认 ring(整屏一圈蓝框)。\n * 与 dialog / drawer 同一条策略(见 assets/modal-surface.css);这里就地收口,因为\n * scrim 的样式是注入到 theme overlay root / body 的独立样式表,够不到组件 shadow 内的共享层。\n */\ndialog.wui-context-menu-scrim:focus,\ndialog.wui-context-menu-scrim:focus-visible {\n outline: none;\n}\n"],"mappings":""}
@@ -1,5 +1,5 @@
1
1
  //#region src/components/context-menu/style.css?inline
2
- var e = "*,:before,:after{box-sizing:border-box}:host{display:inline-block}:host([disabled]){cursor:not-allowed;opacity:.4}.context-menu-anchor{display:contents}";
2
+ var e = "*,:before,:after{box-sizing:border-box}:host{--wui-context-menu-safe-area-bottom:env(safe-area-inset-bottom,0px);display:inline-block}.context-menu-anchor{display:contents}";
3
3
  //#endregion
4
4
  export { e as default };
5
5
 
@@ -1 +1 @@
1
- {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/context-menu/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: inline-block;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n opacity: 0.4;\n}\n\n.context-menu-anchor {\n display: contents;\n}\n"],"mappings":""}
1
+ {"version":3,"file":"style.js","names":[],"sources":["../../../src/components/context-menu/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n /*\n * 视口下缘的安全区高度。贴底展开时菜单要让开它,否则 iOS 的 home indicator 会压住\n * 最下面一项。用自定义属性而不是在 JS 里直接算:JS 侧没有 `env()` 的求值入口,\n * 只能经由声明它的属性读回**计算后的**解析值。宿主可覆盖(如刘海屏模拟)。\n */\n --wui-context-menu-safe-area-bottom: env(safe-area-inset-bottom, 0px);\n display: inline-block;\n}\n\n/*\n * disabled 不在宿主上做视觉降级。它只抑制菜单行为:菜单根本不渲染,宿主降级唯一能作用到的\n * 就是消费者塞进 default slot 的内容(触发区),而禁用右键菜单不等于禁用触发区。\n */\n\n.context-menu-anchor {\n display: contents;\n}\n"],"mappings":""}
@@ -2,6 +2,7 @@ import { LitElement, PropertyValues } from 'lit';
2
2
  export declare class WebUiDialog extends LitElement {
3
3
  static styles: import('lit').CSSResult[];
4
4
  open: boolean;
5
+ closable: boolean;
5
6
  noScrollLock: boolean;
6
7
  noBackdropClose: boolean;
7
8
  noEscapeClose: boolean;
@@ -28,11 +29,19 @@ export declare class WebUiDialog extends LitElement {
28
29
  close(): void;
29
30
  private handleCancel;
30
31
  private handleBackdropClick;
32
+ /** 用户发起的关闭入口(Escape、遮罩点击、关闭按钮)共用一条路径。 */
33
+ private readonly _closeFromUser;
31
34
  private emitOpenChange;
32
35
  private _onTransitionEnd;
33
36
  private _onNativeClose;
34
37
  private _syncScrollLock;
35
38
  private _onBodySlotChange;
39
+ /**
40
+ * 两种内容模式共用同一枚关闭按钮:都绝对定位到卡片右上角,偏移由
41
+ * `--wui-dialog-close-top` / `--wui-dialog-close-right` 控制(见 style.css)。
42
+ * 标题模式只是把它放在 `.title-row` 的 DOM 位置,定位参照仍是卡片本身。
43
+ */
44
+ private _renderCloseButton;
36
45
  render(): import('lit').TemplateResult<1>;
37
46
  readonly $events: {
38
47
  'open-change': CustomEvent<{
@@ -1,31 +1,34 @@
1
1
  import e from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
2
+ import "../icon/index.js";
2
3
  import t from "../../assets/glass.js";
4
+ import n from "../../icons/generated/ooui-close.js";
3
5
  import "../button/index.js";
4
- import { dispatchOpenChangeEvent as n } from "../../shared/open-state/index.js";
5
- import { defineOpenOverlay as r } from "../../shared/overlay/open-overlay.js";
6
- import { defineScrollLockLease as i } from "../../shared/scroll-lock/scroll-lock.js";
7
- import { UserChangeController as a } from "../../shared/events/user-change.js";
8
- import { defineNativeDialogPresence as o } from "../../shared/overlay/native-dialog-presence.js";
9
- import s from "./style.js";
10
- import { LitElement as c, html as l, unsafeCSS as u } from "lit";
11
- import { customElement as d, property as f, state as p } from "lit/decorators.js";
6
+ import { dispatchOpenChangeEvent as r } from "../../shared/open-state/index.js";
7
+ import { defineOpenOverlay as i } from "../../shared/overlay/open-overlay.js";
8
+ import { defineScrollLockLease as a } from "../../shared/scroll-lock/scroll-lock.js";
9
+ import { UserChangeController as o } from "../../shared/events/user-change.js";
10
+ import s from "../../assets/modal-surface.js";
11
+ import { defineNativeDialogPresence as c } from "../../shared/overlay/native-dialog-presence.js";
12
+ import l from "./style.js";
13
+ import { LitElement as u, html as d, nothing as f, unsafeCSS as p } from "lit";
14
+ import { customElement as m, property as h, state as g } from "lit/decorators.js";
12
15
  //#region src/components/dialog/index.ts
13
- var m = class extends c {
16
+ var _ = class extends u {
14
17
  constructor(...e) {
15
- super(...e), this.open = !1, this.noScrollLock = !1, this.noBackdropClose = !1, this.noEscapeClose = !1, this.controlled = !1, this._hasBody = !1, this._userOpenChange = new a(), this._overlay = r().make({
16
- requestClose: () => {
17
- if (this.controlled) {
18
- this.emitOpenChange(!1);
19
- return;
20
- }
21
- this._userOpenChange.mark(), this.close();
22
- },
18
+ super(...e), this.open = !1, this.closable = !1, this.noScrollLock = !1, this.noBackdropClose = !1, this.noEscapeClose = !1, this.controlled = !1, this._hasBody = !1, this._userOpenChange = new o(), this._overlay = i().make({
19
+ requestClose: () => this._closeFromUser(),
23
20
  isConnected: () => this.isConnected
24
- }), this._handle = null, this._scrollLock = i().make(), this._presence = o().make({
21
+ }), this._handle = null, this._scrollLock = a().make(), this._presence = c().make({
25
22
  getDialog: () => this.dialog,
26
23
  isConnected: () => this.isConnected,
27
24
  isOpen: () => this.open
28
- }), this._onTransitionEnd = (e) => {
25
+ }), this._closeFromUser = () => {
26
+ if (this.controlled) {
27
+ this.emitOpenChange(!1);
28
+ return;
29
+ }
30
+ this._userOpenChange.mark(), this.close();
31
+ }, this._onTransitionEnd = (e) => {
29
32
  this._presence.handleTransitionEnd(e);
30
33
  }, this._onNativeClose = () => {
31
34
  if (!this.open) {
@@ -42,7 +45,11 @@ var m = class extends c {
42
45
  };
43
46
  }
44
47
  static {
45
- this.styles = [u(t), u(s)];
48
+ this.styles = [
49
+ p(t),
50
+ p(s),
51
+ p(l)
52
+ ];
46
53
  }
47
54
  get dialog() {
48
55
  return this.shadowRoot?.querySelector("dialog") ?? null;
@@ -50,7 +57,7 @@ var m = class extends c {
50
57
  updated(e) {
51
58
  if (super.updated(e), this.isConnected) {
52
59
  if (e.has("open")) {
53
- this._userOpenChange.consume() && this.emitOpenChange(), this._presence.sync(this.open);
60
+ this._userOpenChange.consume() && this.emitOpenChange(), this._presence.sync(this.open), this.open && window.getSelection()?.removeAllRanges();
54
61
  let e = this.dialog;
55
62
  e && (this.open ? (this._handle?.release(), this._handle = this._overlay.claim(e, { ancestryFrom: this })) : (this._handle?.release(), this._handle = null));
56
63
  }
@@ -78,25 +85,13 @@ var m = class extends c {
78
85
  this.open = !1;
79
86
  }
80
87
  handleCancel(e) {
81
- if (e.target === e.currentTarget && (e.preventDefault(), !this.noEscapeClose)) {
82
- if (this.controlled) {
83
- this.emitOpenChange(!1);
84
- return;
85
- }
86
- this._userOpenChange.mark(), this.close();
87
- }
88
+ e.target === e.currentTarget && (e.preventDefault(), !this.noEscapeClose && this._closeFromUser());
88
89
  }
89
90
  handleBackdropClick(e) {
90
- if (e.target === e.currentTarget && !this.noBackdropClose) {
91
- if (this.controlled) {
92
- this.emitOpenChange(!1);
93
- return;
94
- }
95
- this._userOpenChange.mark(), this.close();
96
- }
91
+ e.target === e.currentTarget && (this.noBackdropClose || this._closeFromUser());
97
92
  }
98
93
  emitOpenChange(e = this.open) {
99
- n(this, e);
94
+ r(this, e);
100
95
  }
101
96
  _syncScrollLock(e = this.open) {
102
97
  this._scrollLock.sync(e && !this.noScrollLock);
@@ -104,8 +99,22 @@ var m = class extends c {
104
99
  _onBodySlotChange(e) {
105
100
  e.target instanceof HTMLSlotElement && (this._hasBody = e.target.assignedElements().length > 0);
106
101
  }
102
+ _renderCloseButton() {
103
+ return d`
104
+ <web-ui-button
105
+ class="wui-dialog-close"
106
+ @click=${this._closeFromUser}
107
+ aria-label="关闭"
108
+ variant="secondary"
109
+ icon
110
+ size="26"
111
+ >
112
+ <web-ui-icon size="14" .icon=${n}></web-ui-icon>
113
+ </web-ui-button>
114
+ `;
115
+ }
107
116
  render() {
108
- return l`
117
+ return d`
109
118
  <dialog
110
119
  @cancel=${this.handleCancel}
111
120
  @close=${this._onNativeClose}
@@ -113,9 +122,19 @@ var m = class extends c {
113
122
  @transitionend=${this._onTransitionEnd}
114
123
  >
115
124
  <div class="wui-dialog-body wui-glass">
116
- ${this._hasBody ? l`<slot name="body" @slotchange=${this._onBodySlotChange}></slot>` : l`
125
+ ${this._hasBody ? d`
126
+ <div class="wui-dialog-content">
127
+ <slot name="body" @slotchange=${this._onBodySlotChange}></slot>
128
+ </div>
129
+ ${this.closable ? this._renderCloseButton() : f}
130
+ ` : d`
117
131
  <slot name="body" @slotchange=${this._onBodySlotChange} hidden></slot>
118
- <div class="title"><slot name="title"></slot></div>
132
+ ${this.closable ? d`
133
+ <div class="title-row">
134
+ <div class="title"><slot name="title"></slot></div>
135
+ ${this._renderCloseButton()}
136
+ </div>
137
+ ` : d`<div class="title"><slot name="title"></slot></div>`}
119
138
  <div class="desc"><slot></slot></div>
120
139
  <div class="wui-dialog-footer"><slot name="footer"></slot></div>
121
140
  `}
@@ -124,26 +143,29 @@ var m = class extends c {
124
143
  `;
125
144
  }
126
145
  };
127
- e([f({
146
+ e([h({
147
+ type: Boolean,
148
+ reflect: !0
149
+ })], _.prototype, "open", void 0), e([h({
128
150
  type: Boolean,
129
151
  reflect: !0
130
- })], m.prototype, "open", void 0), e([f({
152
+ })], _.prototype, "closable", void 0), e([h({
131
153
  type: Boolean,
132
154
  reflect: !0,
133
155
  attribute: "no-scroll-lock"
134
- })], m.prototype, "noScrollLock", void 0), e([f({
156
+ })], _.prototype, "noScrollLock", void 0), e([h({
135
157
  type: Boolean,
136
158
  reflect: !0,
137
159
  attribute: "no-backdrop-close"
138
- })], m.prototype, "noBackdropClose", void 0), e([f({
160
+ })], _.prototype, "noBackdropClose", void 0), e([h({
139
161
  type: Boolean,
140
162
  reflect: !0,
141
163
  attribute: "no-escape-close"
142
- })], m.prototype, "noEscapeClose", void 0), e([f({
164
+ })], _.prototype, "noEscapeClose", void 0), e([h({
143
165
  type: Boolean,
144
166
  reflect: !0
145
- })], m.prototype, "controlled", void 0), e([p()], m.prototype, "_hasBody", void 0), m = e([d("web-ui-dialog")], m);
167
+ })], _.prototype, "controlled", void 0), e([g()], _.prototype, "_hasBody", void 0), _ = e([m("web-ui-dialog")], _);
146
168
  //#endregion
147
- export { m as WebUiDialog };
169
+ export { _ as WebUiDialog };
148
170
 
149
171
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dialog/index.ts"],"sourcesContent":["import { html, LitElement, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport '@/components/button'\nimport glass from '@/assets/glass.css?inline'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineNativeDialogPresence } from '@/shared/overlay/native-dialog-presence'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\n@customElement('web-ui-dialog')\nexport class WebUiDialog extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(style)]\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n @property({ type: Boolean, reflect: true, attribute: 'no-backdrop-close' }) noBackdropClose = false\n @property({ type: Boolean, reflect: true, attribute: 'no-escape-close' }) noEscapeClose = false\n\n /**\n * Controlled 模式下,Escape 和遮罩点击只派发 `open-change` 请求,\n * 不会自行修改 `open`。Consumer 回写 `open` 后才执行关闭动画。\n * 程序化 API(showModal/close/直接赋值 open)不受此模式影响,始终直通。\n */\n @property({ type: Boolean, reflect: true }) controlled = false\n\n @state() private _hasBody = false\n private readonly _userOpenChange = new UserChangeController()\n\n /*\n * 开启态浮层(issue #120 Block 1)。dialog 的 Escape 原本只走原生 cancel 事件:仲裁者\n * 会在 keydown 上 preventDefault 压掉原生关闭请求并 stopPropagation,因此这里必须\n * 自己表达关闭语义(controlled 只派发请求)。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => {\n if (this.controlled) {\n this.emitOpenChange(false)\n return\n }\n this._userOpenChange.mark()\n this.close()\n },\n isConnected: () => this.isConnected\n })\n /** 当前开启会话的句柄;未开启时为 null。查询与惰性同步走它。 */\n private _handle: OpenOverlayHandle | null = null\n private readonly _scrollLock = defineScrollLockLease().make()\n private readonly _presence = defineNativeDialogPresence().make({\n getDialog: () => this.dialog,\n isConnected: () => this.isConnected,\n isOpen: () => this.open\n })\n\n private get dialog() {\n return this.shadowRoot?.querySelector('dialog') ?? null\n }\n\n protected override updated(props: PropertyValues) {\n super.updated(props)\n if (!this.isConnected) return\n\n if (props.has('open')) {\n if (this._userOpenChange.consume()) this.emitOpenChange()\n this._presence.sync(this.open)\n // 原生 dialog 登记为开启态浮层:挂在它上面的 portal 面板成为本层后代,\n // Escape 仲裁据此判出最内层(issue #120 Block 1)。\n const dialog = this.dialog\n if (dialog) {\n if (this.open) {\n // 同一面板重新 claim = 新的一次开启:旧会话(含其子层)整体作废。\n this._handle?.release()\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n } else {\n this._handle?.release()\n this._handle = null\n }\n }\n }\n if (props.has('open') || props.has('noScrollLock')) this._syncScrollLock()\n this._syncOverlayInert()\n }\n\n override connectedCallback() {\n super.connectedCallback()\n // 重挂载对账:断连时 presence、滚动锁与登记都已被撤销,而 `open` 未变化时\n // `updated()` 不会补跑任何 sync 分支。首次连接时 shadow 尚未渲染、`this.dialog`\n // 为 null,三种情况都直接跳过;打开态的首次进入仍由 updated() 的\n // `props.has('open')` 分支处理。\n this._presence.reconcile()\n this._syncScrollLock()\n this._reclaimIfOpen()\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._presence.dispose()\n this._scrollLock.release()\n // 断连即撤销登记:层不留在全局注册表里,重连后由 _reclaimIfOpen 显式重新声明。\n this._handle?.release()\n this._handle = null\n }\n\n /*\n * 「重挂载恢复」的调用方一半:模块刻意不观察 DOM 连接状态,所以断连撤销登记之后,\n * 重连时仍有开启态就必须由组件重新声明。\n */\n private _reclaimIfOpen() {\n if (!this.open || this._handle) return\n const dialog = this.dialog\n if (!dialog) return\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n this._syncOverlayInert()\n }\n\n /*\n * `no-escape-close` 时 dialog 仍是候选(Escape 被吞掉、原生 cancel 被压掉),只是\n * 不走关闭入口。与旧的「跳过候选」语义不同:旧语义放任事件落到下层浮层,把外层\n * 一起关掉。该属性可在开启期间改写,所以每次渲染后读它,而不是在 claim 时固定。\n */\n private _syncOverlayInert() {\n this._handle?.setInert(this.noEscapeClose)\n }\n\n showModal() {\n if (this.open) return\n this.open = true\n }\n\n close() {\n this.open = false\n }\n\n /*\n * 仲裁者在 capture 阶段 preventDefault 了 Escape 的 keydown,UA 因此不再派发原生\n * cancel,但本 handler 仍有两个仲裁者不负责的职责,**不可删**:\n * ① 任何到达 dialog 的 cancel 都要 preventDefault,把 top layer 保留到视觉退场结束\n * (否则原生关闭会跳过退出动画);\n * ② 忽略子控件(例如 file input)冒泡上来的 cancel。\n * 实测(overlay-open-owner-260918,2026-09-18)摘掉 `@cancel` 后 dialog.browser.spec.ts\n * 的「no-escape-close 存在时 Escape/cancel 不关闭对话框」与 remount-reconcile 的\n * 「打开态被移出文档再接回」两例立刻转红,断言正是 `dispatchEvent(cancel) === false`。\n */\n private handleCancel(e: Event) {\n // 子控件(例如 file input)可能派发冒泡的 cancel;只让 native dialog 自身的 cancel 关闭。\n if (e.target !== e.currentTarget) return\n\n // 保留 top layer 直到视觉退场完成,避免原生关闭跳过退出动画。\n e.preventDefault()\n if (this.noEscapeClose) return\n if (this.controlled) {\n this.emitOpenChange(false)\n return\n }\n this._userOpenChange.mark()\n this.close()\n }\n\n private handleBackdropClick(e: MouseEvent) {\n if (e.target !== (e.currentTarget as HTMLDialogElement)) return\n if (this.noBackdropClose) return\n if (this.controlled) {\n this.emitOpenChange(false)\n return\n }\n this._userOpenChange.mark()\n this.close()\n }\n\n // controlled 下派发关闭请求:detail 固定 false(用户只能请求关闭,打开永远由 Consumer 写)。\n private emitOpenChange(open = this.open) {\n dispatchOpenChangeEvent(this, open)\n }\n\n private _onTransitionEnd = (event: TransitionEvent) => {\n this._presence.handleTransitionEnd(event)\n }\n\n private _onNativeClose = () => {\n // 关闭态下的 close 事件只可能是我们自己 finishClosing 排队的异步事件(或冗余的\n // 外部关闭):交给 presence 消费 self-close 标志,避免标志泄漏到下一次真实关闭。\n if (!this.open) {\n this._presence.handleNativeClose()\n return\n }\n\n // 我们自己 dialog.close() 排队的 close 事件是异步任务:正常时序在本关闭会话内\n // 到达,快速「关闭→重开」时它在重新打开之后才到达(过期事件)。两种情况都已\n // 完成清理,由 presence 消费并返回 true,不能当成外部关闭把刚重开的 dialog 关掉。\n if (this._presence.handleNativeClose()) return\n\n // controlled 下原生关闭(如表单 method=\"dialog\")视为未经 Consumer 批准的状态丢失:\n // 恢复受控状态并派发关闭请求,由 Consumer 决定是否关闭。\n if (this.controlled) {\n this._presence.sync(true)\n this.emitOpenChange(false)\n return\n }\n\n this._userOpenChange.mark()\n this.open = false\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n private _onBodySlotChange(e: Event) {\n if (!(e.target instanceof HTMLSlotElement)) return\n this._hasBody = e.target.assignedElements().length > 0\n }\n\n override render() {\n return html`\n <dialog\n @cancel=${this.handleCancel}\n @close=${this._onNativeClose}\n @click=${this.handleBackdropClick}\n @transitionend=${this._onTransitionEnd}\n >\n <div class=\"wui-dialog-body wui-glass\">\n ${\n this._hasBody\n ? html`<slot name=\"body\" @slotchange=${this._onBodySlotChange}></slot>`\n : html`\n <slot name=\"body\" @slotchange=${this._onBodySlotChange} hidden></slot>\n <div class=\"title\"><slot name=\"title\"></slot></div>\n <div class=\"desc\"><slot></slot></div>\n <div class=\"wui-dialog-footer\"><slot name=\"footer\"></slot></div>\n `\n }\n </div>\n </dialog>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-dialog': WebUiDialog\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/dialog/index.ts"],"sourcesContent":["import { html, LitElement, nothing, type PropertyValues, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport '@/components/icon'\nimport '@/components/button'\nimport glass from '@/assets/glass.css?inline'\nimport modalSurface from '@/assets/modal-surface.css?inline'\nimport { oouiClose } from '@/icons'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineNativeDialogPresence } from '@/shared/overlay/native-dialog-presence'\nimport { defineOpenOverlay, type OpenOverlayHandle } from '@/shared/overlay/open-overlay'\nimport { defineScrollLockLease } from '@/shared/scroll-lock/scroll-lock'\n\nimport style from './style.css?inline'\n\n@customElement('web-ui-dialog')\nexport class WebUiDialog extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(modalSurface), unsafeCSS(style)]\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) closable = false\n @property({ type: Boolean, reflect: true, attribute: 'no-scroll-lock' }) noScrollLock = false\n @property({ type: Boolean, reflect: true, attribute: 'no-backdrop-close' }) noBackdropClose = false\n @property({ type: Boolean, reflect: true, attribute: 'no-escape-close' }) noEscapeClose = false\n\n /**\n * Controlled 模式下,Escape 和遮罩点击只派发 `open-change` 请求,\n * 不会自行修改 `open`。Consumer 回写 `open` 后才执行关闭动画。\n * 程序化 API(showModal/close/直接赋值 open)不受此模式影响,始终直通。\n */\n @property({ type: Boolean, reflect: true }) controlled = false\n\n @state() private _hasBody = false\n private readonly _userOpenChange = new UserChangeController()\n\n /*\n * 开启态浮层(issue #120 Block 1)。dialog 的 Escape 原本只走原生 cancel 事件:仲裁者\n * 会在 keydown 上 preventDefault 压掉原生关闭请求并 stopPropagation,因此这里必须\n * 自己表达关闭语义(controlled 只派发请求)。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => this._closeFromUser(),\n isConnected: () => this.isConnected\n })\n /** 当前开启会话的句柄;未开启时为 null。查询与惰性同步走它。 */\n private _handle: OpenOverlayHandle | null = null\n private readonly _scrollLock = defineScrollLockLease().make()\n private readonly _presence = defineNativeDialogPresence().make({\n getDialog: () => this.dialog,\n isConnected: () => this.isConnected,\n isOpen: () => this.open\n })\n\n private get dialog() {\n return this.shadowRoot?.querySelector('dialog') ?? null\n }\n\n protected override updated(props: PropertyValues) {\n super.updated(props)\n if (!this.isConnected) return\n\n if (props.has('open')) {\n if (this._userOpenChange.consume()) this.emitOpenChange()\n this._presence.sync(this.open)\n if (this.open) {\n // presence.sync 内的 showModal() 把 dialog 提升进 top layer。双击手势本身\n // 留下的活选区会被浏览器拿去和提升后的新布局重新解析,结果选区落到刚挂载的\n // dialog 正文上。脆弱的是「手势来源的活选区」,不是 dblclick 的判定窗口:把\n // 打开推迟 250ms(早过双击判定)仍然复现。选区由浏览器在 showModal() 调用\n // 内部生成:脚本没有任何 Selection API 调用,在 dblclick 上 preventDefault\n // 也拦不住。\n //\n // 实测(Chromium)showModal() 不会动打开前已经存在的选区,所以这一次清理\n // 同时也覆盖「先选中页面文本、再打开」的情况——modal 打开后被选中的内容挡在\n // 遮罩后面本来也已不可用。这里只清文档选区,不动 user-select,浮层内正文\n // 仍可正常拖选。\n window.getSelection()?.removeAllRanges()\n }\n // 原生 dialog 登记为开启态浮层:挂在它上面的 portal 面板成为本层后代,\n // Escape 仲裁据此判出最内层(issue #120 Block 1)。\n const dialog = this.dialog\n if (dialog) {\n if (this.open) {\n // 同一面板重新 claim = 新的一次开启:旧会话(含其子层)整体作废。\n this._handle?.release()\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n } else {\n this._handle?.release()\n this._handle = null\n }\n }\n }\n if (props.has('open') || props.has('noScrollLock')) this._syncScrollLock()\n this._syncOverlayInert()\n }\n\n override connectedCallback() {\n super.connectedCallback()\n // 重挂载对账:断连时 presence、滚动锁与登记都已被撤销,而 `open` 未变化时\n // `updated()` 不会补跑任何 sync 分支。首次连接时 shadow 尚未渲染、`this.dialog`\n // 为 null,三种情况都直接跳过;打开态的首次进入仍由 updated() 的\n // `props.has('open')` 分支处理。\n this._presence.reconcile()\n this._syncScrollLock()\n this._reclaimIfOpen()\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._presence.dispose()\n this._scrollLock.release()\n // 断连即撤销登记:层不留在全局注册表里,重连后由 _reclaimIfOpen 显式重新声明。\n this._handle?.release()\n this._handle = null\n }\n\n /*\n * 「重挂载恢复」的调用方一半:模块刻意不观察 DOM 连接状态,所以断连撤销登记之后,\n * 重连时仍有开启态就必须由组件重新声明。\n */\n private _reclaimIfOpen() {\n if (!this.open || this._handle) return\n const dialog = this.dialog\n if (!dialog) return\n this._handle = this._overlay.claim(dialog, { ancestryFrom: this })\n this._syncOverlayInert()\n }\n\n /*\n * `no-escape-close` 时 dialog 仍是候选(Escape 被吞掉、原生 cancel 被压掉),只是\n * 不走关闭入口。与旧的「跳过候选」语义不同:旧语义放任事件落到下层浮层,把外层\n * 一起关掉。该属性可在开启期间改写,所以每次渲染后读它,而不是在 claim 时固定。\n */\n private _syncOverlayInert() {\n this._handle?.setInert(this.noEscapeClose)\n }\n\n showModal() {\n if (this.open) return\n this.open = true\n }\n\n close() {\n this.open = false\n }\n\n /*\n * 仲裁者在 capture 阶段 preventDefault 了 Escape 的 keydown,UA 因此不再派发原生\n * cancel,但本 handler 仍有两个仲裁者不负责的职责,**不可删**:\n * ① 任何到达 dialog 的 cancel 都要 preventDefault,把 top layer 保留到视觉退场结束\n * (否则原生关闭会跳过退出动画);\n * ② 忽略子控件(例如 file input)冒泡上来的 cancel。\n * 实测(overlay-open-owner-260918,2026-09-18)摘掉 `@cancel` 后 dialog.browser.spec.ts\n * 的「no-escape-close 存在时 Escape/cancel 不关闭对话框」与 remount-reconcile 的\n * 「打开态被移出文档再接回」两例立刻转红,断言正是 `dispatchEvent(cancel) === false`。\n */\n private handleCancel(e: Event) {\n if (e.target !== e.currentTarget) return\n\n e.preventDefault()\n if (this.noEscapeClose) return\n this._closeFromUser()\n }\n\n private handleBackdropClick(e: MouseEvent) {\n if (e.target !== (e.currentTarget as HTMLDialogElement)) return\n if (this.noBackdropClose) return\n this._closeFromUser()\n }\n\n /** 用户发起的关闭入口(Escape、遮罩点击、关闭按钮)共用一条路径。 */\n private readonly _closeFromUser = () => {\n if (this.controlled) {\n this.emitOpenChange(false)\n return\n }\n this._userOpenChange.mark()\n this.close()\n }\n\n // controlled 下派发关闭请求:detail 固定 false(用户只能请求关闭,打开永远由 Consumer 写)。\n private emitOpenChange(open = this.open) {\n dispatchOpenChangeEvent(this, open)\n }\n\n private _onTransitionEnd = (event: TransitionEvent) => {\n this._presence.handleTransitionEnd(event)\n }\n\n private _onNativeClose = () => {\n // 关闭态下的 close 事件只可能是我们自己 finishClosing 排队的异步事件(或冗余的\n // 外部关闭):交给 presence 消费 self-close 标志,避免标志泄漏到下一次真实关闭。\n if (!this.open) {\n this._presence.handleNativeClose()\n return\n }\n\n // 我们自己 dialog.close() 排队的 close 事件是异步任务:正常时序在本关闭会话内\n // 到达,快速「关闭→重开」时它在重新打开之后才到达(过期事件)。两种情况都已\n // 完成清理,由 presence 消费并返回 true,不能当成外部关闭把刚重开的 dialog 关掉。\n if (this._presence.handleNativeClose()) return\n\n // controlled 下原生关闭(如表单 method=\"dialog\")视为未经 Consumer 批准的状态丢失:\n // 恢复受控状态并派发关闭请求,由 Consumer 决定是否关闭。\n if (this.controlled) {\n this._presence.sync(true)\n this.emitOpenChange(false)\n return\n }\n\n this._userOpenChange.mark()\n this.open = false\n }\n\n private _syncScrollLock(isOpen = this.open) {\n this._scrollLock.sync(isOpen && !this.noScrollLock)\n }\n\n private _onBodySlotChange(e: Event) {\n if (!(e.target instanceof HTMLSlotElement)) return\n this._hasBody = e.target.assignedElements().length > 0\n }\n\n /**\n * 两种内容模式共用同一枚关闭按钮:都绝对定位到卡片右上角,偏移由\n * `--wui-dialog-close-top` / `--wui-dialog-close-right` 控制(见 style.css)。\n * 标题模式只是把它放在 `.title-row` 的 DOM 位置,定位参照仍是卡片本身。\n */\n private _renderCloseButton() {\n return html`\n <web-ui-button\n class=\"wui-dialog-close\"\n @click=${this._closeFromUser}\n aria-label=\"关闭\"\n variant=\"secondary\"\n icon\n size=\"26\"\n >\n <web-ui-icon size=\"14\" .icon=${oouiClose}></web-ui-icon>\n </web-ui-button>\n `\n }\n\n override render() {\n return html`\n <dialog\n @cancel=${this.handleCancel}\n @close=${this._onNativeClose}\n @click=${this.handleBackdropClick}\n @transitionend=${this._onTransitionEnd}\n >\n <div class=\"wui-dialog-body wui-glass\">\n ${\n this._hasBody\n ? html`\n <div class=\"wui-dialog-content\">\n <slot name=\"body\" @slotchange=${this._onBodySlotChange}></slot>\n </div>\n ${this.closable ? this._renderCloseButton() : nothing}\n `\n : html`\n <slot name=\"body\" @slotchange=${this._onBodySlotChange} hidden></slot>\n ${\n this.closable\n ? html`\n <div class=\"title-row\">\n <div class=\"title\"><slot name=\"title\"></slot></div>\n ${this._renderCloseButton()}\n </div>\n `\n : html`<div class=\"title\"><slot name=\"title\"></slot></div>`\n }\n <div class=\"desc\"><slot></slot></div>\n <div class=\"wui-dialog-footer\"><slot name=\"footer\"></slot></div>\n `\n }\n </div>\n </dialog>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-dialog': WebUiDialog\n }\n}\n"],"mappings":"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"}