@greypan/web-ui 12.1.0 → 12.2.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 (49) hide show
  1. package/README.CN.md +2 -0
  2. package/README.md +2 -0
  3. package/dist/components/autocomplete/index.d.ts.map +1 -1
  4. package/dist/components/autocomplete/index.js +1 -1
  5. package/dist/components/autocomplete/index.js.map +1 -1
  6. package/dist/components/drawer/index.d.ts +11 -0
  7. package/dist/components/drawer/index.d.ts.map +1 -1
  8. package/dist/components/drawer/index.js +23 -1
  9. package/dist/components/drawer/index.js.map +1 -1
  10. package/dist/components/drawer/page-shift.js +113 -0
  11. package/dist/components/drawer/page-shift.js.map +1 -0
  12. package/dist/components/grouped-text/grouping.d.ts +25 -0
  13. package/dist/components/grouped-text/grouping.d.ts.map +1 -0
  14. package/dist/components/grouped-text/grouping.js +147 -0
  15. package/dist/components/grouped-text/grouping.js.map +1 -0
  16. package/dist/components/grouped-text/index.d.ts +36 -0
  17. package/dist/components/grouped-text/index.d.ts.map +1 -0
  18. package/dist/components/grouped-text/index.js +28 -0
  19. package/dist/components/grouped-text/index.js.map +1 -0
  20. package/dist/components/grouped-text/style.js +5 -0
  21. package/dist/components/grouped-text/style.js.map +1 -0
  22. package/dist/components/index.d.ts +4 -1
  23. package/dist/components/index.d.ts.map +1 -1
  24. package/dist/components/index.js +3 -1
  25. package/dist/components/layout/index.d.ts.map +1 -1
  26. package/dist/components/layout/index.js +1 -0
  27. package/dist/components/layout/index.js.map +1 -1
  28. package/dist/components/middle-ellipsis/index.js +1 -1
  29. package/dist/components/popover/index.d.ts +5 -2
  30. package/dist/components/popover/index.d.ts.map +1 -1
  31. package/dist/components/popover/index.js +31 -20
  32. package/dist/components/popover/index.js.map +1 -1
  33. package/dist/components/shake/index.d.ts +43 -0
  34. package/dist/components/shake/index.d.ts.map +1 -0
  35. package/dist/components/shake/index.js +103 -0
  36. package/dist/components/shake/index.js.map +1 -0
  37. package/dist/components/tooltip/index.d.ts +8 -0
  38. package/dist/components/tooltip/index.d.ts.map +1 -1
  39. package/dist/components/tooltip/index.js +59 -9
  40. package/dist/components/tooltip/index.js.map +1 -1
  41. package/dist/shared/overlay/anchored-panel.js +2 -2
  42. package/dist/shared/overlay/anchored-panel.js.map +1 -1
  43. package/dist/shared/overlay/presence.js +2 -2
  44. package/dist/shared/overlay/presence.js.map +1 -1
  45. package/dist/shared/overlay/trigger-props.d.ts +10 -0
  46. package/dist/shared/overlay/trigger-props.d.ts.map +1 -0
  47. package/dist/shared/overlay/trigger-props.js +42 -0
  48. package/dist/shared/overlay/trigger-props.js.map +1 -0
  49. package/package.json +1 -1
@@ -8,15 +8,13 @@ import { defineOpenOverlay } from "../../shared/overlay/open-overlay.js";
8
8
  import { FLOATING_PLACEMENTS } from "../../shared/overlay/placement-props.js";
9
9
  import { defineOverlayPortal } from "../../shared/overlay/portal.js";
10
10
  import { UserChangeController } from "../../shared/events/user-change.js";
11
+ import { hasTriggerMode, normalizeTriggerSet } from "../../shared/overlay/trigger-props.js";
11
12
  import style_default from "./style.js";
12
13
  import { LitElement, html, unsafeCSS } from "lit";
13
14
  import { customElement, property } from "lit/decorators.js";
14
15
  // src/components/popover/index.ts
15
- const ALLOWED_TRIGGERS = [
16
- "click",
17
- "hover",
18
- "manual"
19
- ];
16
+ /** 点击 trigger 开合,点 panel 外、失焦与 Escape 都能收。 */
17
+ const DEFAULT_TRIGGERS = ["click"];
20
18
  let popoverIdCounter = 0;
