@greypan/web-ui 11.0.0 → 11.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.
- package/README.CN.md +105 -41
- package/README.md +69 -0
- package/dist/assets/overlay-motion.js +1 -1
- package/dist/assets/overlay-motion.js.map +1 -1
- package/dist/assets/selection-control.js +1 -1
- package/dist/assets/selection-control.js.map +1 -1
- package/dist/classes/glass.js +6 -0
- package/dist/classes/glass.js.map +1 -0
- package/dist/classes/glow.js +6 -0
- package/dist/classes/glow.js.map +1 -0
- package/dist/classes/style.css +1 -0
- package/dist/components/autocomplete/index.d.ts +7 -0
- package/dist/components/autocomplete/index.js +33 -13
- package/dist/components/autocomplete/index.js.map +1 -1
- package/dist/components/autocomplete/style.js +1 -1
- package/dist/components/autocomplete/style.js.map +1 -1
- package/dist/components/avatar/index.js +2 -2
- package/dist/components/avatar/index.js.map +1 -1
- package/dist/components/avatar/style.js +1 -1
- package/dist/components/avatar/style.js.map +1 -1
- package/dist/components/back-top/index.d.ts +4 -0
- package/dist/components/back-top/index.js +60 -22
- package/dist/components/back-top/index.js.map +1 -1
- package/dist/components/back-top/style.js +1 -1
- package/dist/components/back-top/style.js.map +1 -1
- package/dist/components/badge/index.d.ts +3 -0
- package/dist/components/badge/index.js +29 -14
- package/dist/components/badge/index.js.map +1 -1
- package/dist/components/badge/style.js +1 -1
- package/dist/components/badge/style.js.map +1 -1
- package/dist/components/button/index.d.ts +3 -0
- package/dist/components/button/index.js +27 -18
- package/dist/components/button/index.js.map +1 -1
- package/dist/components/button/style.js +1 -1
- package/dist/components/button/style.js.map +1 -1
- package/dist/components/button-group/index.d.ts +3 -0
- package/dist/components/button-group/index.js +15 -2
- package/dist/components/button-group/index.js.map +1 -1
- package/dist/components/button-group/style.js +1 -1
- package/dist/components/button-group/style.js.map +1 -1
- package/dist/components/checkbox/index.js +6 -6
- package/dist/components/checkbox/index.js.map +1 -1
- package/dist/components/checkbox/style.js +1 -1
- package/dist/components/checkbox/style.js.map +1 -1
- package/dist/components/collapse/index.d.ts +15 -7
- package/dist/components/collapse/index.js +44 -26
- package/dist/components/collapse/index.js.map +1 -1
- package/dist/components/collapse/style.js +1 -1
- package/dist/components/collapse/style.js.map +1 -1
- package/dist/components/dialog/index.d.ts +1 -0
- package/dist/components/dialog/index.js +5 -2
- package/dist/components/dialog/index.js.map +1 -1
- package/dist/components/dialog/style.js +1 -1
- package/dist/components/dialog/style.js.map +1 -1
- package/dist/components/drawer/index.js +1 -1
- package/dist/components/drawer/index.js.map +1 -1
- package/dist/components/dropdown/index.d.ts +16 -0
- package/dist/components/dropdown/index.js +147 -18
- package/dist/components/dropdown/index.js.map +1 -1
- package/dist/components/dropdown-item/style.js +1 -1
- package/dist/components/dropdown-item/style.js.map +1 -1
- package/dist/components/editable-text/index.js +7 -3
- package/dist/components/editable-text/index.js.map +1 -1
- package/dist/components/editable-text/style.js +1 -1
- package/dist/components/editable-text/style.js.map +1 -1
- package/dist/components/empty/index.d.ts +10 -22
- package/dist/components/empty/index.js +19 -35
- package/dist/components/empty/index.js.map +1 -1
- package/dist/components/empty/style.js +1 -1
- package/dist/components/empty/style.js.map +1 -1
- package/dist/components/image-preview/index.js +2 -2
- package/dist/components/image-preview/index.js.map +1 -1
- package/dist/components/input/index.d.ts +1 -0
- package/dist/components/input/index.js +5 -2
- package/dist/components/input/index.js.map +1 -1
- package/dist/components/input/style.js +1 -1
- package/dist/components/input/style.js.map +1 -1
- package/dist/components/input-number/index.d.ts +31 -1
- package/dist/components/input-number/index.js +281 -22
- package/dist/components/input-number/index.js.map +1 -1
- package/dist/components/input-number/style.js +1 -1
- package/dist/components/input-number/style.js.map +1 -1
- package/dist/components/layout/index.js +44 -39
- package/dist/components/layout/index.js.map +1 -1
- package/dist/components/layout/style.js +1 -1
- package/dist/components/layout/style.js.map +1 -1
- package/dist/components/middle-ellipsis/index.d.ts +8 -1
- package/dist/components/middle-ellipsis/index.js +68 -10
- package/dist/components/middle-ellipsis/index.js.map +1 -1
- package/dist/components/option/style.js +1 -1
- package/dist/components/option/style.js.map +1 -1
- package/dist/components/popover/index.js +1 -1
- package/dist/components/popover/index.js.map +1 -1
- package/dist/components/popover/style.js +1 -1
- package/dist/components/popover/style.js.map +1 -1
- package/dist/components/radio/index.d.ts +5 -0
- package/dist/components/radio/index.js +33 -23
- package/dist/components/radio/index.js.map +1 -1
- package/dist/components/radio/interaction.d.ts +4 -0
- package/dist/components/radio/interaction.js +17 -0
- package/dist/components/radio/interaction.js.map +1 -0
- package/dist/components/radio/style.js +1 -1
- package/dist/components/radio/style.js.map +1 -1
- package/dist/components/radio-group/index.d.ts +22 -0
- package/dist/components/radio-group/index.js +219 -28
- package/dist/components/radio-group/index.js.map +1 -1
- package/dist/components/segmented/index.d.ts +2 -0
- package/dist/components/segmented/index.js +74 -18
- package/dist/components/segmented/index.js.map +1 -1
- package/dist/components/segmented/style.js +1 -1
- package/dist/components/segmented/style.js.map +1 -1
- package/dist/components/segmented-trigger/index.js +3 -2
- package/dist/components/segmented-trigger/index.js.map +1 -1
- package/dist/components/segmented-trigger/style.js +1 -1
- package/dist/components/segmented-trigger/style.js.map +1 -1
- package/dist/components/select/index.d.ts +8 -0
- package/dist/components/select/index.js +76 -25
- package/dist/components/select/index.js.map +1 -1
- package/dist/components/select/style.js +1 -1
- package/dist/components/select/style.js.map +1 -1
- package/dist/components/slider/index.d.ts +15 -0
- package/dist/components/slider/index.js +175 -39
- package/dist/components/slider/index.js.map +1 -1
- package/dist/components/slider/style.js +1 -1
- package/dist/components/slider/style.js.map +1 -1
- package/dist/components/spinner/index.d.ts +15 -0
- package/dist/components/spinner/index.js +79 -22
- package/dist/components/spinner/index.js.map +1 -1
- package/dist/components/spinner/style.js +1 -1
- package/dist/components/spinner/style.js.map +1 -1
- package/dist/components/svg-draw-lines/index.js +48 -12
- package/dist/components/svg-draw-lines/index.js.map +1 -1
- package/dist/components/switch/index.d.ts +11 -3
- package/dist/components/switch/index.js +57 -53
- package/dist/components/switch/index.js.map +1 -1
- package/dist/components/switch/style.js +1 -1
- package/dist/components/switch/style.js.map +1 -1
- package/dist/components/textarea/index.js +2 -2
- package/dist/components/textarea/index.js.map +1 -1
- package/dist/components/textarea/style.js +1 -1
- package/dist/components/textarea/style.js.map +1 -1
- package/dist/components/theme/index.d.ts +1 -0
- package/dist/components/theme/index.js +26 -15
- package/dist/components/theme/index.js.map +1 -1
- package/dist/components/theme/style.js +1 -1
- package/dist/components/theme/style.js.map +1 -1
- package/dist/components/toast/style.js +1 -1
- package/dist/components/toast/style.js.map +1 -1
- package/dist/components/toast/toast.js +1 -1
- package/dist/components/toast/toast.js.map +1 -1
- package/dist/components/tooltip/index.d.ts +1 -0
- package/dist/components/tooltip/index.js +10 -5
- package/dist/components/tooltip/index.js.map +1 -1
- package/dist/icons/generated/andrsrxn-inbox.d.ts +6 -0
- package/dist/icons/generated/andrsrxn-inbox.js +10 -0
- package/dist/icons/generated/andrsrxn-inbox.js.map +1 -0
- package/dist/icons/index.d.ts +1 -0
- package/dist/icons/index.js +75 -74
- package/dist/shared/focus/pointer-focus.js +12 -10
- package/dist/shared/focus/pointer-focus.js.map +1 -1
- package/dist/shared/gesture/drag-gesture.js +4 -4
- package/dist/shared/gesture/drag-gesture.js.map +1 -1
- package/dist/shared/group-management/index.js +43 -18
- package/dist/shared/group-management/index.js.map +1 -1
- package/dist/shared/menu-portal/menu-portal.js +1 -1
- package/dist/shared/menu-portal/menu-portal.js.map +1 -1
- package/dist/shared/overlay/overlay-root.js +1 -1
- package/dist/shared/overlay/overlay-root.js.map +1 -1
- package/dist/shared/overlay/overlay.js +7 -1
- package/dist/shared/overlay/overlay.js.map +1 -1
- package/package.json +5 -1
- package/dist/assets/glass.js +0 -6
- package/dist/assets/glass.js.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/middle-ellipsis/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\n\nimport { ElementWidthController } from '@/shared/element-width'\nimport { normalizeNumber } from '@/shared/normalize'\n\nimport style from './style.css?inline'\nimport { DEFAULT_MARKER, truncateMiddle, type WidthProbe } from './truncate'\n\nconst MIN_POSITION = 0\nconst MAX_POSITION = 100\nconst DEFAULT_POSITION = 50\n\n/**\n * 合串复核的容差与轮数上限。\n *\n * 分段预算看不见两个接缝处的字距,实测偏差在亚像素量级:容差取 0.5px 吸收它。\n * 轮数上限是纯防御,不是常规路径:实测四种形态(等宽 ASCII 文件名、CJK 长串、无空格长串、\n * emoji/ZWJ 混排)各挂载一次,`_apply` 每次都只被调用一次——复核自己会再调一次 `_apply`,\n * 所以调用次数就是拟合次数,一次意味着复核一轮都没触发。这条上限防的是「标记被调用方撑得比\n * 容器还宽」这类退化输入。\n */\nconst OVERFLOW_TOLERANCE = 0.5\nconst MAX_REFINE_ROUNDS = 3\n\n/**\n * 可用宽度变化的感知阈(px)。\n *\n * `_currentWidth()` 走 `clientWidth`(整数),ResizeObserver 报的是 `contentRect.width`(分数),\n * 同一个盒子在两条路径上最多差 0.5px 的取整误差。不设阈值时,观察者的首次回调会被当成一次「变宽」\n * 而整份重算——实测挂载时两次 `_apply` 的入参都是同一个 240,也就是每次挂载白做一次二分。\n * 0.5 同时压住了「跳过略小于半个像素的真实收缩」的代价:那个量级下输出最多溢出 0.5px,\n * 与 `OVERFLOW_TOLERANCE` 视为可忽略的量级一致。\n */\nconst WIDTH_EPSILON = 0.5\n\n/**\n * 单行中间省略:把超长文本截成 `首段…尾段`,省略号出现在中间而不是末尾。\n *\n * 适用面与 `text-overflow: ellipsis` 相同——**单行、且有确定宽度**。文本不换行,宽度由容器\n * 给出(块级盒子的包含块、flex 主轴的 `flex: 1`、grid track 都行)。宿主自撑宽度时不存在\n * 「可用空间」可言,组件按当时量到的宽度切一次就不再收缩(见 `_onWidth` 的收敛守卫)。\n *\n * 被丢掉的是中间那段,因此**选中复制拿到的是屏幕上这串**,不是原文;原文经 `title` 暴露,\n * 悬停可见。这条与 CSS 的 `text-overflow` 不同(那里文本节点没被动过、复制得到全文),是\n * JS 方案绕不开的代价,见 docs/research/web-ui-middle-ellipsis-261007.md。\n */\n@customElement('web-ui-middle-ellipsis')\nexport class WebUiMiddleEllipsis extends LitElement {\n static override styles = [unsafeCSS(style)]\n\n /** 要显示的完整文本。组件只读它,不回写。 */\n @property({ type: String }) text = ''\n\n /** 中间插入的标记,默认 `…`(U+2026)。空串即「只截断不给信号」,是否这样用由调用方判断。 */\n @property({ type: String, reflect: true })\n set marker(value: string) {\n const next = value ?? DEFAULT_MARKER\n if (this._marker !== next) {\n const old = this._marker\n this._marker = next\n this.requestUpdate('marker', old)\n }\n if (this.getAttribute('marker') !== next) this.setAttribute('marker', next)\n }\n\n get marker(): string {\n return this._marker\n }\n\n private _marker = DEFAULT_MARKER\n\n /**\n * 标记所在的横向比例,0–100,默认 `50`(居中)。\n *\n * 与 CSSWG css-overflow-5 的 `text-overflow: ellipsis <length-percentage>` 同向:量的是标记\n * 结束边距行末边的距离占可用行内空间的比例。`0` 只剩头部(等价于末尾省略),`100` 只剩尾部。\n * 文件名这类「头部是路径噪声、尾部才是扩展名」的场景用**大于** 50 的值让尾部多留一些。\n */\n @property({ type: Number, reflect: true, attribute: 'marker-position' })\n set markerPosition(value: number) {\n const normalized = normalizeNumber(value, MIN_POSITION, MAX_POSITION, DEFAULT_POSITION)\n if (this._markerPosition !== normalized) {\n const old = this._markerPosition\n this._markerPosition = normalized\n this.requestUpdate('markerPosition', old)\n }\n if (this.getAttribute('marker-position') !== String(normalized)) {\n this.setAttribute('marker-position', String(normalized))\n }\n }\n\n get markerPosition(): number {\n return this._markerPosition\n }\n\n private _markerPosition = DEFAULT_POSITION\n\n /*\n * 渲染结果用普通字段承载,不用 `@state`:切分只能在渲染之后量到真实宽度才算得出来,也就是说\n * 「改渲染结果」必然发生在更新周期里,而周期内的 `requestUpdate` 会被 Lit 记一条\n * `change-in-update` 警告——一个实例一条,一张千行的表就是一千条。改成普通字段 + 只在周期外\n * (ResizeObserver 回调、微任务)显式 `requestUpdate()`,渲染时机不变,警告消失。\n */\n private _display = ''\n private _truncated = false\n\n @query('.root') private _root?: HTMLElement\n @query('.text') private _textEl?: HTMLElement\n @query('.measure') private _measure?: HTMLElement\n @query('.marker-probe') private _markerProbe?: HTMLElement\n\n /** 最近一次切分依据的可用宽度(px);-1 表示还没量到过布局。 */\n private _available = -1\n /** 当前渲染出来的文本实际占宽(px)。收敛守卫靠它判断现有输出是否已经放得下。 */\n private _rendered = 0\n /** 输入变过、还没按新输入切过。 */\n private _dirty = true\n private _refineRound = 0\n\n private readonly _range = document.createRange()\n\n private readonly _width = new ElementWidthController(this, width => this._onWidth(width))\n\n private readonly _probe: WidthProbe = {\n prefix: units => this._rangeWidth(0, units),\n suffix: units => this._rangeWidth(this.text.length - units, this.text.length),\n marker: () => this._markerProbe?.getBoundingClientRect().width ?? 0\n }\n\n override willUpdate(changed: PropertyValues<this>): void {\n if (!changed.has('text') && !changed.has('marker') && !changed.has('markerPosition')) return\n\n /*\n * 输入变了就先把渲染退回全文:上一版的切分是按旧文本算的,拿它渲染新文本会显示一段两个\n * 版本都不属于的串。真正的重切由 `updated()` 排进微任务,在同一帧渲染之前完成,所以用户\n * 看不到这个中间态。这里写普通字段,正是为了不在更新周期里再排一次更新。\n */\n this._dirty = true\n this._display = this.text\n this._truncated = false\n }\n\n override updated(): void {\n const textEl = this._textEl\n if (!textEl) return\n\n this._rendered = textEl.getBoundingClientRect().width\n\n if (this._dirty) {\n this._refineRound = 0\n this._defer(() => this._apply(this._currentWidth()))\n return\n }\n\n /*\n * 合串复核:分段预算只保证「首段和尾段各自没超额度」,接缝处的字距与连字能把合串推过\n * 边界,而这件事只有渲染出来的盒子量得到。超出时按超出量回收额度重切;轮数上限兜住\n * 「怎么切都放不下」的退化情形(例如调用方把标记撑得比容器还宽)。\n */\n if (this._truncated && this._rendered > this._available + OVERFLOW_TOLERANCE) {\n if (this._refineRound < MAX_REFINE_ROUNDS) {\n this._refineRound += 1\n const tighter = this._available - (this._rendered - this._available) - OVERFLOW_TOLERANCE\n this._defer(() => this._apply(tighter))\n }\n return\n }\n\n this._refineRound = 0\n }\n\n /**\n * 容器宽度变化。\n *\n * 收缩方向带一条收敛守卫:宿主宽度由内容决定时(flex 主轴 `auto`、或 `width: fit-content` 在内容\n * 缩到容器以内之后),把自己截短会反过来把宿主量窄,于是排下一次尺寸回调。这个自驱动收缩**可能**\n * 一路棘轮到只剩标记,是否发生取决于度量:\n *\n * - flex + 等宽 16px + 默认 50%:缩掉的量小于一个簇宽时,重新二分落回同一组切点,走一步就停。\n * - `width: fit-content` + `marker-position: 80` + CJK:每个簇都是整宽块,切短一簇与宿主窄一簇\n * 互相咬合,摘掉本守卫后 300px 容器里逐帧 282→266→…→10,九步塌到只剩标记(同仓 browser spec\n * 的「fit-content 宿主 + 非居中标记」用例钉住了这一条)。\n *\n * 判据因此取「当前输出已经放得下」:放得下就没有必须重切的理由,自驱动收缩在第一步就停,与度量\n * 无关;真正的容器收缩会让输出宽于新宽度,那时才重切。放长方向没有这个问题,一律重切(可能能\n * 少截一些)。\n */\n private _onWidth(width: number): void {\n if (!(width > 0)) return\n\n /*\n * 两条短路都只在「没有未落的拟合」时才成立,它们的依据都是「当前输出就是按这个宽度切出来的」——\n * 而 `_dirty` 为真恰恰说明它不是(输入换过了,切分还没跟上)。宿主无布局期间改 text 就是这样:\n * `_apply` 按宽度 ≤ 0 直接返回,`_dirty` 留着,恢复显示时宽度与上次**相同**,若这里按\n * 「宽度没变」短路,就再也没有任何回调会把它切回来——屏幕上留着一段被 `overflow: hidden`\n * 硬裁的全文,没有标记也没有 title。\n */\n if (!this._dirty) {\n // 与上次拟合用的宽度相同(含亚像素取整差)就没有可算的:当前结果正是按这个宽度切出来的。\n // 这一档实测存在——观察者的首次回调报的就是 `_currentWidth()` 量到的同一个宽度。\n if (this._available >= 0 && Math.abs(width - this._available) <= WIDTH_EPSILON) return\n if (this._available >= 0 && width < this._available && this._rendered <= width + OVERFLOW_TOLERANCE) {\n return\n }\n }\n\n this._refineRound = 0\n this._apply(width)\n }\n\n /**\n * 把一次重切排到当前更新周期之外。\n *\n * 重切只能在渲染之后量到宽度才做得了,所以第二次更新是必然的;区别只在它从哪里排出去。\n * 微任务仍排在下一帧渲染之前,画面不会先闪一版全文,而 `updated()` 返回时更新已不再是\n * pending,Lit 的 `change-in-update` 警告不会触发。\n */\n private _defer(fit: () => void): void {\n queueMicrotask(fit)\n }\n\n /**\n * 同步可读的可用行内空间。\n *\n * ResizeObserver 的回调在布局之后才到,属性刚改完的那一帧还没有宽度可用;用 `.root` 的\n * 内容盒补上,免得先按全文画一帧再切。jsdom 没有布局,读到的 0 由 `_apply` 跳过。\n */\n private _currentWidth(): number {\n return this._root?.clientWidth ?? 0\n }\n\n /**\n * 按可用宽度切分并写回渲染结果。\n *\n * 宽度为 0(没有布局)时什么都不做,**并且保留 `_dirty`**:宿主不可见期间改 text 时这一趟\n * 必然被跳过,若在这里就把「有未落的拟合」清掉,等它重新有布局时就没有任何东西记得要切了。\n * 清标记因此只发生在真的切完之后。\n */\n private _apply(available: number): void {\n if (!(available > 0)) return\n\n // 量具必须在切分之前装上当前文本:首次更新时量具还不存在,只靠输入变化那次同步会让第一次\n // 切分读到空量具,`prefix(text.length)` 量出 0 而被误判成「放得下」。\n this._syncMeasure()\n\n const result = truncateMiddle(this.text, this._marker, this._markerPosition, available, this._probe)\n this._available = available\n this._dirty = false\n this._settle(result.truncated ? result.head + this._marker + result.tail : this.text, result.truncated)\n }\n\n private _settle(display: string, truncated: boolean): void {\n if (this._display === display && this._truncated === truncated) return\n\n this._display = display\n this._truncated = truncated\n // 周期内的调用会被 Lit 记 change-in-update:这里的两条来路(尺寸回调、微任务)都在周期之外。\n this.requestUpdate()\n }\n\n /** 把全文装进隐藏量具。它没有 Lit 绑定,改它不会触发重渲染。 */\n private _syncMeasure(): void {\n const measure = this._measure\n if (measure && measure.textContent !== this.text) measure.textContent = this.text\n }\n\n /** `[from, to)` 这段原文在量具里的渲染宽度。空区间恒为 0——Range 塌缩矩形的宽度不可靠。 */\n private _rangeWidth(from: number, to: number): number {\n if (to <= from) return 0\n\n const node = this._measure?.firstChild\n if (!(node instanceof Text)) return 0\n\n this._range.setStart(node, from)\n this._range.setEnd(node, to)\n return this._range.getBoundingClientRect().width\n }\n\n override render() {\n return html`\n <div class=\"root\">\n <span class=\"text\" title=${ifDefined(this._truncated ? this.text : undefined)}>${this._display}</span>\n <span class=\"measure\"></span>\n <span class=\"marker-probe\" aria-hidden=\"true\">${this._marker}</span>\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-middle-ellipsis': WebUiMiddleEllipsis\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/middle-ellipsis/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS, type PropertyValues } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { ifDefined } from 'lit/directives/if-defined.js'\n\nimport { ElementWidthController } from '@/shared/element-width'\nimport { normalizeNumber } from '@/shared/normalize'\n\nimport style from './style.css?inline'\nimport { DEFAULT_MARKER, truncateMiddle, type WidthProbe } from './truncate'\n\nconst MIN_POSITION = 0\nconst MAX_POSITION = 100\nconst DEFAULT_POSITION = 50\n\n/**\n * 合串复核的容差与轮数上限。\n *\n * 分段预算看不见两个接缝处的字距,实测偏差在亚像素量级:容差取 0.5px 吸收它。\n * 轮数上限是纯防御,不是常规路径:实测四种形态(等宽 ASCII 文件名、CJK 长串、无空格长串、\n * emoji/ZWJ 混排)各挂载一次,`_apply` 每次都只被调用一次——复核自己会再调一次 `_apply`,\n * 所以调用次数就是拟合次数,一次意味着复核一轮都没触发。这条上限防的是「标记被调用方撑得比\n * 容器还宽」这类退化输入。\n */\nconst OVERFLOW_TOLERANCE = 0.5\nconst MAX_REFINE_ROUNDS = 3\n\n/**\n * 可用宽度变化的感知阈(px)。\n *\n * `_currentWidth()` 走 `clientWidth`(整数),ResizeObserver 报的是 `contentRect.width`(分数),\n * 同一个盒子在两条路径上最多差 0.5px 的取整误差。不设阈值时,观察者的首次回调会被当成一次「变宽」\n * 而整份重算——实测挂载时两次 `_apply` 的入参都是同一个 240,也就是每次挂载白做一次二分。\n * 0.5 同时压住了「跳过略小于半个像素的真实收缩」的代价:那个量级下输出最多溢出 0.5px,\n * 与 `OVERFLOW_TOLERANCE` 视为可忽略的量级一致。\n */\nconst WIDTH_EPSILON = 0.5\n\ninterface MeasurementTarget {\n readonly notify: () => void\n text: string | null\n width: number\n}\n\nconst measurementTargets = new WeakMap<Element, MeasurementTarget>()\nlet measurementObserver: ResizeObserver | undefined\nconst fontMetricSubscribers = new Set<() => void>()\nlet observedFontSet: FontFaceSet | undefined\n\nfunction onFontsLoaded(): void {\n for (const notify of fontMetricSubscribers) notify()\n}\n\nfunction observeFontMetrics(notify: () => void): void {\n const fontSet = document.fonts\n if (!fontSet) return\n\n if (!observedFontSet) {\n observedFontSet = fontSet\n observedFontSet.addEventListener('loadingdone', onFontsLoaded)\n }\n fontMetricSubscribers.add(notify)\n}\n\nfunction unobserveFontMetrics(notify: () => void): void {\n fontMetricSubscribers.delete(notify)\n if (fontMetricSubscribers.size > 0 || !observedFontSet) return\n\n observedFontSet.removeEventListener('loadingdone', onFontsLoaded)\n observedFontSet = undefined\n}\n\n/** Observe the full-text probe so font metric changes can refit without a host resize. */\nfunction getMeasurementObserver(): ResizeObserver | null {\n if (typeof ResizeObserver === 'undefined') return null\n if (measurementObserver) return measurementObserver\n\n measurementObserver = new ResizeObserver(entries => {\n for (const entry of entries) {\n const target = measurementTargets.get(entry.target)\n if (!target) continue\n\n const text = entry.target.textContent\n const width = entry.contentRect.width\n if (target.text !== text) {\n target.text = text\n target.width = width\n continue\n }\n\n if (target.width < 0) {\n target.width = width\n continue\n }\n\n if (target.width === width) continue\n target.width = width\n target.notify()\n }\n })\n\n return measurementObserver\n}\n\nfunction observeMeasurement(target: Element, notify: () => void): void {\n measurementTargets.set(target, { notify, text: target.textContent, width: -1 })\n observeFontMetrics(notify)\n\n const observer = getMeasurementObserver()\n if (!observer) return\n\n observer.observe(target, { box: 'content-box' })\n}\n\nfunction unobserveMeasurement(target: Element): void {\n const observation = measurementTargets.get(target)\n if (observation) unobserveFontMetrics(observation.notify)\n measurementTargets.delete(target)\n measurementObserver?.unobserve(target)\n}\n\nfunction updateMeasurementText(target: Element, text: string): void {\n const observation = measurementTargets.get(target)\n if (!observation) return\n\n observation.text = text\n observation.width = target.getBoundingClientRect().width\n}\n\n/**\n * 单行中间省略:把超长文本截成 `首段…尾段`,省略号出现在中间而不是末尾。\n *\n * 适用面与 `text-overflow: ellipsis` 相同——**单行、且有确定宽度**。文本不换行,宽度由容器\n * 给出(块级盒子的包含块、flex 主轴的 `flex: 1`、grid track 都行)。宿主自撑宽度时不存在\n * 「可用空间」可言,组件按当时量到的宽度切一次就不再收缩(见 `_onWidth` 的收敛守卫)。\n *\n * 被丢掉的是中间那段,因此**选中复制拿到的是屏幕上这串**,不是原文;原文经 `title` 暴露,\n * 悬停可见。这条与 CSS 的 `text-overflow` 不同(那里文本节点没被动过、复制得到全文),是\n * JS 方案绕不开的代价,见 docs/research/web-ui-middle-ellipsis-261007.md。\n */\n@customElement('web-ui-middle-ellipsis')\nexport class WebUiMiddleEllipsis extends LitElement {\n static override styles = [unsafeCSS(style)]\n\n /** 要显示的完整文本。组件只读它,不回写。 */\n @property({ type: String }) text = ''\n\n /** 中间插入的标记,默认 `…`(U+2026)。空串即「只截断不给信号」,是否这样用由调用方判断。 */\n @property({ type: String, reflect: true })\n set marker(value: string) {\n const next = value ?? DEFAULT_MARKER\n if (this._marker !== next) {\n const old = this._marker\n this._marker = next\n this.requestUpdate('marker', old)\n }\n if (this.getAttribute('marker') !== next) this.setAttribute('marker', next)\n }\n\n get marker(): string {\n return this._marker\n }\n\n private _marker = DEFAULT_MARKER\n\n /**\n * 标记所在的横向比例,0–100,默认 `50`(居中)。\n *\n * 与 CSSWG css-overflow-5 的 `text-overflow: ellipsis <length-percentage>` 同向:量的是标记\n * 结束边距行末边的距离占可用行内空间的比例。`0` 只剩头部(等价于末尾省略),`100` 只剩尾部。\n * 文件名这类「头部是路径噪声、尾部才是扩展名」的场景用**大于** 50 的值让尾部多留一些。\n */\n @property({ type: Number, reflect: true, attribute: 'marker-position' })\n set markerPosition(value: number) {\n const normalized = normalizeNumber(value, MIN_POSITION, MAX_POSITION, DEFAULT_POSITION)\n if (this._markerPosition !== normalized) {\n const old = this._markerPosition\n this._markerPosition = normalized\n this.requestUpdate('markerPosition', old)\n }\n if (this.getAttribute('marker-position') !== String(normalized)) {\n this.setAttribute('marker-position', String(normalized))\n }\n }\n\n get markerPosition(): number {\n return this._markerPosition\n }\n\n private _markerPosition = DEFAULT_POSITION\n\n /*\n * 渲染结果用普通字段承载,不用 `@state`:切分只能在渲染之后量到真实宽度才算得出来,也就是说\n * 「改渲染结果」必然发生在更新周期里,而周期内的 `requestUpdate` 会被 Lit 记一条\n * `change-in-update` 警告——一个实例一条,一张千行的表就是一千条。改成普通字段 + 只在周期外\n * (ResizeObserver 回调、微任务)显式 `requestUpdate()`,渲染时机不变,警告消失。\n */\n private _display = ''\n private _truncated = false\n\n @query('.root') private _root?: HTMLElement\n @query('.text') private _textEl?: HTMLElement\n @query('.measure') private _measure?: HTMLElement\n @query('.marker-probe') private _markerProbe?: HTMLElement\n\n /** 最近一次切分依据的可用宽度(px);-1 表示还没量到过布局。 */\n private _available = -1\n /** 最近一次 ResizeObserver 测得的宿主内容盒宽度;收敛守卫跳过拟合时也继续更新。 */\n private _observedWidth = -1\n /** 当前渲染出来的文本实际占宽(px)。收敛守卫靠它判断现有输出是否已经放得下。 */\n private _rendered = 0\n /** 输入或字体度量变过、还没重新拟合。 */\n private _dirty = true\n private _refineRound = 0\n private _observedMeasure?: HTMLElement\n\n private readonly _range = document.createRange()\n\n private readonly _width = new ElementWidthController(this, width => this._onWidth(width))\n\n private readonly _onMeasurementChange = (): void => {\n this._refineRound = 0\n this._dirty = true\n // `_available` can predate a shrink; use the latest precise content-box width even when it skipped a refit.\n this._apply(this._observedWidth > 0 ? this._observedWidth : this._currentWidth())\n }\n\n private readonly _probe: WidthProbe = {\n prefix: units => this._rangeWidth(0, units),\n suffix: units => this._rangeWidth(this.text.length - units, this.text.length),\n marker: () => this._markerProbe?.getBoundingClientRect().width ?? 0\n }\n\n override connectedCallback(): void {\n super.connectedCallback()\n if (this._available >= 0) {\n this._dirty = true\n this._refineRound = 0\n }\n this._observeMeasure()\n }\n\n override disconnectedCallback(): void {\n if (this._observedMeasure) {\n unobserveMeasurement(this._observedMeasure)\n this._observedMeasure = undefined\n }\n super.disconnectedCallback()\n }\n\n override willUpdate(changed: PropertyValues<this>): void {\n if (!changed.has('text') && !changed.has('marker') && !changed.has('markerPosition')) return\n\n /*\n * 输入变了就先把渲染退回全文:上一版的切分是按旧文本算的,拿它渲染新文本会显示一段两个\n * 版本都不属于的串。真正的重切由 `updated()` 排进微任务,在同一帧渲染之前完成,所以用户\n * 看不到这个中间态。这里写普通字段,正是为了不在更新周期里再排一次更新。\n */\n this._dirty = true\n this._display = this.text\n this._truncated = false\n }\n\n override updated(): void {\n const textEl = this._textEl\n // Lit can finish an update after this host was removed. Resume fitting only after reconnect.\n if (!textEl || !this.isConnected) return\n\n this._observeMeasure()\n\n this._rendered = textEl.getBoundingClientRect().width\n\n if (this._dirty) {\n this._refineRound = 0\n this._defer(() => this._apply(this._currentWidth()))\n return\n }\n\n /*\n * 合串复核:分段预算只保证「首段和尾段各自没超额度」,接缝处的字距与连字能把合串推过\n * 边界,而这件事只有渲染出来的盒子量得到。超出时按超出量回收额度重切;轮数上限兜住\n * 「怎么切都放不下」的退化情形(例如调用方把标记撑得比容器还宽)。\n */\n if (this._truncated && this._rendered > this._available + OVERFLOW_TOLERANCE) {\n if (this._refineRound < MAX_REFINE_ROUNDS) {\n this._refineRound += 1\n const tighter = this._available - (this._rendered - this._available) - OVERFLOW_TOLERANCE\n this._defer(() => this._apply(tighter))\n }\n return\n }\n\n this._refineRound = 0\n }\n\n /**\n * 容器宽度变化。\n *\n * 收缩方向带一条收敛守卫:宿主宽度由内容决定时(flex 主轴 `auto`、或 `width: fit-content` 在内容\n * 缩到容器以内之后),把自己截短会反过来把宿主量窄,于是排下一次尺寸回调。这个自驱动收缩**可能**\n * 一路棘轮到只剩标记,是否发生取决于度量:\n *\n * - flex + 等宽 16px + 默认 50%:缩掉的量小于一个簇宽时,重新二分落回同一组切点,走一步就停。\n * - `width: fit-content` + `marker-position: 80` + CJK:每个簇都是整宽块,切短一簇与宿主窄一簇\n * 互相咬合,摘掉本守卫后 300px 容器里逐帧 282→266→…→10,九步塌到只剩标记(同仓 browser spec\n * 的「fit-content 宿主 + 非居中标记」用例钉住了这一条)。\n *\n * 判据因此取「当前输出已经放得下」:放得下就没有必须重切的理由,自驱动收缩在第一步就停,与度量\n * 无关;真正的容器收缩会让输出宽于新宽度,那时才重切。放长方向没有这个问题,一律重切(可能能\n * 少截一些)。\n */\n private _onWidth(width: number): void {\n this._observedWidth = width\n if (!(width > 0)) return\n\n /*\n * 两条短路都只在「没有未落的拟合」时才成立,它们的依据都是「当前输出就是按这个宽度切出来的」——\n * 而 `_dirty` 为真恰恰说明它不是(输入换过了,切分还没跟上)。宿主无布局期间改 text 就是这样:\n * `_apply` 按宽度 ≤ 0 直接返回,`_dirty` 留着,恢复显示时宽度与上次**相同**,若这里按\n * 「宽度没变」短路,就再也没有任何回调会把它切回来——屏幕上留着一段被 `overflow: hidden`\n * 硬裁的全文,没有标记也没有 title。\n */\n if (!this._dirty) {\n // 与上次拟合用的宽度相同(含亚像素取整差)就没有可算的:当前结果正是按这个宽度切出来的。\n // 这一档实测存在——观察者的首次回调报的就是 `_currentWidth()` 量到的同一个宽度。\n if (this._available >= 0 && Math.abs(width - this._available) <= WIDTH_EPSILON) return\n if (this._available >= 0 && width < this._available && this._rendered <= width + OVERFLOW_TOLERANCE) {\n return\n }\n }\n\n this._refineRound = 0\n this._apply(width)\n }\n\n /**\n * 把一次重切排到当前更新周期之外。\n *\n * 重切只能在渲染之后量到宽度才做得了,所以第二次更新是必然的;区别只在它从哪里排出去。\n * 微任务仍排在下一帧渲染之前,画面不会先闪一版全文,而 `updated()` 返回时更新已不再是\n * pending,Lit 的 `change-in-update` 警告不会触发。\n */\n private _defer(fit: () => void): void {\n queueMicrotask(fit)\n }\n\n /**\n * 同步可读的可用行内空间。\n *\n * ResizeObserver 的回调在布局之后才到,属性刚改完的那一帧还没有宽度可用;用 `.root` 的\n * 内容盒补上,免得先按全文画一帧再切。jsdom 没有布局,读到的 0 由 `_apply` 跳过。\n */\n private _currentWidth(): number {\n return this._root?.clientWidth ?? 0\n }\n\n /**\n * 按可用宽度切分并写回渲染结果。\n *\n * 宽度为 0(没有布局)时什么都不做,**并且保留 `_dirty`**:宿主不可见期间改 text 时这一趟\n * 必然被跳过,若在这里就把「有未落的拟合」清掉,等它重新有布局时就没有任何东西记得要切了。\n * 清标记因此只发生在真的切完之后。\n */\n private _apply(available: number): void {\n if (!(available > 0)) return\n\n // 量具必须在切分之前装上当前文本:首次更新时量具还不存在,只靠输入变化那次同步会让第一次\n // 切分读到空量具,`prefix(text.length)` 量出 0 而被误判成「放得下」。\n this._syncMeasure()\n\n const result = truncateMiddle(this.text, this._marker, this._markerPosition, available, this._probe)\n this._available = available\n this._dirty = false\n this._settle(result.truncated ? result.head + this._marker + result.tail : this.text, result.truncated)\n }\n\n private _settle(display: string, truncated: boolean): void {\n if (this._display === display && this._truncated === truncated) return\n\n this._display = display\n this._truncated = truncated\n // 周期内的调用会被 Lit 记 change-in-update:这里的两条来路(尺寸回调、微任务)都在周期之外。\n this.requestUpdate()\n }\n\n /** 把全文装进隐藏量具。它没有 Lit 绑定,改它不会触发重渲染。 */\n private _syncMeasure(): void {\n const measure = this._measure\n if (measure && measure.textContent !== this.text) {\n measure.textContent = this.text\n updateMeasurementText(measure, this.text)\n }\n }\n\n private _observeMeasure(): void {\n const measure = this._measure\n if (!measure || measure === this._observedMeasure) return\n\n if (this._observedMeasure) unobserveMeasurement(this._observedMeasure)\n this._observedMeasure = measure\n observeMeasurement(measure, this._onMeasurementChange)\n }\n\n /** `[from, to)` 这段原文在量具里的渲染宽度。空区间恒为 0——Range 塌缩矩形的宽度不可靠。 */\n private _rangeWidth(from: number, to: number): number {\n if (to <= from) return 0\n\n const node = this._measure?.firstChild\n if (!(node instanceof Text)) return 0\n\n this._range.setStart(node, from)\n this._range.setEnd(node, to)\n return this._range.getBoundingClientRect().width\n }\n\n override render() {\n return html`\n <div class=\"root\">\n <span class=\"text\" title=${ifDefined(this._truncated ? this.text : undefined)}>${this._display}</span>\n <span class=\"measure\"></span>\n <span class=\"marker-probe\" aria-hidden=\"true\">${this._marker}</span>\n </div>\n `\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-middle-ellipsis': WebUiMiddleEllipsis\n }\n}\n"],"mappings":";;;;;;;;;AAUA,IAAM,IAAe,GACf,IAAe,KACf,IAAmB,IAWnB,IAAqB,IACrB,IAAoB,GAWpB,IAAgB,IAQhB,oBAAqB,IAAI,QAAoC,GAC/D,GACE,oBAAwB,IAAI,IAAgB,GAC9C;AAEJ,SAAS,IAAsB;CAC7B,KAAK,IAAM,KAAU,GAAuB,EAAO;AACrD;AAEA,SAAS,EAAmB,GAA0B;CACpD,IAAM,IAAU,SAAS;CACpB,MAEA,MACH,IAAkB,GAClB,EAAgB,iBAAiB,eAAe,CAAa,IAE/D,EAAsB,IAAI,CAAM;AAClC;AAEA,SAAS,EAAqB,GAA0B;CACtD,EAAsB,OAAO,CAAM,GAC/B,IAAsB,OAAO,KAAK,CAAC,OAEvC,EAAgB,oBAAoB,eAAe,CAAa,GAChE,IAAkB,KAAA;AACpB;AAGA,SAAS,IAAgD;CA4BvD,OA3BI,OAAO,iBAAmB,MAAoB,OAC9C,MAEJ,IAAsB,IAAI,gBAAe,MAAW;EAClD,KAAK,IAAM,KAAS,GAAS;GAC3B,IAAM,IAAS,EAAmB,IAAI,EAAM,MAAM;GAClD,IAAI,CAAC,GAAQ;GAEb,IAAM,IAAO,EAAM,OAAO,aACpB,IAAQ,EAAM,YAAY;GAChC,IAAI,EAAO,SAAS,GAAM;IAExB,AADA,EAAO,OAAO,GACd,EAAO,QAAQ;IACf;GACF;GAEA,IAAI,EAAO,QAAQ,GAAG;IACpB,EAAO,QAAQ;IACf;GACF;GAEI,EAAO,UAAU,MACrB,EAAO,QAAQ,GACf,EAAO,OAAO;EAChB;CACF,CAAC,GAEM;AACT;AAEA,SAAS,EAAmB,GAAiB,GAA0B;CAErE,AADA,EAAmB,IAAI,GAAQ;EAAE;EAAQ,MAAM,EAAO;EAAa,OAAO;CAAG,CAAC,GAC9E,EAAmB,CAAM;CAEzB,IAAM,IAAW,EAAuB;CACnC,KAEL,EAAS,QAAQ,GAAQ,EAAE,KAAK,cAAc,CAAC;AACjD;AAEA,SAAS,EAAqB,GAAuB;CACnD,IAAM,IAAc,EAAmB,IAAI,CAAM;CAGjD,AAFI,KAAa,EAAqB,EAAY,MAAM,GACxD,EAAmB,OAAO,CAAM,GAChC,GAAqB,UAAU,CAAM;AACvC;AAEA,SAAS,EAAsB,GAAiB,GAAoB;CAClE,IAAM,IAAc,EAAmB,IAAI,CAAM;CAC5C,MAEL,EAAY,OAAO,GACnB,EAAY,QAAQ,EAAO,sBAAsB,CAAC,CAAC;AACrD;AAcO,IAAM,IAAN,cAAkC,EAAW;;EAsFZ,aAlFH,KAAA,OAAA,IAkBjB,KAAA,UAAA,KA0BQ,KAAA,kBAAA,GAQP,KAAA,WAAA,IACE,KAAA,aAAA,IAQA,KAAA,aAAA,IAEI,KAAA,iBAAA,IAEL,KAAA,YAAA,GAEH,KAAA,SAAA,IACM,KAAA,eAAA,GAGG,KAAA,SAAA,SAAS,YAAY,GAErB,KAAA,SAAA,IAAI,EAAuB,OAAM,MAAS,KAAK,SAAS,CAAK,CAAC,GAEpC,KAAA,6BAAA;GAIlD,AAHA,KAAK,eAAe,GACpB,KAAK,SAAS,IAEd,KAAK,OAAO,KAAK,iBAAiB,IAAI,KAAK,iBAAiB,KAAK,cAAc,CAAC;EAClF,GAEsC,KAAA,SAAA;GACpC,SAAQ,MAAS,KAAK,YAAY,GAAG,CAAK;GAC1C,SAAQ,MAAS,KAAK,YAAY,KAAK,KAAK,SAAS,GAAO,KAAK,KAAK,MAAM;GAC5E,cAAc,KAAK,cAAc,sBAAsB,CAAC,CAAC,SAAS;EACpE;;;EAzFyB,KAAA,SAAA,CAAC,EAAU,CAAK,CAAC;;CAM1C,IACI,OAAO,GAAe;EACxB,IAAM,IAAO,KAAA;EACb,IAAI,KAAK,YAAY,GAAM;GACzB,IAAM,IAAM,KAAK;GAEjB,AADA,KAAK,UAAU,GACf,KAAK,cAAc,UAAU,CAAG;EAClC;EACA,AAAI,KAAK,aAAa,QAAQ,MAAM,KAAM,KAAK,aAAa,UAAU,CAAI;CAC5E;CAEA,IAAI,SAAiB;EACnB,OAAO,KAAK;CACd;CAWA,IACI,eAAe,GAAe;EAChC,IAAM,IAAa,EAAgB,GAAO,GAAc,GAAc,CAAgB;EACtF,IAAI,KAAK,oBAAoB,GAAY;GACvC,IAAM,IAAM,KAAK;GAEjB,AADA,KAAK,kBAAkB,GACvB,KAAK,cAAc,kBAAkB,CAAG;EAC1C;EACA,AAAI,KAAK,aAAa,iBAAiB,MAAM,OAAO,CAAU,KAC5D,KAAK,aAAa,mBAAmB,OAAO,CAAU,CAAC;CAE3D;CAEA,IAAI,iBAAyB;EAC3B,OAAO,KAAK;CACd;CA8CA,oBAAmC;EAMjC,AALA,MAAM,kBAAkB,GACpB,KAAK,cAAc,MACrB,KAAK,SAAS,IACd,KAAK,eAAe,IAEtB,KAAK,gBAAgB;CACvB;CAEA,uBAAsC;EAKpC,AAJA,AAEE,KAAK,sBADL,EAAqB,KAAK,gBAAgB,GAClB,KAAA,IAE1B,MAAM,qBAAqB;CAC7B;CAEA,WAAoB,GAAqC;EACnD,CAAC,EAAQ,IAAI,MAAM,KAAM,EAAQ,IAAI,QAAQ,KAAM,EAAQ,IAAI,gBAAgB,OAOnF,KAAK,SAAS,IACd,KAAK,WAAW,KAAK,MACrB,KAAK,aAAa;CACpB;CAEA,UAAyB;EACvB,IAAM,IAAS,KAAK;EAEhB,IAAC,KAAW,KAAK,aAMrB;OAJA,KAAK,gBAAgB,GAErB,KAAK,YAAY,EAAO,sBAAsB,CAAC,CAAC,OAE5C,KAAK,QAAQ;IAEf,AADA,KAAK,eAAe,GACpB,KAAK,aAAa,KAAK,OAAO,KAAK,cAAc,CAAC,CAAC;IACnD;GACF;GAOA,IAAI,KAAK,cAAc,KAAK,YAAY,KAAK,aAAa,GAAoB;IAC5E,IAAI,KAAK,eAAe,GAAmB;KACzC,KAAK,gBAAgB;KACrB,IAAM,IAAU,KAAK,cAAc,KAAK,YAAY,KAAK,cAAc;KACvE,KAAK,aAAa,KAAK,OAAO,CAAO,CAAC;IACxC;IACA;GACF;GAEA,KAAK,eAAe;EAhBpB;CAiBF;CAkBA,SAAiB,GAAqB;EACpC,KAAK,iBAAiB,GAChB,IAAQ,MASV,CAAC,KAAK,WAGJ,KAAK,cAAc,KAAK,KAAK,IAAI,IAAQ,KAAK,UAAU,KAAK,KAC7D,KAAK,cAAc,KAAK,IAAQ,KAAK,cAAc,KAAK,aAAa,IAAQ,OAKnF,KAAK,eAAe,GACpB,KAAK,OAAO,CAAK;CACnB;CASA,OAAe,GAAuB;EACpC,eAAe,CAAG;CACpB;CAQA,gBAAgC;EAC9B,OAAO,KAAK,OAAO,eAAe;CACpC;CASA,OAAe,GAAyB;EACtC,IAAI,EAAE,IAAY,IAAI;EAItB,KAAK,aAAa;EAElB,IAAM,IAAS,EAAe,KAAK,MAAM,KAAK,SAAS,KAAK,iBAAiB,GAAW,KAAK,MAAM;EAGnG,AAFA,KAAK,aAAa,GAClB,KAAK,SAAS,IACd,KAAK,QAAQ,EAAO,YAAY,EAAO,OAAO,KAAK,UAAU,EAAO,OAAO,KAAK,MAAM,EAAO,SAAS;CACxG;CAEA,QAAgB,GAAiB,GAA0B;EACrD,MAAK,aAAa,KAAW,KAAK,eAAe,OAErD,KAAK,WAAW,GAChB,KAAK,aAAa,GAElB,KAAK,cAAc;CACrB;CAGA,eAA6B;EAC3B,IAAM,IAAU,KAAK;EACrB,AAAI,KAAW,EAAQ,gBAAgB,KAAK,SAC1C,EAAQ,cAAc,KAAK,MAC3B,EAAsB,GAAS,KAAK,IAAI;CAE5C;CAEA,kBAAgC;EAC9B,IAAM,IAAU,KAAK;EACjB,AAAC,KAAW,MAAY,KAAK,qBAE7B,KAAK,oBAAkB,EAAqB,KAAK,gBAAgB,GACrE,KAAK,mBAAmB,GACxB,EAAmB,GAAS,KAAK,oBAAoB;CACvD;CAGA,YAAoB,GAAc,GAAoB;EACpD,IAAI,KAAM,GAAM,OAAO;EAEvB,IAAM,IAAO,KAAK,UAAU;EAK5B,OAJM,aAAgB,QAEtB,KAAK,OAAO,SAAS,GAAM,CAAI,GAC/B,KAAK,OAAO,OAAO,GAAM,CAAE,GACpB,KAAK,OAAO,sBAAsB,CAAC,CAAC,SAJP;CAKtC;CAEA,SAAkB;EAChB,OAAO,CAAI;;mCAEoB,EAAU,KAAK,aAAa,KAAK,OAAO,KAAA,CAAS,EAAE,GAAG,KAAK,SAAS;;wDAE/C,KAAK,QAAQ;;;CAGnE;AACF;AAvRG,EAAA,CAAA,EAAS,EAAE,MAAM,OAAO,CAAC,CAAA,GAAA,EAAA,WAAA,QAAA,KAAA,CAAA,GAGzB,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;AAAK,CAAC,CAAA,GAAA,EAAA,WAAA,UAAA,IAAA,GAwBxC,EAAA,CAAA,EAAS;CAAE,MAAM;CAAQ,SAAS;CAAM,WAAW;AAAkB,CAAC,CAAA,GAAA,EAAA,WAAA,kBAAA,IAAA,GA4BtE,EAAA,CAAA,EAAM,OAAO,CAAA,GAAA,EAAA,WAAA,SAAA,KAAA,CAAA,GACb,EAAA,CAAA,EAAM,OAAO,CAAA,GAAA,EAAA,WAAA,WAAA,KAAA,CAAA,GACb,EAAA,CAAA,EAAM,UAAU,CAAA,GAAA,EAAA,WAAA,YAAA,KAAA,CAAA,GAChB,EAAA,CAAA,EAAM,eAAe,CAAA,GAAA,EAAA,WAAA,gBAAA,KAAA,CAAA,GA/DvB,IAAA,EAAA,CAAA,EAAc,wBAAwB,CAAA,GAAA,CAAA"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/components/option/style.css?inline
|
|
2
|
-
var e = "*,:before,:after{box-sizing:border-box}:host{display:block}:host([disabled]){cursor:not-allowed;opacity:.4}.option-label{cursor:default;-webkit-user-select:none;user-select:none;gap:var(--wui-space-2,8px);box-sizing:border-box;height:32px;padding:0 var(--wui-space-2,8px);border-radius:var(--wui-radius-control,calc(infinity * 1px));font-size:var(--wui-font-size,14px);color:var(--wui-color-text,#1b1b1b);text-overflow:ellipsis;white-space:nowrap;align-items:center;display:flex;overflow:hidden}.check{display:var(--wui-option-check-display,block);flex-shrink:0}.content-wrap{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}:host(:not([selected])) .check{visibility:hidden}::slotted([slot=prefix]),::slotted([slot=suffix]){flex-shrink:0}.option-label:focus-visible{outline:var(--wui-focus-ring-width,3px) solid var(--wui-color-focus-ring,color-mix(in srgb, var(--wui-color-accent,#
|
|
2
|
+
var e = "*,:before,:after{box-sizing:border-box}:host{display:block}:host([disabled]){cursor:not-allowed;opacity:.4}.option-label{cursor:default;-webkit-user-select:none;user-select:none;gap:var(--wui-space-2,8px);box-sizing:border-box;height:32px;padding:0 var(--wui-space-2,8px);border-radius:var(--wui-radius-control,calc(infinity * 1px));font-size:var(--wui-font-size,14px);color:var(--wui-color-text,#1b1b1b);text-overflow:ellipsis;white-space:nowrap;align-items:center;display:flex;overflow:hidden}.check{display:var(--wui-option-check-display,block);flex-shrink:0}.content-wrap{text-overflow:ellipsis;white-space:nowrap;flex:1;overflow:hidden}:host(:not([selected])) .check{visibility:hidden}::slotted([slot=prefix]),::slotted([slot=suffix]){flex-shrink:0}.option-label:focus-visible{outline:var(--wui-focus-ring-width,3px) solid var(--wui-color-focus-ring,color-mix(in srgb, var(--wui-color-accent,#0071e3) 40%, transparent));outline-offset:2px}:host([active]) .option-label{color:var(--wui-color-on-accent,#fff);background:var(--wui-color-accent,#0071e3)}@media (hover:hover) and (pointer:fine){:host(:not([active])) .option-label:hover{background:var(--wui-color-border,#0000001a)}}:host([disabled]) .option-label{cursor:not-allowed}";
|
|
3
3
|
//#endregion
|
|
4
4
|
export { e as default };
|
|
5
5
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/option/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: block;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n opacity: 0.4;\n}\n\n.option-label {\n cursor: default;\n user-select: none;\n\n overflow: hidden;\n display: flex;\n gap: var(--wui-space-2, 8px);\n align-items: center;\n\n box-sizing: border-box;\n height: 32px;\n padding: 0 var(--wui-space-2, 8px);\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n\n font-size: var(--wui-font-size, 14px);\n color: var(--wui-color-text, #1b1b1b);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.check {\n display: var(--wui-option-check-display, block);\n flex-shrink: 0;\n}\n\n.content-wrap {\n overflow: hidden;\n flex: 1;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n:host(:not([selected])) .check {\n visibility: hidden;\n}\n\n::slotted([slot='prefix']),\n::slotted([slot='suffix']) {\n flex-shrink: 0;\n}\n\n.option-label:focus-visible {\n outline: var(--wui-focus-ring-width, 3px) solid\n var(--wui-color-focus-ring, color-mix(in srgb, var(--wui-color-accent, #
|
|
1
|
+
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/option/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: block;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n opacity: 0.4;\n}\n\n.option-label {\n cursor: default;\n user-select: none;\n\n overflow: hidden;\n display: flex;\n gap: var(--wui-space-2, 8px);\n align-items: center;\n\n box-sizing: border-box;\n height: 32px;\n padding: 0 var(--wui-space-2, 8px);\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n\n font-size: var(--wui-font-size, 14px);\n color: var(--wui-color-text, #1b1b1b);\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n.check {\n display: var(--wui-option-check-display, block);\n flex-shrink: 0;\n}\n\n.content-wrap {\n overflow: hidden;\n flex: 1;\n text-overflow: ellipsis;\n white-space: nowrap;\n}\n\n:host(:not([selected])) .check {\n visibility: hidden;\n}\n\n::slotted([slot='prefix']),\n::slotted([slot='suffix']) {\n flex-shrink: 0;\n}\n\n.option-label:focus-visible {\n outline: var(--wui-focus-ring-width, 3px) solid\n var(--wui-color-focus-ring, color-mix(in srgb, var(--wui-color-accent, #0071e3) 40%, transparent));\n outline-offset: 2px;\n}\n\n:host([active]) .option-label {\n color: var(--wui-color-on-accent, #fff);\n background: var(--wui-color-accent, #0071e3);\n}\n\n@media (hover: hover) and (pointer: fine) {\n :host(:not([active])) .option-label:hover {\n background: var(--wui-color-border, rgb(0 0 0 / 0.1));\n }\n}\n\n:host([disabled]) .option-label {\n cursor: not-allowed;\n}\n"],"mappings":""}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import e from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
|
|
2
|
-
import t from "../../
|
|
2
|
+
import t from "../../classes/glass.js";
|
|
3
3
|
import { normalizeLiteral as n, normalizeNumber as r } from "../../shared/normalize/index.js";
|
|
4
4
|
import i from "../../assets/overlay-motion.js";
|
|
5
5
|
import { dispatchOpenChangeEvent as a } from "../../shared/open-state/index.js";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"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 glass from '@/assets/glass.css?inline'\nimport overlayMotion from '@/assets/overlay-motion.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 * lifecycle)—— 它的 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\n * 落到下层 overlay,把外层一起关掉。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":[],"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 * lifecycle)—— 它的 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\n * 落到下层 overlay,把外层一起关掉。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,5 +1,5 @@
|
|
|
1
1
|
//#region src/components/popover/style.css?inline
|
|
2
|
-
var e = "*,:before,:after{box-sizing:border-box}:host{display:inline-flex}:host([disabled]){cursor:not-allowed}.popover-trigger{display:inline-flex}.popover-anchor{display:inline-flex;position:relative}.popover-panel{--wui-internal-glass-shadow:var(--wui-shadow-panel,0 3px 9px #00000045);--wui-glass-corner-radius:var(--wui-radius-menu,18px);--wui-color-glass-ring:transparent;z-index:var(--wui-layer-inline-overlay,1);box-sizing:border-box;width:max-content;max-width:min(90vw,400px);padding:var(--wui-space-2,8px) var(--wui-space-3,12px);border-radius:var(--wui-radius-menu,18px);font-size:var(--wui-font-size,14px);line-height:var(--wui-line-height-normal,1.5);color:var(--wui-color-text,#1b1b1b);background-color:var(--wui-color-surface-menu,#fafafac2);position:absolute;top:-9999px;left:-9999px}.popover-panel:where([hidden]){display:none}.popover-panel:focus-visible{outline:var(--wui-focus-ring-width,3px) solid var(--wui-color-focus-ring,color-mix(in srgb, var(--wui-color-accent,#
|
|
2
|
+
var e = "*,:before,:after{box-sizing:border-box}:host{display:inline-flex}:host([disabled]){cursor:not-allowed}.popover-trigger{display:inline-flex}.popover-anchor{display:inline-flex;position:relative}.popover-panel{--wui-internal-glass-shadow:var(--wui-shadow-panel,0 3px 9px #00000045);--wui-glass-corner-radius:var(--wui-radius-menu,18px);--wui-color-glass-ring:transparent;z-index:var(--wui-layer-inline-overlay,1);box-sizing:border-box;width:max-content;max-width:min(90vw,400px);padding:var(--wui-space-2,8px) var(--wui-space-3,12px);border-radius:var(--wui-radius-menu,18px);font-size:var(--wui-font-size,14px);line-height:var(--wui-line-height-normal,1.5);color:var(--wui-color-text,#1b1b1b);background-color:var(--wui-color-surface-menu,#fafafac2);position:absolute;top:-9999px;left:-9999px}.popover-panel:where([hidden]){display:none}.popover-panel:focus-visible{outline:var(--wui-focus-ring-width,3px) solid var(--wui-color-focus-ring,color-mix(in srgb, var(--wui-color-accent,#0071e3) 40%, transparent));outline-offset:2px}.popover-panel.portal{z-index:var(--wui-layer-menu,100);position:fixed}:host([portal]) .popover-panel{display:none}";
|
|
3
3
|
//#endregion
|
|
4
4
|
export { e as default };
|
|
5
5
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/popover/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: inline-flex;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n}\n\n.popover-trigger {\n display: inline-flex;\n}\n\n.popover-anchor {\n position: relative;\n display: inline-flex;\n}\n\n.popover-panel {\n --wui-internal-glass-shadow: var(--wui-shadow-panel, 0 3px 9px rgb(0 0 0 / 0.27));\n --wui-glass-corner-radius: var(--wui-radius-menu, 18px);\n\n /* panel 宽远大于角半径,环底色铺出的整圈铺底会在长边中段读成一条比四角更重的\n 直边描边;转 transparent 后只剩四段角弧,见 menu-portal.css 同处注释。 */\n --wui-color-glass-ring: transparent;\n position: absolute;\n z-index: var(--wui-layer-inline-overlay, 1);\n top: -9999px;\n left: -9999px;\n\n box-sizing: border-box;\n width: max-content;\n max-width: min(90vw, 400px);\n padding: var(--wui-space-2, 8px) var(--wui-space-3, 12px);\n border-radius: var(--wui-radius-menu, 18px);\n\n font-size: var(--wui-font-size, 14px);\n line-height: var(--wui-line-height-normal, 1.5);\n color: var(--wui-color-text, #1b1b1b);\n\n /* 玻璃背景:wui-glass 在 panel 自身,这里覆盖其默认玻璃底为 menu surface 色 */\n background-color: var(--wui-color-surface-menu, rgb(250 250 250 / 0.76));\n\n &:where([hidden]) {\n display: none;\n }\n\n &:focus-visible {\n outline: var(--wui-focus-ring-width, 3px) solid\n var(--wui-color-focus-ring, color-mix(in srgb, var(--wui-color-accent, #
|
|
1
|
+
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/popover/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n:host {\n display: inline-flex;\n}\n\n:host([disabled]) {\n cursor: not-allowed;\n}\n\n.popover-trigger {\n display: inline-flex;\n}\n\n.popover-anchor {\n position: relative;\n display: inline-flex;\n}\n\n.popover-panel {\n --wui-internal-glass-shadow: var(--wui-shadow-panel, 0 3px 9px rgb(0 0 0 / 0.27));\n --wui-glass-corner-radius: var(--wui-radius-menu, 18px);\n\n /* panel 宽远大于角半径,环底色铺出的整圈铺底会在长边中段读成一条比四角更重的\n 直边描边;转 transparent 后只剩四段角弧,见 menu-portal.css 同处注释。 */\n --wui-color-glass-ring: transparent;\n position: absolute;\n z-index: var(--wui-layer-inline-overlay, 1);\n top: -9999px;\n left: -9999px;\n\n box-sizing: border-box;\n width: max-content;\n max-width: min(90vw, 400px);\n padding: var(--wui-space-2, 8px) var(--wui-space-3, 12px);\n border-radius: var(--wui-radius-menu, 18px);\n\n font-size: var(--wui-font-size, 14px);\n line-height: var(--wui-line-height-normal, 1.5);\n color: var(--wui-color-text, #1b1b1b);\n\n /* 玻璃背景:wui-glass 在 panel 自身,这里覆盖其默认玻璃底为 menu surface 色 */\n background-color: var(--wui-color-surface-menu, rgb(250 250 250 / 0.76));\n\n &:where([hidden]) {\n display: none;\n }\n\n &:focus-visible {\n outline: var(--wui-focus-ring-width, 3px) solid\n var(--wui-color-focus-ring, color-mix(in srgb, var(--wui-color-accent, #0071e3) 40%, transparent));\n outline-offset: 2px;\n }\n}\n\n.popover-panel.portal {\n position: fixed;\n z-index: var(--wui-layer-menu, 100);\n}\n\n:host([portal]) .popover-panel {\n display: none;\n}\n"],"mappings":""}
|
|
@@ -369,6 +369,8 @@ export declare class WebUiRadio extends WebUiRadio_base {
|
|
|
369
369
|
private _checked;
|
|
370
370
|
/** label 没有可渲染内容(空槽位,或只挂着一个无障碍名)时收掉 gap,见 LabelEmptinessController。 */
|
|
371
371
|
private _labelEmpty;
|
|
372
|
+
private _renderedDisabled;
|
|
373
|
+
private _wasManagedByGroup;
|
|
372
374
|
get checked(): boolean;
|
|
373
375
|
set checked(v: boolean);
|
|
374
376
|
value: string;
|
|
@@ -377,9 +379,12 @@ export declare class WebUiRadio extends WebUiRadio_base {
|
|
|
377
379
|
required: boolean;
|
|
378
380
|
private get _isDisabled();
|
|
379
381
|
private get _isManagedByGroup();
|
|
382
|
+
private get _isTabStop();
|
|
380
383
|
private readonly _formAssociation;
|
|
381
384
|
private readonly _formAssociationController;
|
|
385
|
+
focus(options?: FocusOptions): void;
|
|
382
386
|
private readonly _labelEmptiness;
|
|
387
|
+
updated(): void;
|
|
383
388
|
private _syncValidity;
|
|
384
389
|
private handleClick;
|
|
385
390
|
private handleKeyDown;
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import e from "../../_virtual/_@oxc-project_runtime@0.149.0/helpers/esm/decorate.js";
|
|
2
2
|
import { installPointerFocusSuppression as t } from "../../shared/focus/pointer-focus.js";
|
|
3
3
|
import { FormAssociated as n, FormAssociationController as r, defineFormAssociation as i } from "../../shared/form-association/index.js";
|
|
4
|
-
import {
|
|
5
|
-
import
|
|
6
|
-
import { LabelEmptinessController as
|
|
7
|
-
import
|
|
8
|
-
import { LitElement as
|
|
9
|
-
import { customElement as
|
|
10
|
-
import { classMap as
|
|
4
|
+
import { GROUP_MEMBER_DISABLED_CHANGE_EVENT as a, defineGroupManaged as o, selectionGroupContextKey as s } from "../../shared/group-management/index.js";
|
|
5
|
+
import c from "../../assets/selection-control.js";
|
|
6
|
+
import { LabelEmptinessController as l } from "../../shared/label-emptiness/index.js";
|
|
7
|
+
import u from "./style.js";
|
|
8
|
+
import { LitElement as d, html as f, unsafeCSS as p } from "lit";
|
|
9
|
+
import { customElement as m, property as h, state as g } from "lit/decorators.js";
|
|
10
|
+
import { classMap as _ } from "lit/directives/class-map.js";
|
|
11
11
|
//#region src/components/radio/index.ts
|
|
12
12
|
t();
|
|
13
|
-
var
|
|
13
|
+
var v = class extends n(d) {
|
|
14
14
|
constructor(...e) {
|
|
15
|
-
super(...e), this._groupManagement =
|
|
16
|
-
context:
|
|
15
|
+
super(...e), this._groupManagement = o(this, {
|
|
16
|
+
context: s,
|
|
17
17
|
requestUpdate: () => this.requestUpdate()
|
|
18
|
-
}).make(), this._checked = !1, this._labelEmpty = !1, this.value = "", this.name = "", this.disabled = !1, this.required = !1, this._formAssociation = i({
|
|
18
|
+
}).make(), this._checked = !1, this._labelEmpty = !1, this._wasManagedByGroup = !1, this.value = "", this.name = "", this.disabled = !1, this.required = !1, this._formAssociation = i({
|
|
19
19
|
host: this,
|
|
20
20
|
initialize: () => {
|
|
21
21
|
this.hasAttribute("checked") && (this._checked = !0);
|
|
@@ -31,12 +31,12 @@ var _ = class extends n(u) {
|
|
|
31
31
|
},
|
|
32
32
|
isStateManaged: () => this._isManagedByGroup,
|
|
33
33
|
syncValidity: () => this._syncValidity()
|
|
34
|
-
}).make(), this._formAssociationController = new r(this, this._formAssociation), this._labelEmptiness = new
|
|
34
|
+
}).make(), this._formAssociationController = new r(this, this._formAssociation), this._labelEmptiness = new l(this, ".wui-radio-label", (e) => {
|
|
35
35
|
this._labelEmpty = e;
|
|
36
36
|
});
|
|
37
37
|
}
|
|
38
38
|
static {
|
|
39
|
-
this.styles = [
|
|
39
|
+
this.styles = [p(c), p(u)];
|
|
40
40
|
}
|
|
41
41
|
get checked() {
|
|
42
42
|
return this._checked;
|
|
@@ -51,6 +51,16 @@ var _ = class extends n(u) {
|
|
|
51
51
|
get _isManagedByGroup() {
|
|
52
52
|
return this._groupManagement.getContext() !== void 0;
|
|
53
53
|
}
|
|
54
|
+
get _isTabStop() {
|
|
55
|
+
return this._isDisabled ? !1 : !this._isManagedByGroup || this._groupManagement.getContext()?.rovingTabStop === !0;
|
|
56
|
+
}
|
|
57
|
+
focus(e) {
|
|
58
|
+
this._isDisabled || this.shadowRoot?.querySelector("[role=\"radio\"]")?.focus(e);
|
|
59
|
+
}
|
|
60
|
+
updated() {
|
|
61
|
+
let e = this._isManagedByGroup, t = this._isDisabled;
|
|
62
|
+
e && (!this._wasManagedByGroup || this._renderedDisabled !== t) && queueMicrotask(() => this.dispatchEvent(new Event(a))), this._wasManagedByGroup = e, this._renderedDisabled = t;
|
|
63
|
+
}
|
|
54
64
|
_syncValidity() {
|
|
55
65
|
let e = this._formAssociation.getInternals();
|
|
56
66
|
if (e && typeof e.setValidity == "function") {
|
|
@@ -83,10 +93,10 @@ var _ = class extends n(u) {
|
|
|
83
93
|
"is-label-empty": this._labelEmpty,
|
|
84
94
|
"is-disabled": this._isDisabled
|
|
85
95
|
};
|
|
86
|
-
return
|
|
87
|
-
<
|
|
88
|
-
class=${
|
|
89
|
-
tabindex=${this.
|
|
96
|
+
return f`
|
|
97
|
+
<span
|
|
98
|
+
class=${_(e)}
|
|
99
|
+
tabindex=${this._isTabStop ? "0" : "-1"}
|
|
90
100
|
role="radio"
|
|
91
101
|
aria-checked=${String(this._checked)}
|
|
92
102
|
aria-disabled=${String(this._isDisabled)}
|
|
@@ -97,21 +107,21 @@ var _ = class extends n(u) {
|
|
|
97
107
|
<span class="wui-radio-dot"></span>
|
|
98
108
|
</span>
|
|
99
109
|
<span class="wui-radio-label"><slot></slot></span>
|
|
100
|
-
</
|
|
110
|
+
</span>
|
|
101
111
|
`;
|
|
102
112
|
}
|
|
103
113
|
};
|
|
104
|
-
e([
|
|
114
|
+
e([g()], v.prototype, "_checked", void 0), e([g()], v.prototype, "_labelEmpty", void 0), e([h({ type: String })], v.prototype, "value", void 0), e([h({
|
|
105
115
|
type: String,
|
|
106
116
|
reflect: !0
|
|
107
|
-
})],
|
|
117
|
+
})], v.prototype, "name", void 0), e([h({
|
|
108
118
|
type: Boolean,
|
|
109
119
|
reflect: !0
|
|
110
|
-
})],
|
|
120
|
+
})], v.prototype, "disabled", void 0), e([h({
|
|
111
121
|
type: Boolean,
|
|
112
122
|
reflect: !0
|
|
113
|
-
})],
|
|
123
|
+
})], v.prototype, "required", void 0), v = e([m("web-ui-radio")], v);
|
|
114
124
|
//#endregion
|
|
115
|
-
export {
|
|
125
|
+
export { v as WebUiRadio };
|
|
116
126
|
|
|
117
127
|
//# sourceMappingURL=index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/radio/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\n\nimport selectionControl from '@/assets/selection-control.css?inline'\nimport { installPointerFocusSuppression } from '@/shared/focus/pointer-focus'\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport {
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/radio/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\n\nimport selectionControl from '@/assets/selection-control.css?inline'\nimport { installPointerFocusSuppression } from '@/shared/focus/pointer-focus'\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport {\n defineGroupManaged,\n GROUP_MEMBER_DISABLED_CHANGE_EVENT,\n selectionGroupContextKey,\n type SelectionGroupContext\n} from '@/shared/group-management'\nimport { LabelEmptinessController } from '@/shared/label-emptiness'\n\nimport style from './style.css?inline'\n\ninstallPointerFocusSuppression()\n\n@customElement('web-ui-radio')\nexport class WebUiRadio extends FormAssociated(LitElement) {\n static override styles = [unsafeCSS(selectionControl), unsafeCSS(style)]\n private readonly _groupManagement = defineGroupManaged<SelectionGroupContext>(this, {\n context: selectionGroupContextKey,\n requestUpdate: () => this.requestUpdate()\n }).make()\n\n @state() private _checked = false\n\n /** label 没有可渲染内容(空槽位,或只挂着一个无障碍名)时收掉 gap,见 LabelEmptinessController。 */\n @state() private _labelEmpty = false\n\n private _renderedDisabled: boolean | undefined\n private _wasManagedByGroup = false\n\n get checked(): boolean {\n return this._checked\n }\n\n set checked(v: boolean) {\n const old = this._checked\n this._checked = v\n this._formAssociation.sync()\n this.requestUpdate('checked', old)\n }\n\n @property({ type: String }) value = ''\n @property({ type: String, reflect: true }) name = ''\n @property({ type: Boolean, reflect: true }) disabled = false\n @property({ type: Boolean, reflect: true }) required = false\n\n private get _isDisabled(): boolean {\n return (\n this.disabled || this._formAssociation.isFormDisabled() || this._groupManagement.getContext()?.disabled === true\n )\n }\n\n private get _isManagedByGroup(): boolean {\n return this._groupManagement.getContext() !== undefined\n }\n\n private get _isTabStop(): boolean {\n if (this._isDisabled) return false\n if (!this._isManagedByGroup) return true\n return this._groupManagement.getContext()?.rovingTabStop === true\n }\n\n private readonly _formAssociation = defineFormAssociation<boolean>({\n host: this,\n initialize: () => {\n if (this.hasAttribute('checked')) this._checked = true\n },\n getState: () => this._checked,\n setState: checked => {\n this.checked = checked\n },\n getFormValue: () => (!this._isManagedByGroup && this._checked ? this.value || 'on' : null),\n getFormState: () => String(this._checked),\n restoreState: state => {\n if (typeof state === 'string') this.checked = state === 'true'\n },\n isStateManaged: () => this._isManagedByGroup,\n syncValidity: () => this._syncValidity()\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n override focus(options?: FocusOptions): void {\n if (this._isDisabled) return\n this.shadowRoot?.querySelector<HTMLElement>('[role=\"radio\"]')?.focus(options)\n }\n\n private readonly _labelEmptiness = new LabelEmptinessController(this, '.wui-radio-label', empty => {\n this._labelEmpty = empty\n })\n\n override updated() {\n const isManagedByGroup = this._isManagedByGroup\n const isDisabled = this._isDisabled\n if (isManagedByGroup && (!this._wasManagedByGroup || this._renderedDisabled !== isDisabled)) {\n queueMicrotask(() => this.dispatchEvent(new Event(GROUP_MEMBER_DISABLED_CHANGE_EVENT)))\n }\n this._wasManagedByGroup = isManagedByGroup\n this._renderedDisabled = isDisabled\n }\n\n private _syncValidity() {\n const internals = this._formAssociation.getInternals()\n if (!internals || typeof internals.setValidity !== 'function') return\n if (this._isDisabled || this._isManagedByGroup || !this.required || this._checked) {\n internals.setValidity({})\n return\n }\n internals.setValidity({ valueMissing: true }, 'Select an option')\n }\n\n private handleClick() {\n if (this._isDisabled || this._checked) return\n this._checked = true\n this._formAssociation.sync()\n this.requestUpdate('checked', false)\n // group-managed 时事件不冒泡/不组合,由 group 统一派发一次 host 事件\n const opts: EventInit = this._isManagedByGroup\n ? { bubbles: false, composed: false }\n : { bubbles: true, composed: true }\n this.dispatchEvent(new Event('input', opts))\n this.dispatchEvent(new Event('change', opts))\n }\n\n private handleKeyDown(e: KeyboardEvent) {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault()\n this.handleClick()\n }\n }\n\n override render() {\n const cls = {\n 'wui-radio': true,\n 'is-checked': this._checked,\n 'is-label-empty': this._labelEmpty,\n 'is-disabled': this._isDisabled\n }\n\n return html`\n <span\n class=${classMap(cls)}\n tabindex=${this._isTabStop ? '0' : '-1'}\n role=\"radio\"\n aria-checked=${String(this._checked)}\n aria-disabled=${String(this._isDisabled)}\n @click=${this.handleClick}\n @keydown=${this.handleKeyDown}\n >\n <span class=\"wui-radio-circle\">\n <span class=\"wui-radio-dot\"></span>\n </span>\n <span class=\"wui-radio-label\"><slot></slot></span>\n </span>\n `\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-radio': WebUiRadio\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
//#region src/components/radio/interaction.ts
|
|
2
|
+
function e(e) {
|
|
3
|
+
return e.renderRoot?.querySelector("[role=\"radio\"]") ?? null;
|
|
4
|
+
}
|
|
5
|
+
function t(t) {
|
|
6
|
+
e(t)?.focus();
|
|
7
|
+
}
|
|
8
|
+
function n(t) {
|
|
9
|
+
return t.disabled || e(t)?.getAttribute("aria-disabled") === "true";
|
|
10
|
+
}
|
|
11
|
+
function r(t) {
|
|
12
|
+
e(t)?.click();
|
|
13
|
+
}
|
|
14
|
+
//#endregion
|
|
15
|
+
export { r as activateRadio, t as focusRadio, n as isRadioDisabled };
|
|
16
|
+
|
|
17
|
+
//# sourceMappingURL=interaction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interaction.js","names":[],"sources":["../../../src/components/radio/interaction.ts"],"sourcesContent":["import type { WebUiRadio } from './index'\n\nfunction getRadioControl(radio: WebUiRadio): HTMLElement | null {\n return radio.renderRoot?.querySelector<HTMLElement>('[role=\"radio\"]') ?? null\n}\n\nexport function focusRadio(radio: WebUiRadio): void {\n getRadioControl(radio)?.focus()\n}\n\nexport function isRadioDisabled(radio: WebUiRadio): boolean {\n return radio.disabled || getRadioControl(radio)?.getAttribute('aria-disabled') === 'true'\n}\n\nexport function activateRadio(radio: WebUiRadio): void {\n getRadioControl(radio)?.click()\n}\n"],"mappings":";AAEA,SAAS,EAAgB,GAAuC;CAC9D,OAAO,EAAM,YAAY,cAA2B,kBAAgB,KAAK;AAC3E;AAEA,SAAgB,EAAW,GAAyB;CAClD,EAAgB,CAAK,CAAC,EAAE,MAAM;AAChC;AAEA,SAAgB,EAAgB,GAA4B;CAC1D,OAAO,EAAM,YAAY,EAAgB,CAAK,CAAC,EAAE,aAAa,eAAe,MAAM;AACrF;AAEA,SAAgB,EAAc,GAAyB;CACrD,EAAgB,CAAK,CAAC,EAAE,MAAM;AAChC"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
//#region src/components/radio/style.css?inline
|
|
2
|
-
var e = ".wui-radio-circle{border-radius:var(--wui-radius-control,calc(infinity * 1px))}.is-checked .wui-radio-circle{background-color:var(--wui-color-accent,#
|
|
2
|
+
var e = ".wui-radio-circle{border-radius:var(--wui-radius-control,calc(infinity * 1px))}.is-checked .wui-radio-circle{background-color:var(--wui-color-accent,#0071e3)}.wui-radio-dot{border-radius:var(--wui-radius-control,calc(infinity * 1px));opacity:0;background-color:var(--wui-color-on-control,#fff);width:6px;height:6px;box-shadow:inset 0 -1px 2px #0000001f, inset 0 1px 2px #ffffff80, 0 0 4px color-mix(in srgb, var(--wui-color-accent,#0071e3) 30%, transparent);transition:opacity var(--wui-duration-trigger,.16s) ease, transform var(--wui-duration-trigger,.16s) var(--wui-ease-enter,cubic-bezier(.23, 1, .32, 1));transform:scale(0)}.is-checked .wui-radio-dot{opacity:1;transform:scale(1)}@media (prefers-reduced-motion:reduce){.wui-radio-dot,.is-checked .wui-radio-dot{transform:none}}";
|
|
3
3
|
//#endregion
|
|
4
4
|
export { e as default };
|
|
5
5
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/radio/style.css?inline"],"sourcesContent":[".wui-radio-circle {\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n}\n\n.is-checked .wui-radio-circle {\n background-color: var(--wui-color-accent, #
|
|
1
|
+
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/radio/style.css?inline"],"sourcesContent":[".wui-radio-circle {\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n}\n\n.is-checked .wui-radio-circle {\n background-color: var(--wui-color-accent, #0071e3);\n}\n\n.wui-radio-dot {\n transform: scale(0);\n\n width: 6px;\n height: 6px;\n border-radius: var(--wui-radius-control, calc(infinity * 1px));\n\n opacity: 0;\n background-color: var(--wui-color-on-control, #fff);\n box-shadow:\n inset 0 -1px 2px rgb(0 0 0 / 0.12),\n inset 0 1px 2px rgb(255 255 255 / 0.5),\n /*\n * 外发光跟着 accent,而不是 --wui-color-focus-ring:这是选中态的装饰性辉光,\n * 与键盘焦点环是两种东西(alpha 也不同:0.3 vs 0.4/0.62)。默认值下与原值等价。\n */\n 0 0 4px color-mix(in srgb, var(--wui-color-accent, #0071e3) 30%, transparent);\n\n transition:\n opacity var(--wui-duration-trigger, 160ms) ease,\n transform var(--wui-duration-trigger, 160ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n}\n\n.is-checked .wui-radio-dot {\n transform: scale(1);\n opacity: 1;\n}\n\n@media (prefers-reduced-motion: reduce) {\n .wui-radio-dot,\n .is-checked .wui-radio-dot {\n transform: none;\n }\n}\n"],"mappings":""}
|
|
@@ -368,10 +368,16 @@ export declare class WebUiRadioGroup extends WebUiRadioGroup_base {
|
|
|
368
368
|
name: string;
|
|
369
369
|
disabled: boolean;
|
|
370
370
|
required: boolean;
|
|
371
|
+
ariaLabel: string | null;
|
|
372
|
+
ariaLabelledby: string | undefined;
|
|
371
373
|
get direction(): 'horizontal' | 'vertical';
|
|
372
374
|
set direction(v: string);
|
|
373
375
|
private _direction;
|
|
374
376
|
private _value;
|
|
377
|
+
private _getRadios;
|
|
378
|
+
private _findSelectedRadio;
|
|
379
|
+
private _getRovingTabStop;
|
|
380
|
+
private _handleKeyDown;
|
|
375
381
|
private readonly _groupController;
|
|
376
382
|
/**
|
|
377
383
|
* 成员被移除后,组值若仍指向它就成了幽灵值(表单照旧提交一个不存在的 option)。
|
|
@@ -379,12 +385,28 @@ export declare class WebUiRadioGroup extends WebUiRadioGroup_base {
|
|
|
379
385
|
* 否则 v-if / 异步渲染时先于成员到达的合法组值会被误清。
|
|
380
386
|
*/
|
|
381
387
|
private _knownMemberValues;
|
|
388
|
+
private _availabilityObserver;
|
|
389
|
+
private _availabilityResizeObserver;
|
|
390
|
+
private _availabilityToggleTargets;
|
|
391
|
+
private _availabilitySlotTargets;
|
|
392
|
+
private _unsubscribeDocumentAvailability;
|
|
393
|
+
private readonly _handleAvailabilityToggle;
|
|
394
|
+
private readonly _handleAvailabilitySlotChange;
|
|
395
|
+
private _getComposedAncestors;
|
|
382
396
|
private _pruneRemovedMembers;
|
|
383
397
|
get value(): string;
|
|
384
398
|
set value(v: string);
|
|
385
399
|
private get _isDisabled();
|
|
386
400
|
private readonly _formAssociation;
|
|
387
401
|
private readonly _formAssociationController;
|
|
402
|
+
connectedCallback(): void;
|
|
403
|
+
disconnectedCallback(): void;
|
|
404
|
+
updated(): void;
|
|
405
|
+
private _syncHostSemantics;
|
|
406
|
+
private _isRadioUnavailable;
|
|
407
|
+
private _observeAvailabilityAncestors;
|
|
408
|
+
private _syncAvailabilityToggleTargets;
|
|
409
|
+
private _syncAvailabilitySlotTargets;
|
|
388
410
|
formDisabledCallback(disabled: boolean): void;
|
|
389
411
|
private _syncValidity;
|
|
390
412
|
render(): import('lit').TemplateResult<1>;
|