21
19
  let WebUiPopover = class WebUiPopover extends LitElement {
22
20
  constructor(..._args) {
@@ -25,7 +23,7 @@ let WebUiPopover = class WebUiPopover extends LitElement {
25
23
  this.disabled = false;
26
24
  this._placement = "bottom";
27
25
  this._offset = 8;
28
- this._trigger = "click";
26
+ this._trigger = DEFAULT_TRIGGERS.join(" ");
29
27
  this.portal = false;
30
28
  this._userOpenChange = new UserChangeController();
31
29
  this._shouldOpenInstantly = true;
@@ -53,21 +51,30 @@ let WebUiPopover = class WebUiPopover extends LitElement {
53
51
  if (this.disabled) return;
54
52
  clearTimeout(this._showTimer);
55
53
  clearTimeout(this._hideTimer);
56
- if (this.trigger === "hover") return;
54
+ if (!hasTriggerMode(this.trigger, "click") && !hasTriggerMode(this.trigger, "manual")) return;
57
55
  if (!this.open) this._shouldOpenInstantly = event.detail === 0;
58
56
  this._userOpenChange.mark();
59
57
  this.toggle();
60
58
  };
61
59
  this._onClickOutside = (e) => {
62
60
  if (!this.open) return;
63
- if (this.trigger === "manual" || this.trigger === "hover") return;
61
+ if (!hasTriggerMode(this.trigger, "click")) return;
64
62
  if (this._panel.getHandle()?.containsEvent(e)) return;
65
63
  if (this._isInsideShadowRoot(e)) return;
66
64
  this._userOpenChange.mark();
67
65
  this.open = false;
68
66
  };
67
+ this._onFocusIn = (event) => {
68
+ if (this.disabled) return;
69
+ if (this.open) return;
70
+ const focusedElement = event.composedPath()[0] ?? event.target;
71
+ if (!(focusedElement instanceof Element) || !focusedElement.matches(":focus-visible")) return;
72
+ this._shouldOpenInstantly = true;
73
+ this._userOpenChange.mark();
74
+ this.show();
75
+ };
69
76
  this._onFocusOut = () => {
70
- if (this.trigger === "manual" || this.trigger === "hover") return;
77
+ if (!hasTriggerMode(this.trigger, "click") && !hasTriggerMode(this.trigger, "focus")) return;
71
78
  this._overlay.scheduleFrame(() => {
72
79
  if (!this.matches(":focus-within") && !this._panel.getHandle()?.hasFocusWithin()) {
73
80
  this._userOpenChange.mark();
@@ -77,7 +84,7 @@ let WebUiPopover = class WebUiPopover extends LitElement {
77
84
  };
78
85
  this._onPointerEnter = (e) => {
79
86
  if (e.pointerType === "touch") return;
80
- if (this.disabled || this.trigger !== "hover") return;
87
+ if (this.disabled || !hasTriggerMode(this.trigger, "hover")) return;
81
88
  clearTimeout(this._hideTimer);
82
89
  this._showTimer = setTimeout(() => {
83
90
  this._shouldOpenInstantly = false;
@@ -88,7 +95,7 @@ let WebUiPopover = class WebUiPopover extends LitElement {
88
95
  };
89
96
  this._onPointerLeave = (e) => {
90
97
  if (e.pointerType === "touch") return;
91
- if (this.disabled || this.trigger !== "hover") return;
98
+ if (this.disabled || !hasTriggerMode(this.trigger, "hover")) return;
92
99
  clearTimeout(this._showTimer);
93
100
  this._hideTimer = setTimeout(() => {
94
101
  if (!this.open) return;
@@ -98,12 +105,12 @@ let WebUiPopover = class WebUiPopover extends LitElement {
98
105
  };
99
106
  this._onPanelPointerEnter = (e) => {
100
107
  if (e.pointerType === "touch") return;
101
- if (this.trigger !== "hover") return;
108
+ if (!hasTriggerMode(this.trigger, "hover")) return;
102
109
  clearTimeout(this._hideTimer);
103
110
  };
104
111
  this._onPanelPointerLeave = (e) => {
105
112
  if (e.pointerType === "touch") return;
106
- if (this.trigger !== "hover") return;
113
+ if (!hasTriggerMode(this.trigger, "hover")) return;
107
114
  this._hideTimer = setTimeout(() => {
108
115
  if (!this.open) return;
109
116
  this._userOpenChange.mark();
@@ -139,7 +146,7 @@ let WebUiPopover = class WebUiPopover extends LitElement {
139
146
  }
140
147
  set trigger(v) {
141
148
  const old = this._trigger;
142
- this._trigger = normalizeLiteral(v, ALLOWED_TRIGGERS, "click");
149
+ this._trigger = normalizeTriggerSet(v, DEFAULT_TRIGGERS);
143
150
  this.requestUpdate("trigger", old);
144
151
  }
145
152
  get isOpen() {
@@ -165,8 +172,7 @@ let WebUiPopover = class WebUiPopover extends LitElement {
165
172
  super.disconnectedCallback();
166
173
  document.removeEventListener("click", this._onClickOutside);
167
174
  this.removeEventListener("focusout", this._onFocusOut);
168
- this.removeEventListener("pointerenter", this._onPointerEnter);
169
- this.removeEventListener("pointerleave", this._onPointerLeave);
175
+ this._clearGestureListeners();
170
176
  this._overlay.suspend();
171
177
  clearTimeout(this._showTimer);
172
178
  clearTimeout(this._hideTimer);
@@ -195,8 +201,7 @@ let WebUiPopover = class WebUiPopover extends LitElement {
195
201
  }
196
202
  }
197
203
  if (changed.has("trigger")) {
198
- this.removeEventListener("pointerenter", this._onPointerEnter);
199
- this.removeEventListener("pointerleave", this._onPointerLeave);
204
+ this._clearGestureListeners();
200
205
  clearTimeout(this._showTimer);
201
206
  clearTimeout(this._hideTimer);
202
207
  this._syncTriggerListeners();
@@ -220,13 +225,19 @@ let WebUiPopover = class WebUiPopover extends LitElement {
220
225
  return false;
221
226
  }
222
227
  _syncTriggerListeners() {
223
- if (this.trigger === "hover") {
228
+ if (hasTriggerMode(this.trigger, "hover")) {
224
229
  this.addEventListener("pointerenter", this._onPointerEnter);
225
230
  this.addEventListener("pointerleave", this._onPointerLeave);
226
231
  }
232
+ if (hasTriggerMode(this.trigger, "focus")) this.addEventListener("focusin", this._onFocusIn);
233
+ }
234
+ _clearGestureListeners() {
235
+ this.removeEventListener("pointerenter", this._onPointerEnter);
236
+ this.removeEventListener("pointerleave", this._onPointerLeave);
237
+ this.removeEventListener("focusin", this._onFocusIn);
227
238
  }
228
239
  _syncOverlayInert() {
229
- this._panel.getHandle()?.setInert(this.trigger === "manual");
240
+ this._panel.getHandle()?.setInert(hasTriggerMode(this.trigger, "manual"));
230
241
  }
231
242
  _openOverlay(isInstant = false) {
232
243
  this._panel.open(isInstant);
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":["glass","overlayMotion","style"],"sources":["../../../src/components/popover/index.ts"],"sourcesContent":["import type { Placement } from '@floating-ui/dom'\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport overlayMotion from '@/assets/overlay-motion.css?inline'\nimport glass from '@/classes/glass.css?inline'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { normalizeLiteral, normalizeNumber } from '@/shared/normalize'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineAnchoredPanel } from '@/shared/overlay/anchored-panel'\nimport { defineOpenOverlay } from '@/shared/overlay/open-overlay'\nimport { FLOATING_PLACEMENTS } from '@/shared/overlay/placement-props'\nimport { defineOverlayPortal, type OverlayContainer, type OverlayPortal } from '@/shared/overlay/portal'\n\nimport style from './style.css?inline'\n\nconst ALLOWED_TRIGGERS = ['click', 'hover', 'manual'] as const\n\nlet popoverIdCounter = 0\n\n@customElement('web-ui-popover')\nexport class WebUiPopover extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(overlayMotion), unsafeCSS(style)]\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) disabled = false\n\n @property({ type: String, reflect: true })\n get placement(): Placement {\n return this._placement\n }\n set placement(v: string) {\n const old = this._placement\n this._placement = normalizeLiteral(v, FLOATING_PLACEMENTS, 'bottom')\n this.requestUpdate('placement', old)\n }\n private _placement: Placement = 'bottom'\n\n @property({ type: Number })\n get offset(): number {\n return this._offset\n }\n set offset(v: number) {\n const old = this._offset\n this._offset = normalizeNumber(v, 0, 100, 8)\n this.requestUpdate('offset', old)\n }\n private _offset = 8\n\n @property({ type: String, reflect: true })\n get trigger(): 'click' | 'hover' | 'manual' {\n return this._trigger\n }\n set trigger(v: string) {\n const old = this._trigger\n this._trigger = normalizeLiteral(v, ALLOWED_TRIGGERS, 'click')\n this.requestUpdate('trigger', old)\n }\n private _trigger: 'click' | 'hover' | 'manual' = 'click'\n\n @property({ type: Boolean, reflect: true }) portal = false\n @property({ attribute: false }) overlayContainer?: OverlayContainer\n\n private _showTimer?: ReturnType<typeof setTimeout>\n private _hideTimer?: ReturnType<typeof setTimeout>\n private readonly _userOpenChange = new UserChangeController()\n private _shouldOpenInstantly = true\n\n private _panelId = `wui-popover-panel-${++popoverIdCounter}`\n private _portal?: OverlayPortal\n /**\n * 开启态 overlay(issue #120 Block 1):本组件不自行监听 keydown,只声明「我开着」,\n * 由唯一仲裁者决定一次 Escape 关谁。实例作用域的帧事务入口也在这里\n * —— 它的 lifetime 长于一次开合(跨 `suspend()`/`resume()`),\n * 但不长于实例本身。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => {\n this._userOpenChange.mark()\n this.open = false\n },\n isConnected: () => this.isConnected\n })\n private readonly _panel = defineAnchoredPanel().make({\n getAnchor: () => this.shadowRoot?.querySelector<HTMLElement>('.popover-trigger') ?? null,\n getLocalPanel: () => this.shadowRoot?.querySelector<HTMLElement>('.popover-panel') ?? null,\n getPositioning: () => ({\n placement: this.placement,\n offset: this.offset,\n strategy: this.portal ? 'fixed' : 'absolute'\n }),\n isPortal: () => this.portal,\n createPortal: () => this._createPortal(),\n openOverlay: this._overlay\n })\n\n get isOpen(): boolean {\n return this.open\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._overlay.resume()\n document.addEventListener('click', this._onClickOutside)\n this.addEventListener('focusout', this._onFocusOut)\n this._syncTriggerListeners()\n }\n\n override firstUpdated() {\n // trigger slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,\n // 由 updated() 的 ARIA 回写覆盖晚到的 trigger 元素。\n this.shadowRoot\n ?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n ?.addEventListener('slotchange', () => this.requestUpdate())\n\n if (this.open) {\n // 建立会话的那一帧没有 handle 可作 liveness 凭据:关闭路径会 invalidate()(见 updated),\n // 微任务先于 rAF 排空,因此该帧不可能跨过一次关闭执行。\n this._overlay.scheduleFrame(() => {\n this._openOverlay(this._shouldOpenInstantly)\n })\n this._shouldOpenInstantly = true\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n document.removeEventListener('click', this._onClickOutside)\n this.removeEventListener('focusout', this._onFocusOut)\n this.removeEventListener('pointerenter', this._onPointerEnter)\n this.removeEventListener('pointerleave', this._onPointerLeave)\n // 停止帧调度;撤销登记由 _panel.dispose() 完成(handle 归它持有)。\n this._overlay.suspend()\n clearTimeout(this._showTimer)\n clearTimeout(this._hideTimer)\n this._panel.dispose()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // ARIA 回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerAria()\n\n if (changed.has('portal') || changed.has('overlayContainer')) {\n // 同帧 open + reconfigure 可能排两个回调;先结束旧事务,让 reconfigure 成为本帧唯一入口。\n this._overlay.invalidate()\n this._overlay.scheduleFrame(() => this._reconfigureOverlay())\n } else if (changed.has('placement') || changed.has('offset'))\n requestAnimationFrame(() => this._panel.updatePosition())\n\n if (changed.has('open')) {\n if (this.open) {\n const isInstant = this._shouldOpenInstantly\n this._shouldOpenInstantly = true\n this._overlay.scheduleFrame(() => {\n this._openOverlay(isInstant)\n })\n if (this._userOpenChange.consume()) this._dispatchChange(true)\n this._focusPanel()\n } else {\n this._overlay.invalidate()\n this._returnFocus()\n void this._closeOverlay()\n if (this._userOpenChange.consume()) this._dispatchChange(false)\n }\n }\n\n if (changed.has('trigger')) {\n this.removeEventListener('pointerenter', this._onPointerEnter)\n this.removeEventListener('pointerleave', this._onPointerLeave)\n clearTimeout(this._showTimer)\n clearTimeout(this._hideTimer)\n this._syncTriggerListeners()\n }\n this._syncOverlayInert()\n }\n\n show() {\n if (this.disabled || this.open) return\n this.open = true\n }\n\n close() {\n if (!this.open) return\n this.open = false\n }\n\n toggle() {\n if (this.open) this.close()\n else this.show()\n }\n\n private _isInsideShadowRoot(e: MouseEvent): boolean {\n for (const node of e.composedPath()) {\n if (node instanceof Node && node.getRootNode() === this.shadowRoot) return true\n }\n return false\n }\n\n private _syncTriggerListeners() {\n if (this.trigger === 'hover') {\n this.addEventListener('pointerenter', this._onPointerEnter)\n this.addEventListener('pointerleave', this._onPointerLeave)\n }\n }\n\n /*\n * `manual` trigger 的 popover 不由用户关闭,但仍是候选:Escape 被它吞掉(同时压掉\n * 原生 cancel),只是不走关闭入口:Escape 不落到下层 overlay,不会把外层一起关掉。\n * trigger 可在开启期间改写,所以走动态通道同步。\n */\n private _syncOverlayInert() {\n this._panel.getHandle()?.setInert(this.trigger === 'manual')\n }\n\n private _openOverlay(isInstant = false) {\n /*\n * 登记与仲裁归属由 anchored panel 完成(claim 要求 panel 已就位,祖先链才判得对)。\n * 不必再以宿主为 owner 额外登记一次:anchor 是宿主 shadow 内的\n * `.popover-trigger` 包装 div(不是会被 slot 重定向的 trigger 内容),其祖先链上\n * 首个已开启 overlay 与从宿主上溯结果一致,多出的那次登记是冗余的。\n */\n this._panel.open(isInstant)\n this._syncOverlayInert()\n }\n\n private _migratableContentNodes(nodes: Node[]): Node[] {\n // 框架注释锚点(v-if/v-for 占位)必须留在宿主:panel 内容是实时渲染契约,\n // 框架后续 patch 以宿主内锚点为插入基准,锚点进 panel 会让插入落空。\n return nodes.filter(node => !(node instanceof Comment) && !(node instanceof HTMLElement && node.slot === 'trigger'))\n }\n\n private _createPortal(): OverlayPortal {\n const portal = defineOverlayPortal().make({\n container: this.overlayContainer,\n target: this,\n style: `${glass}\\n${overlayMotion}\\n${style}`,\n className: 'popover-panel portal wui-glass wui-floating-panel',\n // 已迁移节点被框架删除后的解除追踪是 portal 内建默认行为,这里无需 onContentChange\n migrateAddedNodes: addedNodes => this._migratableContentNodes(addedNodes)\n })\n this._portal = portal\n portal.panel.id = this._panelId\n portal.panel.setAttribute('role', 'dialog')\n portal.panel.tabIndex = -1\n portal.panel.addEventListener('pointerenter', this._onPanelPointerEnter)\n portal.panel.addEventListener('pointerleave', this._onPanelPointerLeave)\n portal.moveContent(this._migratableContentNodes(Array.from(this.childNodes)))\n return portal\n }\n\n private async _closeOverlay() {\n await this._panel.close(() => this.open)\n }\n\n private _reconfigureOverlay() {\n // portal 变更也会登记 rAF;宿主卸载后不得再通过 reconfigure 重建 panel。\n if (!this.isConnected) return\n this._panel.reconfigure(this.open)\n /*\n * reconfigure 重新 claim = 新会话,inert 回到 false。这里的重推是**承重的**,不是\n * 防御性备份:`reconfigure` 不触发 Lit 渲染,`updated()` 那次同步不会跟着来。\n * 实测摘掉本行,「portal 变更后 Escape 仍不关闭」立刻转红(select / autocomplete 的\n * 同类重推则被各自的渲染掩盖,摘掉仍绿)。\n */\n this._syncOverlayInert()\n }\n\n private _dispatchChange(open: boolean) {\n dispatchOpenChangeEvent(this, open)\n }\n\n /*\n * trigger 包装 div 不可聚焦,AT 读不到其 aria 状态:把 aria-expanded /\n * aria-controls 同步回写到 trigger slot 的首个 assigned element(Q8a)。\n * 包装 div 上的同名属性保留(additive,兼容既有查询)。\n */\n private _syncTriggerAria() {\n const trigger = this._queryTrigger()\n if (!trigger) return\n trigger.setAttribute('aria-expanded', String(this.open))\n trigger.setAttribute('aria-controls', this._panelId)\n }\n\n private _focusPanel() {\n this._overlay.scheduleFrame(() => {\n const panel = this._panel.getPanel()\n const autofocus = panel?.querySelector<HTMLElement>('[autofocus]')\n if (autofocus && !autofocus.matches(':disabled, [disabled]')) autofocus.focus()\n })\n }\n\n private _returnFocus() {\n const panel = this._panel.getPanel()\n if (!panel?.matches(':focus-within')) return\n const trigger = this._queryTrigger()\n trigger?.focus()\n }\n\n private _queryTrigger(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n clearTimeout(this._showTimer)\n clearTimeout(this._hideTimer)\n if (this.trigger === 'hover') return\n if (!this.open) this._shouldOpenInstantly = event.detail === 0\n this._userOpenChange.mark()\n this.toggle()\n }\n\n private _onClickOutside = (e: MouseEvent) => {\n if (!this.open) return\n if (this.trigger === 'manual' || this.trigger === 'hover') return\n // 在监听器内部判定:composedPath() 在派发结束后会被清空。\n if (this._panel.getHandle()?.containsEvent(e)) return\n if (this._isInsideShadowRoot(e)) return\n this._userOpenChange.mark()\n this.open = false\n }\n\n private _onFocusOut = () => {\n if (this.trigger === 'manual' || this.trigger === 'hover') return\n\n this._overlay.scheduleFrame(() => {\n if (!this.matches(':focus-within') && !this._panel.getHandle()?.hasFocusWithin()) {\n this._userOpenChange.mark()\n this.open = false\n }\n })\n }\n\n private _onPointerEnter = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (this.disabled || this.trigger !== 'hover') return\n clearTimeout(this._hideTimer)\n this._showTimer = setTimeout(() => {\n this._shouldOpenInstantly = false\n if (this.open) return\n this._userOpenChange.mark()\n this.show()\n }, 100)\n }\n\n private _onPointerLeave = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (this.disabled || this.trigger !== 'hover') return\n clearTimeout(this._showTimer)\n this._hideTimer = setTimeout(() => {\n if (!this.open) return\n this._userOpenChange.mark()\n this.close()\n }, 100)\n }\n\n private _onPanelPointerEnter = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (this.trigger !== 'hover') return\n clearTimeout(this._hideTimer)\n }\n\n private _onPanelPointerLeave = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (this.trigger !== 'hover') return\n this._hideTimer = setTimeout(() => {\n if (!this.open) return\n this._userOpenChange.mark()\n this.close()\n }, 100)\n }\n\n override render() {\n return html`\n <div class=\"popover-anchor\">\n <div\n class=\"popover-trigger\"\n aria-expanded=${String(this.open)}\n aria-controls=${this._panelId}\n @click=${this._onTriggerClick}\n >\n <slot name=\"trigger\"></slot>\n </div>\n <div\n id=${this._panelId}\n class=\"popover-panel wui-floating-panel wui-glass\"\n hidden\n role=\"dialog\"\n tabindex=\"-1\"\n @pointerenter=${this._onPanelPointerEnter}\n @pointerleave=${this._onPanelPointerLeave}\n >\n <slot></slot>\n </div>\n </div>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-popover': WebUiPopover\n }\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":["glass","overlayMotion","style"],"sources":["../../../src/components/popover/index.ts"],"sourcesContent":["import type { Placement } from '@floating-ui/dom'\nimport { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property } from 'lit/decorators.js'\n\nimport overlayMotion from '@/assets/overlay-motion.css?inline'\nimport glass from '@/classes/glass.css?inline'\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { normalizeLiteral, normalizeNumber } from '@/shared/normalize'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { defineAnchoredPanel } from '@/shared/overlay/anchored-panel'\nimport { defineOpenOverlay } from '@/shared/overlay/open-overlay'\nimport { FLOATING_PLACEMENTS } from '@/shared/overlay/placement-props'\nimport { defineOverlayPortal, type OverlayContainer, type OverlayPortal } from '@/shared/overlay/portal'\nimport { hasTriggerMode, normalizeTriggerSet, type OverlayTriggerMode } from '@/shared/overlay/trigger-props'\n\nimport style from './style.css?inline'\n\n/** 点击 trigger 开合,点 panel 外、失焦与 Escape 都能收。 */\nconst DEFAULT_TRIGGERS = ['click'] as const\n\nlet popoverIdCounter = 0\n\n@customElement('web-ui-popover')\nexport class WebUiPopover extends LitElement {\n static override styles = [unsafeCSS(glass), unsafeCSS(overlayMotion), unsafeCSS(style)]\n\n @property({ type: Boolean, reflect: true }) open = false\n @property({ type: Boolean, reflect: true }) disabled = false\n\n @property({ type: String, reflect: true })\n get placement(): Placement {\n return this._placement\n }\n set placement(v: string) {\n const old = this._placement\n this._placement = normalizeLiteral(v, FLOATING_PLACEMENTS, 'bottom')\n this.requestUpdate('placement', old)\n }\n private _placement: Placement = 'bottom'\n\n @property({ type: Number })\n get offset(): number {\n return this._offset\n }\n set offset(v: number) {\n const old = this._offset\n this._offset = normalizeNumber(v, 0, 100, 8)\n this.requestUpdate('offset', old)\n }\n private _offset = 8\n\n @property({ type: String, reflect: true })\n get trigger(): string {\n return this._trigger\n }\n set trigger(v: string | readonly OverlayTriggerMode[]) {\n const old = this._trigger\n this._trigger = normalizeTriggerSet(v, DEFAULT_TRIGGERS)\n this.requestUpdate('trigger', old)\n }\n private _trigger: string = DEFAULT_TRIGGERS.join(' ')\n\n @property({ type: Boolean, reflect: true }) portal = false\n @property({ attribute: false }) overlayContainer?: OverlayContainer\n\n private _showTimer?: ReturnType<typeof setTimeout>\n private _hideTimer?: ReturnType<typeof setTimeout>\n private readonly _userOpenChange = new UserChangeController()\n private _shouldOpenInstantly = true\n\n private _panelId = `wui-popover-panel-${++popoverIdCounter}`\n private _portal?: OverlayPortal\n /**\n * 开启态 overlay(issue #120 Block 1):本组件不自行监听 keydown,只声明「我开着」,\n * 由唯一仲裁者决定一次 Escape 关谁。实例作用域的帧事务入口也在这里\n * —— 它的 lifetime 长于一次开合(跨 `suspend()`/`resume()`),\n * 但不长于实例本身。\n */\n private readonly _overlay = defineOpenOverlay().make({\n requestClose: () => {\n this._userOpenChange.mark()\n this.open = false\n },\n isConnected: () => this.isConnected\n })\n private readonly _panel = defineAnchoredPanel().make({\n getAnchor: () => this.shadowRoot?.querySelector<HTMLElement>('.popover-trigger') ?? null,\n getLocalPanel: () => this.shadowRoot?.querySelector<HTMLElement>('.popover-panel') ?? null,\n getPositioning: () => ({\n placement: this.placement,\n offset: this.offset,\n strategy: this.portal ? 'fixed' : 'absolute'\n }),\n isPortal: () => this.portal,\n createPortal: () => this._createPortal(),\n openOverlay: this._overlay\n })\n\n get isOpen(): boolean {\n return this.open\n }\n\n override connectedCallback() {\n super.connectedCallback()\n this._overlay.resume()\n document.addEventListener('click', this._onClickOutside)\n this.addEventListener('focusout', this._onFocusOut)\n this._syncTriggerListeners()\n }\n\n override firstUpdated() {\n // trigger slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,\n // 由 updated() 的 ARIA 回写覆盖晚到的 trigger 元素。\n this.shadowRoot\n ?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n ?.addEventListener('slotchange', () => this.requestUpdate())\n\n if (this.open) {\n // 建立会话的那一帧没有 handle 可作 liveness 凭据:关闭路径会 invalidate()(见 updated),\n // 微任务先于 rAF 排空,因此该帧不可能跨过一次关闭执行。\n this._overlay.scheduleFrame(() => {\n this._openOverlay(this._shouldOpenInstantly)\n })\n this._shouldOpenInstantly = true\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n document.removeEventListener('click', this._onClickOutside)\n this.removeEventListener('focusout', this._onFocusOut)\n this._clearGestureListeners()\n // 停止帧调度;撤销登记由 _panel.dispose() 完成(handle 归它持有)。\n this._overlay.suspend()\n clearTimeout(this._showTimer)\n clearTimeout(this._hideTimer)\n this._panel.dispose()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // ARIA 回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerAria()\n\n if (changed.has('portal') || changed.has('overlayContainer')) {\n // 同帧 open + reconfigure 可能排两个回调;先结束旧事务,让 reconfigure 成为本帧唯一入口。\n this._overlay.invalidate()\n this._overlay.scheduleFrame(() => this._reconfigureOverlay())\n } else if (changed.has('placement') || changed.has('offset'))\n requestAnimationFrame(() => this._panel.updatePosition())\n\n if (changed.has('open')) {\n if (this.open) {\n const isInstant = this._shouldOpenInstantly\n this._shouldOpenInstantly = true\n this._overlay.scheduleFrame(() => {\n this._openOverlay(isInstant)\n })\n if (this._userOpenChange.consume()) this._dispatchChange(true)\n this._focusPanel()\n } else {\n this._overlay.invalidate()\n this._returnFocus()\n void this._closeOverlay()\n if (this._userOpenChange.consume()) this._dispatchChange(false)\n }\n }\n\n if (changed.has('trigger')) {\n this._clearGestureListeners()\n clearTimeout(this._showTimer)\n clearTimeout(this._hideTimer)\n this._syncTriggerListeners()\n }\n this._syncOverlayInert()\n }\n\n show() {\n if (this.disabled || this.open) return\n this.open = true\n }\n\n close() {\n if (!this.open) return\n this.open = false\n }\n\n toggle() {\n if (this.open) this.close()\n else this.show()\n }\n\n private _isInsideShadowRoot(e: MouseEvent): boolean {\n for (const node of e.composedPath()) {\n if (node instanceof Node && node.getRootNode() === this.shadowRoot) return true\n }\n return false\n }\n\n private _syncTriggerListeners() {\n if (hasTriggerMode(this.trigger, 'hover')) {\n this.addEventListener('pointerenter', this._onPointerEnter)\n this.addEventListener('pointerleave', this._onPointerLeave)\n }\n if (hasTriggerMode(this.trigger, 'focus')) this.addEventListener('focusin', this._onFocusIn)\n }\n\n private _clearGestureListeners() {\n this.removeEventListener('pointerenter', this._onPointerEnter)\n this.removeEventListener('pointerleave', this._onPointerLeave)\n this.removeEventListener('focusin', this._onFocusIn)\n }\n\n /*\n * `manual` trigger 的 popover 不由用户关闭,但仍是候选:Escape 被它吞掉(同时压掉\n * 原生 cancel),只是不走关闭入口:Escape 不落到下层 overlay,不会把外层一起关掉。\n * trigger 可在开启期间改写,所以走动态通道同步。\n */\n private _syncOverlayInert() {\n this._panel.getHandle()?.setInert(hasTriggerMode(this.trigger, 'manual'))\n }\n\n private _openOverlay(isInstant = false) {\n /*\n * 登记与仲裁归属由 anchored panel 完成(claim 要求 panel 已就位,祖先链才判得对)。\n * 不必再以宿主为 owner 额外登记一次:anchor 是宿主 shadow 内的\n * `.popover-trigger` 包装 div(不是会被 slot 重定向的 trigger 内容),其祖先链上\n * 首个已开启 overlay 与从宿主上溯结果一致,多出的那次登记是冗余的。\n */\n this._panel.open(isInstant)\n this._syncOverlayInert()\n }\n\n private _migratableContentNodes(nodes: Node[]): Node[] {\n // 框架注释锚点(v-if/v-for 占位)必须留在宿主:panel 内容是实时渲染契约,\n // 框架后续 patch 以宿主内锚点为插入基准,锚点进 panel 会让插入落空。\n return nodes.filter(node => !(node instanceof Comment) && !(node instanceof HTMLElement && node.slot === 'trigger'))\n }\n\n private _createPortal(): OverlayPortal {\n const portal = defineOverlayPortal().make({\n container: this.overlayContainer,\n target: this,\n style: `${glass}\\n${overlayMotion}\\n${style}`,\n className: 'popover-panel portal wui-glass wui-floating-panel',\n // 已迁移节点被框架删除后的解除追踪是 portal 内建默认行为,这里无需 onContentChange\n migrateAddedNodes: addedNodes => this._migratableContentNodes(addedNodes)\n })\n this._portal = portal\n portal.panel.id = this._panelId\n portal.panel.setAttribute('role', 'dialog')\n portal.panel.tabIndex = -1\n portal.panel.addEventListener('pointerenter', this._onPanelPointerEnter)\n portal.panel.addEventListener('pointerleave', this._onPanelPointerLeave)\n portal.moveContent(this._migratableContentNodes(Array.from(this.childNodes)))\n return portal\n }\n\n private async _closeOverlay() {\n await this._panel.close(() => this.open)\n }\n\n private _reconfigureOverlay() {\n // portal 变更也会登记 rAF;宿主卸载后不得再通过 reconfigure 重建 panel。\n if (!this.isConnected) return\n this._panel.reconfigure(this.open)\n /*\n * reconfigure 重新 claim = 新会话,inert 回到 false。这里的重推是**承重的**,不是\n * 防御性备份:`reconfigure` 不触发 Lit 渲染,`updated()` 那次同步不会跟着来。\n * 实测摘掉本行,「portal 变更后 Escape 仍不关闭」立刻转红(select / autocomplete 的\n * 同类重推则被各自的渲染掩盖,摘掉仍绿)。\n */\n this._syncOverlayInert()\n }\n\n private _dispatchChange(open: boolean) {\n dispatchOpenChangeEvent(this, open)\n }\n\n /*\n * trigger 包装 div 不可聚焦,AT 读不到其 aria 状态:把 aria-expanded /\n * aria-controls 同步回写到 trigger slot 的首个 assigned element(Q8a)。\n * 包装 div 上的同名属性保留(additive,兼容既有查询)。\n */\n private _syncTriggerAria() {\n const trigger = this._queryTrigger()\n if (!trigger) return\n trigger.setAttribute('aria-expanded', String(this.open))\n trigger.setAttribute('aria-controls', this._panelId)\n }\n\n private _focusPanel() {\n this._overlay.scheduleFrame(() => {\n const panel = this._panel.getPanel()\n const autofocus = panel?.querySelector<HTMLElement>('[autofocus]')\n if (autofocus && !autofocus.matches(':disabled, [disabled]')) autofocus.focus()\n })\n }\n\n private _returnFocus() {\n const panel = this._panel.getPanel()\n if (!panel?.matches(':focus-within')) return\n this._queryTrigger()?.focus()\n }\n\n private _queryTrigger(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('slot[name=\"trigger\"]')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n clearTimeout(this._showTimer)\n clearTimeout(this._hideTimer)\n // manual 保留点击切换:它的语义是「不接受任何自动收起」,不是「不接受用户打开」。\n if (!hasTriggerMode(this.trigger, 'click') && !hasTriggerMode(this.trigger, 'manual')) return\n if (!this.open) this._shouldOpenInstantly = event.detail === 0\n this._userOpenChange.mark()\n this.toggle()\n }\n\n private _onClickOutside = (e: MouseEvent) => {\n if (!this.open) return\n if (!hasTriggerMode(this.trigger, 'click')) return\n // 在监听器内部判定:composedPath() 在派发结束后会被清空。\n if (this._panel.getHandle()?.containsEvent(e)) return\n if (this._isInsideShadowRoot(e)) return\n this._userOpenChange.mark()\n this.open = false\n }\n\n private _onFocusIn = (event: FocusEvent) => {\n if (this.disabled) return\n if (this.open) return\n\n const focusedElement = event.composedPath()[0] ?? event.target\n if (!(focusedElement instanceof Element) || !focusedElement.matches(':focus-visible')) return\n\n this._shouldOpenInstantly = true\n this._userOpenChange.mark()\n this.show()\n }\n\n private _onFocusOut = () => {\n // click 与 focus 都随焦点离开收起;hover 与 manual 不收起(后者不接受自动开合)。\n if (!hasTriggerMode(this.trigger, 'click') && !hasTriggerMode(this.trigger, 'focus')) return\n\n this._overlay.scheduleFrame(() => {\n if (!this.matches(':focus-within') && !this._panel.getHandle()?.hasFocusWithin()) {\n this._userOpenChange.mark()\n this.open = false\n }\n })\n }\n\n private _onPointerEnter = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (this.disabled || !hasTriggerMode(this.trigger, 'hover')) return\n clearTimeout(this._hideTimer)\n this._showTimer = setTimeout(() => {\n this._shouldOpenInstantly = false\n if (this.open) return\n this._userOpenChange.mark()\n this.show()\n }, 100)\n }\n\n private _onPointerLeave = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (this.disabled || !hasTriggerMode(this.trigger, 'hover')) return\n clearTimeout(this._showTimer)\n this._hideTimer = setTimeout(() => {\n if (!this.open) return\n this._userOpenChange.mark()\n this.close()\n }, 100)\n }\n\n private _onPanelPointerEnter = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (!hasTriggerMode(this.trigger, 'hover')) return\n clearTimeout(this._hideTimer)\n }\n\n private _onPanelPointerLeave = (e: PointerEvent) => {\n if (e.pointerType === 'touch') return\n if (!hasTriggerMode(this.trigger, 'hover')) return\n this._hideTimer = setTimeout(() => {\n if (!this.open) return\n this._userOpenChange.mark()\n this.close()\n }, 100)\n }\n\n override render() {\n return html`\n <div class=\"popover-anchor\">\n <div\n class=\"popover-trigger\"\n aria-expanded=${String(this.open)}\n aria-controls=${this._panelId}\n @click=${this._onTriggerClick}\n >\n <slot name=\"trigger\"></slot>\n </div>\n <div\n id=${this._panelId}\n class=\"popover-panel wui-floating-panel wui-glass\"\n hidden\n role=\"dialog\"\n tabindex=\"-1\"\n @pointerenter=${this._onPanelPointerEnter}\n @pointerleave=${this._onPanelPointerLeave}\n >\n <slot></slot>\n </div>\n </div>\n `\n }\n\n declare readonly $events: {\n 'open-change': CustomEvent<{ open: boolean }>\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-popover': WebUiPopover\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,43 @@
1
+ /** `shake()` 的可选项。除 `target` 外都有默认值,`shake()` 不带参数即可用。 */
2
+ export interface ShakeOptions {
3
+ /**
4
+ * 位移目标。省略时震整页(`document.documentElement`):页面里 in-flow 与 `position: fixed`
5
+ * 的内容一起跟着走,而 top layer 的 `<dialog>` 与它的 `::backdrop` 不受影响(Chromium 实测),
6
+ * 因此正在打开的 drawer / dialog 不会跟着晃。
7
+ */
8
+ target?: HTMLElement;
9
+ /**
10
+ * 横向首摆幅度(px)。省略时读目标计算样式上的 `--wui-shake-x-distance`。
11
+ * 震整页时该 token 要写在 `html` / `:root` 上:主题元素在 body 之下,它的声明继承
12
+ * 不到 `documentElement`(继承只往下走)。
13
+ */
14
+ xDistance?: number;
15
+ /**
16
+ * 纵向幅度(px),默认与横向同值。省略时读目标计算样式上的 `--wui-shake-y-distance`。
17
+ * 与横向同相位,所以合成位移沿一条直线、倾角由两个幅度决定;把横向置 `0` 就是纯上下摆,
18
+ * 把纵向置 `0` 就是纯横摆。
19
+ */
20
+ yDistance?: number;
21
+ /** 整段时长(ms)。省略时读目标计算样式上的 `--wui-shake-duration`。 */
22
+ duration?: number;
23
+ }
24
+ /** 横向幅度 token 的名字;注册项与 JS 读取共用这一个字符串。 */
25
+ export declare const SHAKE_X_DISTANCE_VARIABLE = "--wui-shake-x-distance";
26
+ /** 纵向幅度 token 的名字。 */
27
+ export declare const SHAKE_Y_DISTANCE_VARIABLE = "--wui-shake-y-distance";
28
+ export declare const SHAKE_X_DISTANCE_FALLBACK = 12;
29
+ export declare const SHAKE_Y_DISTANCE_FALLBACK = 12;
30
+ /**
31
+ * 让一次「不行 / 请先处理」的信号可见:目标摆几下后回到原位。
32
+ *
33
+ * 用于打断一次被拦下的操作(例如表单还有未保存内容却要退出),或把注意力拉回某个元素。
34
+ * 传 `target` 就只震那一块,不传就震整页。播放期间目标上已有的 `transform` 会被逐帧带上,
35
+ * 因此居中定位不会被震动吃掉。
36
+ *
37
+ * 两个幅度默认同值,所以不带参数是斜向摆;把某一轴设为 0 就只剩另一条轴,
38
+ * 两轴同相位所以合成轨迹始终是一条直线。
39
+ * 同一目标上重复调用会**重新开始**摆(撤掉上一条),而不是叠加成更大的位移;
40
+ * `prefers-reduced-motion`(系统或 `web-ui-theme` 的 motion 档位)下不产生任何位移。
41
+ */
42
+ export declare function shake(input?: HTMLElement | ShakeOptions): void;
43
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/components/shake/index.ts"],"mappings":";iBAIiB;;;;;;EAMf,SAAS;;;;;;EAMT;;;;;;EAMA;;EAEA;;;qBAIW;;qBAEA;qBASA;qBACA;;;;;;;;;;;;;wBAmHG,MAAM,QAAQ,cAAc"}
@@ -0,0 +1,103 @@
1
+ import { findRootTheme } from "../../shared/overlay/theme-overlay-scope.js";
2
+ import { prefersReducedMotion } from "../../shared/theme/reduced-motion.js";
3
+ // src/components/shake/index.ts
4
+ /** 横向幅度 token 的名字;注册项与 JS 读取共用这一个字符串。 */
5
+ const SHAKE_X_DISTANCE_VARIABLE = "--wui-shake-x-distance";
6
+ /** 纵向幅度 token 的名字。 */
7
+ const SHAKE_Y_DISTANCE_VARIABLE = "--wui-shake-y-distance";
8
+ const DURATION_PROPERTY = "--wui-shake-duration";
9
+ const SHAKE_X_DISTANCE_FALLBACK = 12;
10
+ const SHAKE_Y_DISTANCE_FALLBACK = 12;
11
+ const DURATION_FALLBACK = 420;
12
+ /** 挂在 Animation.id 上的标记,用于「同一次调用只保留一条震动」。 */
13
+ const SHAKE_ANIMATION_ID = "wui-shake";
14
+ const SHAKE_CYCLES = 3.5;
15
+ const SHAKE_SAMPLE_COUNT = 24;
16
+ if (typeof CSS !== "undefined" && "registerProperty" in CSS) for (const [name, initialValue] of [[SHAKE_X_DISTANCE_VARIABLE, "12px"], [SHAKE_Y_DISTANCE_VARIABLE, "12px"]]) try {
17
+ CSS.registerProperty({
18
+ name,
19
+ syntax: "<length>",
20
+ inherits: true,
21
+ initialValue
22
+ });
23
+ } catch {}
24
+ function readLength(target, property, fallback) {
25
+ const parsed = Number.parseFloat(getComputedStyle(target).getPropertyValue(property));
26
+ return Number.isFinite(parsed) ? parsed : fallback;
27
+ }
28
+ function readDuration(target, property, fallback) {
29
+ const raw = getComputedStyle(target).getPropertyValue(property).trim();
30
+ const value = Number.parseFloat(raw);
31
+ if (!Number.isFinite(value)) return fallback;
32
+ return raw.endsWith("ms") ? value : value * 1e3;
33
+ }
34
+ /**
35
+ * 目标所在范围是否要求减少动效。
36
+ *
37
+ * 指定 target 时按它自己的主题范围走。默认震整页时不能只看祖先:主题元素总在 body 之下,
38
+ * `documentElement` 往上走找不到它们,于是 `shake()` 会和页面里其它动效认的档位分叉。
39
+ * 这里改用**根主题**——与 toast / imagePreview 解析挂载根时同一个 `findRootTheme()`,
40
+ * 命令式入口的「页面级默认值」两条路径认的是同一个主题;没有主题范围时回退系统偏好。
41
+ */
42
+ function isReducedFor(target) {
43
+ if (target !== document.documentElement) return prefersReducedMotion(target);
44
+ const theme = findRootTheme();
45
+ return theme ? theme.isReducedMotion() : prefersReducedMotion(target);
46
+ }
47
+ function resolveOptions(input) {
48
+ const options = input instanceof HTMLElement ? { target: input } : input ?? {};
49
+ const target = options.target ?? document.documentElement;
50
+ return {
51
+ target,
52
+ xDistance: options.xDistance ?? readLength(target, "--wui-shake-x-distance", 12),
53
+ yDistance: options.yDistance ?? readLength(target, "--wui-shake-y-distance", 12),
54
+ duration: options.duration ?? readDuration(target, DURATION_PROPERTY, DURATION_FALLBACK)
55
+ };
56
+ }
57
+ /**
58
+ * 关键帧里写的是 `<transform-list>`,而计算值在没有变换时是 `none`——`none translate(-6px, 0px)`
59
+ * 不是合法的变换列表,整条关键帧会被丢弃。
60
+ */
61
+ function withBaseTransform(base, dx, dy) {
62
+ const translate = `translate(${dx.toFixed(3)}px, ${dy.toFixed(3)}px)`;
63
+ return base === "none" ? translate : `${base} ${translate}`;
64
+ }
65
+ /**
66
+ * 让一次「不行 / 请先处理」的信号可见:目标摆几下后回到原位。
67
+ *
68
+ * 用于打断一次被拦下的操作(例如表单还有未保存内容却要退出),或把注意力拉回某个元素。
69
+ * 传 `target` 就只震那一块,不传就震整页。播放期间目标上已有的 `transform` 会被逐帧带上,
70
+ * 因此居中定位不会被震动吃掉。
71
+ *
72
+ * 两个幅度默认同值,所以不带参数是斜向摆;把某一轴设为 0 就只剩另一条轴,
73
+ * 两轴同相位所以合成轨迹始终是一条直线。
74
+ * 同一目标上重复调用会**重新开始**摆(撤掉上一条),而不是叠加成更大的位移;
75
+ * `prefers-reduced-motion`(系统或 `web-ui-theme` 的 motion 档位)下不产生任何位移。
76
+ */
77
+ function shake(input) {
78
+ const { target, xDistance, yDistance, duration } = resolveOptions(input);
79
+ if (duration <= 0 || xDistance === 0 && yDistance === 0) return;
80
+ if (isReducedFor(target)) return;
81
+ if (typeof target.animate !== "function") return;
82
+ for (const animation of target.getAnimations()) if (animation.id === SHAKE_ANIMATION_ID) animation.cancel();
83
+ const base = getComputedStyle(target).transform;
84
+ const samples = [];
85
+ for (let i = 0; i <= SHAKE_SAMPLE_COUNT; i++) {
86
+ const t = i / SHAKE_SAMPLE_COUNT;
87
+ samples.push(-Math.sin(2 * Math.PI * SHAKE_CYCLES * t) * Math.exp(-3.2 * t));
88
+ }
89
+ const peak = Math.max(...samples.map(Math.abs));
90
+ const xUnit = xDistance / peak;
91
+ const yUnit = yDistance / peak;
92
+ const animation = target.animate(samples.map((sample, index) => ({
93
+ offset: index / SHAKE_SAMPLE_COUNT,
94
+ transform: withBaseTransform(base, sample * xUnit, sample * yUnit)
95
+ })), {
96
+ duration,
97
+ easing: "linear"
98
+ });
99
+ animation.id = SHAKE_ANIMATION_ID;
100
+ }
101
+ export { SHAKE_X_DISTANCE_FALLBACK, SHAKE_X_DISTANCE_VARIABLE, SHAKE_Y_DISTANCE_FALLBACK, SHAKE_Y_DISTANCE_VARIABLE, shake };
102
+
103
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../../src/components/shake/index.ts"],"sourcesContent":["import { findRootTheme } from '@/shared/overlay/theme-overlay-scope'\nimport { prefersReducedMotion } from '@/shared/theme/reduced-motion'\n\n/** `shake()` 的可选项。除 `target` 外都有默认值,`shake()` 不带参数即可用。 */\nexport interface ShakeOptions {\n /**\n * 位移目标。省略时震整页(`document.documentElement`):页面里 in-flow 与 `position: fixed`\n * 的内容一起跟着走,而 top layer 的 `<dialog>` 与它的 `::backdrop` 不受影响(Chromium 实测),\n * 因此正在打开的 drawer / dialog 不会跟着晃。\n */\n target?: HTMLElement\n /**\n * 横向首摆幅度(px)。省略时读目标计算样式上的 `--wui-shake-x-distance`。\n * 震整页时该 token 要写在 `html` / `:root` 上:主题元素在 body 之下,它的声明继承\n * 不到 `documentElement`(继承只往下走)。\n */\n xDistance?: number\n /**\n * 纵向幅度(px),默认与横向同值。省略时读目标计算样式上的 `--wui-shake-y-distance`。\n * 与横向同相位,所以合成位移沿一条直线、倾角由两个幅度决定;把横向置 `0` 就是纯上下摆,\n * 把纵向置 `0` 就是纯横摆。\n */\n yDistance?: number\n /** 整段时长(ms)。省略时读目标计算样式上的 `--wui-shake-duration`。 */\n duration?: number\n}\n\n/** 横向幅度 token 的名字;注册项与 JS 读取共用这一个字符串。 */\nexport const SHAKE_X_DISTANCE_VARIABLE = '--wui-shake-x-distance'\n/** 纵向幅度 token 的名字。 */\nexport const SHAKE_Y_DISTANCE_VARIABLE = '--wui-shake-y-distance'\nconst DURATION_PROPERTY = '--wui-shake-duration'\n\n/*\n * 两个 FALLBACK 各是对应注册项 initialValue 的镜像,**不是**第二真相源:浏览器里\n * computed style 永远有值,走不到这个兜底;保留它们只为 jsdom——那里没有 registerProperty,\n * 读回来是空串。shake.browser.spec.ts 有断言把两者钉在一起,防止漂移。\n * 两轴默认同值:不带参数就是斜向摆,单轴归零才只摆另一条轴。\n */\nexport const SHAKE_X_DISTANCE_FALLBACK = 12\nexport const SHAKE_Y_DISTANCE_FALLBACK = 12\n// 时长不注册,没有对应的 initialValue 可镜像,兜底就是唯一真相源。\nconst DURATION_FALLBACK = 420\n\n/** 挂在 Animation.id 上的标记,用于「同一次调用只保留一条震动」。 */\nconst SHAKE_ANIMATION_ID = 'wui-shake'\n\n/*\n * 波形:阻尼正弦 f(t) = -sin(2π·CYCLES·t)·e^(-DECAY·t),t ∈ [0, 1],负号让首摆先朝**自己幅度的负方向**\n * (默认两轴同幅,首摆朝左上)。\n *\n * - CYCLES 取**半整数**,于是 sin(2π·CYCLES) = 0:t=0 与 t=1 天然落在原位,收尾不需要额外补一条\n * 归零关键帧,也不会因为「最后一个采样点还剩一点振幅」而在动画结束时跳一下。\n * - 采样后按最大绝对值归一,`xDistance` 与 `yDistance` 因此就是**各自轴上实际摆出的最大位移**,\n * 不随 DECAY 漂移:改衰减只改「摆几下来回」,不改「摆多大」。两轴共用同一条采样序列,\n * 即**同相位**——合成轨迹因此是一条直线而不是椭圆轨道,方向可预期,读起来仍是「摆几下」。\n * - 采样点取 25(含首尾):420ms / 24 段 ≈ 17.5ms,约一帧一段,折线对正弦的逼近误差在\n * 亚像素量级,人眼读不出台阶。\n * - 关键帧之间的插值用 `linear`:整段 `easing` 会重排**每段之间的时间分配**,\n * ease-out 会把尾段拉长、首摆压短,而首摆正是「被拦下了」这一下信号,不能被压。\n */\nconst SHAKE_CYCLES = 3.5\nconst SHAKE_DECAY = 3.2\nconst SHAKE_SAMPLE_COUNT = 24\n\nif (typeof CSS !== 'undefined' && 'registerProperty' in CSS) {\n /*\n * 幅度注册为 <length>:消费方写 unitless 的 0 会在计算值阶段归一成 0px,读回来永远是带单位的\n * 长度;inherits 为 true,让挂在 html 或 web-ui-theme 上的一次改写覆盖到页面里任何一处调用点。\n * `<length>` 同时吃视口单位与 calc()(`2vmax`、`calc(1vmax + 4px)` 在两个引擎里都在计算值阶段\n * 落成确定 px),但**不接受百分号**——写了会静默回落到 initialValue。\n * 时长不注册为 <time>:注册项的计算值会按规范单位序列化成 `s`,一个名为 duration 却只能写\n * 毫秒数的 token 反而更容易读错,所以走「带单位读、按后缀换算」的解析。\n * 注册表是全局的,模块重复执行(HMR)时捕获已注册错误并忽略。\n */\n for (const [name, initialValue] of [\n [SHAKE_X_DISTANCE_VARIABLE, '12px'],\n [SHAKE_Y_DISTANCE_VARIABLE, '12px']\n ] as const) {\n try {\n CSS.registerProperty({ name, syntax: '<length>', inherits: true, initialValue })\n } catch {\n // 已注册(如 HMR 重复执行)时忽略\n }\n }\n}\n\nfunction readLength(target: HTMLElement, property: string, fallback: number): number {\n const parsed = Number.parseFloat(getComputedStyle(target).getPropertyValue(property))\n return Number.isFinite(parsed) ? parsed : fallback\n}\n\n/*\n * 时长读的是带单位的 CSS 时间字面量。计算值可能序列化成 `420ms`(未注册的原样透传)或\n * `0.42s`(注册为 <time> 的规范单位),所以按后缀换算,不能直接 parseFloat——那会把 0.42 当成\n * 0.42ms,整段震动塌成一次瞬移。\n */\nfunction readDuration(target: HTMLElement, property: string, fallback: number): number {\n const raw = getComputedStyle(target).getPropertyValue(property).trim()\n const value = Number.parseFloat(raw)\n if (!Number.isFinite(value)) return fallback\n return raw.endsWith('ms') ? value : value * 1000\n}\n\n/**\n * 目标所在范围是否要求减少动效。\n *\n * 指定 target 时按它自己的主题范围走。默认震整页时不能只看祖先:主题元素总在 body 之下,\n * `documentElement` 往上走找不到它们,于是 `shake()` 会和页面里其它动效认的档位分叉。\n * 这里改用**根主题**——与 toast / imagePreview 解析挂载根时同一个 `findRootTheme()`,\n * 命令式入口的「页面级默认值」两条路径认的是同一个主题;没有主题范围时回退系统偏好。\n */\nfunction isReducedFor(target: HTMLElement): boolean {\n if (target !== document.documentElement) return prefersReducedMotion(target)\n const theme = findRootTheme()\n return theme ? theme.isReducedMotion() : prefersReducedMotion(target)\n}\n\nfunction resolveOptions(input: HTMLElement | ShakeOptions | undefined): {\n target: HTMLElement\n xDistance: number\n yDistance: number\n duration: number\n} {\n const options: ShakeOptions = input instanceof HTMLElement ? { target: input } : (input ?? {})\n const target = options.target ?? document.documentElement\n return {\n target,\n xDistance: options.xDistance ?? readLength(target, SHAKE_X_DISTANCE_VARIABLE, SHAKE_X_DISTANCE_FALLBACK),\n yDistance: options.yDistance ?? readLength(target, SHAKE_Y_DISTANCE_VARIABLE, SHAKE_Y_DISTANCE_FALLBACK),\n duration: options.duration ?? readDuration(target, DURATION_PROPERTY, DURATION_FALLBACK)\n }\n}\n\n/**\n * 关键帧里写的是 `<transform-list>`,而计算值在没有变换时是 `none`——`none translate(-6px, 0px)`\n * 不是合法的变换列表,整条关键帧会被丢弃。\n */\nfunction withBaseTransform(base: string, dx: number, dy: number): string {\n const translate = `translate(${dx.toFixed(3)}px, ${dy.toFixed(3)}px)`\n return base === 'none' ? translate : `${base} ${translate}`\n}\n\n/**\n * 让一次「不行 / 请先处理」的信号可见:目标摆几下后回到原位。\n *\n * 用于打断一次被拦下的操作(例如表单还有未保存内容却要退出),或把注意力拉回某个元素。\n * 传 `target` 就只震那一块,不传就震整页。播放期间目标上已有的 `transform` 会被逐帧带上,\n * 因此居中定位不会被震动吃掉。\n *\n * 两个幅度默认同值,所以不带参数是斜向摆;把某一轴设为 0 就只剩另一条轴,\n * 两轴同相位所以合成轨迹始终是一条直线。\n * 同一目标上重复调用会**重新开始**摆(撤掉上一条),而不是叠加成更大的位移;\n * `prefers-reduced-motion`(系统或 `web-ui-theme` 的 motion 档位)下不产生任何位移。\n */\nexport function shake(input?: HTMLElement | ShakeOptions): void {\n const { target, xDistance, yDistance, duration } = resolveOptions(input)\n // 两轴都为 0 才是「什么都不摆」:单轴归零只是把那条轴摘掉,另一轴照旧。\n if (duration <= 0 || (xDistance === 0 && yDistance === 0)) return\n // 前庭动效敏感用户的档位:不摆,也不留任何状态。\n if (isReducedFor(target)) return\n // jsdom 等没有 WAAPI 的环境:命令式入口不该因为「测试环境没有动画」而抛错。\n if (typeof target.animate !== 'function') return\n\n // 重触发时先撤掉自己上一条:同一属性上的两条动画会各自生效,读起来是「位移翻倍」而不是「重摆」。\n for (const animation of target.getAnimations()) {\n if (animation.id === SHAKE_ANIMATION_ID) animation.cancel()\n }\n\n const base = getComputedStyle(target).transform\n const samples: number[] = []\n for (let i = 0; i <= SHAKE_SAMPLE_COUNT; i++) {\n const t = i / SHAKE_SAMPLE_COUNT\n samples.push(-Math.sin(2 * Math.PI * SHAKE_CYCLES * t) * Math.exp(-SHAKE_DECAY * t))\n }\n const peak = Math.max(...samples.map(Math.abs))\n // 采样点里必有非零波峰(由 SHAKE_CYCLES 与 SHAKE_SAMPLE_COUNT 决定),按它把幅度归一到峰值。\n const xUnit = xDistance / peak\n const yUnit = yDistance / peak\n\n const animation = target.animate(\n samples.map((sample, index) => ({\n offset: index / SHAKE_SAMPLE_COUNT,\n transform: withBaseTransform(base, sample * xUnit, sample * yUnit)\n })),\n { duration, easing: 'linear' }\n )\n animation.id = SHAKE_ANIMATION_ID\n}\n"],"mappings":"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"}
@@ -1,4 +1,5 @@
1
1
  import { OverlayContainer } from "../../shared/overlay/portal.js";
2
+ import { OverlayTriggerMode } from "../../shared/overlay/trigger-props.js";
2
3
  import { LitElement } from "lit";
3
4
  import { Placement } from "@floating-ui/dom";
4
5
  export declare class WebUiTooltip extends LitElement {
@@ -6,6 +7,9 @@ export declare class WebUiTooltip extends LitElement {
6
7
  get placement(): Placement;
7
8
  set placement(v: string);
8
9
  private _placement;
10
+ get trigger(): string;
11
+ set trigger(v: string | readonly OverlayTriggerMode[]);
12
+ private _trigger;
9
13
  get showDelay(): number;
10
14
  set showDelay(v: number);
11
15
  private _showDelay;
@@ -41,6 +45,10 @@ export declare class WebUiTooltip extends LitElement {
41
45
  firstUpdated(): void;
42
46
  protected willUpdate(changed: Map<string, unknown>): void;
43
47
  protected updated(changed: Map<string, unknown>): void;
48
+ private _syncTriggerListeners;
49
+ private _clearTriggerListeners;
50
+ private _onTriggerClick;
51
+ private _onClickOutside;
44
52
  private _onPointerEnter;
45
53
  private _onPointerLeave;
46
54
  private _onFocusIn;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/components/tooltip/index.ts"],"mappings":";;;qBAqBa,qBAAqB;SAChB,sBAAM;MAGlB,aAAa;MAGb,UAAU;UAKN;MAGJ;MAGA,UAAU;UAKN;MAGJ;MAGA,UAAU;UAKN;EAEoC;MAGxC;MAGA,OAAO;UAKH;EAEoB;EAEgB;EACA;EACZ,mBAAmB;UAE3C;UACA;UACA;mBACS;UACT;UACA;mBACS;mBAOA;;;;;;mBAMA;mBAIA;MAgBb;EAIK;EASA;EAeA;YAQU,WAAW,SAAS;YAQpB,QAAQ,SAAS;UA4B5B;UASA;UAMA;UAUA;UAKA;UAOA;UAMA;UAIA;UAwBM;UAIN;UAIA;UAkBA;UAIA;UAUA;UAWA;UAwBA;UAKA;UAKA;UAIA;UAIA;EAUC,wBAAM;WAoBE;IACf,eAAe;MAAc;;;;QAIzB;YACI;IACR,kBAAkB"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../../src/components/tooltip/index.ts"],"mappings":";;;;qBAyBa,qBAAqB;SAChB,sBAAM;MAGlB,aAAa;MAGb,UAAU;UAKN;MAGJ;MAGA,QAAQ,qBAAqB;UAKzB;MAGJ;MAGA,UAAU;UAKN;MAGJ;MAGA,UAAU;UAKN;EAEoC;MAGxC;MAGA,OAAO;UAKH;EAEoB;EAEgB;EACA;EACZ,mBAAmB;UAE3C;UACA;UACA;mBACS;UACT;UACA;mBACS;mBAOA;;;;;;mBAMA;mBAIA;MAgBb;EAIK;EAMA;EAYA;YAQU,WAAW,SAAS;YAQpB,QAAQ,SAAS;UAkC5B;UAYA;UAQA;UASA;UAWA;UASA;UAMA;UAUA;UAKA;UAOA;UAMA;UAIA;UAwBM;UAIN;UAIA;UAkBA;UAIA;UAUA;UAWA;UAwBA;UAKA;UAKA;UAIA;UAIA;EAUC,wBAAM;WAuBE;IACf,eAAe;MAAc;;;;QAIzB;YACI;IACR,kBAAkB"}
@@ -9,16 +9,20 @@ import { FLOATING_PLACEMENTS } from "../../shared/overlay/placement-props.js";
9
9
  import { defineOverlayPortal } from "../../shared/overlay/portal.js";
10
10
  import { UserChangeController } from "../../shared/events/user-change.js";
11
11
  import { findFocusTarget } from "../../shared/focus/tabbable.js";
12
+ import { hasTriggerMode, normalizeTriggerSet } from "../../shared/overlay/trigger-props.js";
12
13
  import style_default from "./style.js";
13
14
  import { LitElement, html, nothing, unsafeCSS } from "lit";
14
15
  import { customElement, property } from "lit/decorators.js";
15
16
  // src/components/tooltip/index.ts
17
+ /** 指针悬停与键盘聚焦是 tooltip 的常驻手势;`manual` 表示只由 `open` 控制。 */
18
+ const DEFAULT_TRIGGERS = ["hover", "focus"];
16
19
  let visibleTooltipCount = 0;
17
20
  let tooltipIdCounter = 0;
18
21
  let WebUiTooltip = class WebUiTooltip extends LitElement {
19
22
  constructor(..._args) {
20
23
  super(..._args);
21
24
  this._placement = "top";
25
+ this._trigger = DEFAULT_TRIGGERS.join(" ");
22
26
  this._showDelay = 200;
23
27
  this._hideDelay = 100;
24
28
  this.disabled = false;
@@ -48,6 +52,19 @@ let WebUiTooltip = class WebUiTooltip extends LitElement {
48
52
  openOverlay: this._overlay,
49
53
  arbitration: "none"
50
54
  });
55
+ this._onTriggerClick = (event) => {
56
+ if (this.disabled || !hasTriggerMode(this.trigger, "click")) return;
57
+ clearTimeout(this._showTimer);
58
+ clearTimeout(this._hideTimer);
59
+ if (this.open) this._hide();
60
+ else this._show(event.detail === 0);
61
+ };
62
+ this._onClickOutside = (event) => {
63
+ if (!this.open) return;
64
+ if (event.composedPath().includes(this)) return;
65
+ if (this._panel.getHandle()?.containsEvent(event)) return;
66
+ this._hide();
67
+ };
51
68
  this._onPointerEnter = (e) => {
52
69
  if (e.pointerType === "touch") return;
53
70
  if (this.disabled) return;
@@ -93,6 +110,14 @@ let WebUiTooltip = class WebUiTooltip extends LitElement {
93
110
  this._placement = normalizeLiteral(v, FLOATING_PLACEMENTS, "top");
94
111
  this.requestUpdate("placement", old);
95
112
  }
113
+ get trigger() {
114
+ return this._trigger;
115
+ }
116
+ set trigger(v) {
117
+ const old = this._trigger;
118
+ this._trigger = normalizeTriggerSet(v, DEFAULT_TRIGGERS);
119
+ this.requestUpdate("trigger", old);
120
+ }
96
121
  get showDelay() {
97
122
  return this._showDelay;
98
123
  }
@@ -123,17 +148,11 @@ let WebUiTooltip = class WebUiTooltip extends LitElement {
123
148
  connectedCallback() {
124
149
  super.connectedCallback();
125
150
  this._overlay.resume();
126
- this.addEventListener("pointerenter", this._onPointerEnter);
127
- this.addEventListener("pointerleave", this._onPointerLeave);
128
- this.addEventListener("focusin", this._onFocusIn);
129
- this.addEventListener("focusout", this._onFocusOut);
151
+ this._syncTriggerListeners();
130
152
  }
131
153
  disconnectedCallback() {
132
154
  super.disconnectedCallback();
133
- this.removeEventListener("pointerenter", this._onPointerEnter);
134
- this.removeEventListener("pointerleave", this._onPointerLeave);
135
- this.removeEventListener("focusin", this._onFocusIn);
136
- this.removeEventListener("focusout", this._onFocusOut);
155
+ this._clearTriggerListeners();
137
156
  clearTimeout(this._showTimer);
138
157
  clearTimeout(this._hideTimer);
139
158
  this._overlay.suspend();
@@ -156,6 +175,12 @@ let WebUiTooltip = class WebUiTooltip extends LitElement {
156
175
  }
157
176
  }
158
177
  updated(changed) {
178
+ if (changed.has("trigger")) {
179
+ this._clearTriggerListeners();
180
+ clearTimeout(this._showTimer);
181
+ clearTimeout(this._hideTimer);
182
+ this._syncTriggerListeners();
183
+ }
159
184
  if (changed.has("portal") || changed.has("overlayContainer")) {
160
185
  this._overlay.invalidate();
161
186
  this._overlay.scheduleFrame(() => this._reconfigureOverlay());
@@ -179,6 +204,24 @@ let WebUiTooltip = class WebUiTooltip extends LitElement {
179
204
  if (this.open) this._syncDescription();
180
205
  }
181
206
  }
207
+ _syncTriggerListeners() {
208
+ if (hasTriggerMode(this.trigger, "hover")) {
209
+ this.addEventListener("pointerenter", this._onPointerEnter);
210
+ this.addEventListener("pointerleave", this._onPointerLeave);
211
+ }
212
+ if (hasTriggerMode(this.trigger, "focus")) {
213
+ this.addEventListener("focusin", this._onFocusIn);
214
+ this.addEventListener("focusout", this._onFocusOut);
215
+ }
216
+ if (hasTriggerMode(this.trigger, "click")) document.addEventListener("click", this._onClickOutside);
217
+ }
218
+ _clearTriggerListeners() {
219
+ this.removeEventListener("pointerenter", this._onPointerEnter);
220
+ this.removeEventListener("pointerleave", this._onPointerLeave);
221
+ this.removeEventListener("focusin", this._onFocusIn);
222
+ this.removeEventListener("focusout", this._onFocusOut);
223
+ document.removeEventListener("click", this._onClickOutside);
224
+ }
182
225
  _show(isInstant = false) {
183
226
  if (this.open) return;
184
227
  this._shouldOpenInstantly = isInstant;
@@ -294,7 +337,10 @@ let WebUiTooltip = class WebUiTooltip extends LitElement {
294
337
  render() {
295
338
  return html`
296
339
  <div class="tooltip-anchor">
297
- <div class="tooltip-trigger"><slot @slotchange=${this._onTriggerSlotChange}></slot></div>
340
+ <!-- 点击只挂在 trigger 包装层:panel 里的内容被点击不该把提示切走。 -->
341
+ <div class="tooltip-trigger" @click=${this._onTriggerClick}>
342
+ <slot @slotchange=${this._onTriggerSlotChange}></slot>
343
+ </div>
298
344
  <div
299
345
  class="tooltip-panel wui-floating-panel wui-glass"
300
346
  hidden
@@ -311,6 +357,10 @@ __decorate([property({
311
357
  type: String,
312
358
  reflect: true
313
359
  })], WebUiTooltip.prototype, "placement", null);
360
+ __decorate([property({
361
+ type: String,
362
+ reflect: true
363
+ })], WebUiTooltip.prototype, "trigger", null);
314
364
  __decorate([property({
315
365
  type: Number,
316
366
  attribute: "show-delay"