@greypan/web-ui 10.2.0 → 11.0.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 +68 -68
- package/README.md +43 -43
- package/dist/assets/glass.js.map +1 -1
- package/dist/assets/menu-portal.js.map +1 -1
- package/dist/assets/modal-surface.js.map +1 -1
- package/dist/assets/overlay-motion.js.map +1 -1
- package/dist/assets/selection-control.js.map +1 -1
- package/dist/components/autocomplete/index.d.ts +6 -6
- package/dist/components/autocomplete/index.js +1 -1
- package/dist/components/autocomplete/index.js.map +1 -1
- package/dist/components/autocomplete/style.js.map +1 -1
- package/dist/components/avatar/index.js.map +1 -1
- package/dist/components/back-top/index.js.map +1 -1
- package/dist/components/back-top/style.js.map +1 -1
- package/dist/components/badge/index.js +1 -1
- package/dist/components/badge/index.js.map +1 -1
- package/dist/components/button/index.js.map +1 -1
- package/dist/components/button/style.js.map +1 -1
- package/dist/components/checkbox/index.d.ts +1 -1
- package/dist/components/checkbox/index.js +1 -1
- package/dist/components/checkbox/index.js.map +1 -1
- package/dist/components/checkbox/style.js.map +1 -1
- package/dist/components/checkbox-group/index.d.ts +9 -0
- package/dist/components/checkbox-group/index.js +8 -2
- package/dist/components/checkbox-group/index.js.map +1 -1
- package/dist/components/collapse/index.d.ts +5 -5
- package/dist/components/collapse/index.js.map +1 -1
- package/dist/components/collapse/style.js.map +1 -1
- package/dist/components/context-menu/index.d.ts +60 -60
- package/dist/components/context-menu/index.js +2 -2
- package/dist/components/context-menu/index.js.map +1 -1
- package/dist/components/context-menu/scrim.js.map +1 -1
- package/dist/components/context-menu/style.js.map +1 -1
- package/dist/components/dialog/index.d.ts +4 -4
- package/dist/components/dialog/index.js +1 -1
- package/dist/components/dialog/index.js.map +1 -1
- package/dist/components/dialog/style.js.map +1 -1
- package/dist/components/drawer/index.d.ts +8 -8
- package/dist/components/drawer/index.js +1 -1
- package/dist/components/drawer/index.js.map +1 -1
- package/dist/components/drawer/style.js.map +1 -1
- package/dist/components/dropdown/index.d.ts +6 -6
- package/dist/components/dropdown/index.js.map +1 -1
- package/dist/components/dropdown-item/index.js.map +1 -1
- package/dist/components/editable-text/index.d.ts +2 -2
- package/dist/components/editable-text/index.js.map +1 -1
- package/dist/components/editable-text/style.js.map +1 -1
- package/dist/components/empty/index.d.ts +3 -3
- package/dist/components/empty/index.js.map +1 -1
- package/dist/components/image-preview/index.d.ts +5 -5
- package/dist/components/image-preview/index.js +7 -7
- package/dist/components/image-preview/index.js.map +1 -1
- package/dist/components/image-preview/style.js.map +1 -1
- package/dist/components/input/index.js +4 -1
- package/dist/components/input/index.js.map +1 -1
- package/dist/components/input/style.js.map +1 -1
- package/dist/components/input-number/index.d.ts +6 -0
- package/dist/components/input-number/index.js +27 -21
- package/dist/components/input-number/index.js.map +1 -1
- package/dist/components/layout/index.d.ts +7 -7
- package/dist/components/layout/index.js +4 -4
- package/dist/components/layout/index.js.map +1 -1
- package/dist/components/layout/style.js.map +1 -1
- package/dist/components/middle-ellipsis/index.d.ts +1 -1
- package/dist/components/middle-ellipsis/index.js.map +1 -1
- package/dist/components/middle-ellipsis/truncate.d.ts +1 -1
- package/dist/components/middle-ellipsis/truncate.js.map +1 -1
- package/dist/components/popover/index.d.ts +1 -1
- package/dist/components/popover/index.js.map +1 -1
- package/dist/components/popover/style.js.map +1 -1
- package/dist/components/radio/index.d.ts +1 -1
- package/dist/components/radio/index.js +1 -1
- package/dist/components/radio/index.js.map +1 -1
- package/dist/components/radio-group/index.d.ts +7 -0
- package/dist/components/radio-group/index.js +21 -3
- package/dist/components/radio-group/index.js.map +1 -1
- package/dist/components/segmented/index.js.map +1 -1
- package/dist/components/segmented/style.js.map +1 -1
- package/dist/components/segmented-trigger/style.js.map +1 -1
- package/dist/components/select/index.js +1 -1
- package/dist/components/select/index.js.map +1 -1
- package/dist/components/select/style.js.map +1 -1
- package/dist/components/slider/index.js +3 -3
- 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.js +1 -1
- package/dist/components/spinner/index.js.map +1 -1
- package/dist/components/switch/index.d.ts +3 -0
- package/dist/components/switch/index.js +26 -8
- package/dist/components/switch/index.js.map +1 -1
- package/dist/components/switch/style.js.map +1 -1
- package/dist/components/textarea/index.js +1 -1
- package/dist/components/textarea/index.js.map +1 -1
- package/dist/components/textarea/style.js.map +1 -1
- package/dist/components/theme/index.js.map +1 -1
- package/dist/components/theme/style.js.map +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 -1
- package/dist/components/tooltip/index.js.map +1 -1
- package/dist/components/tooltip/style.js.map +1 -1
- package/dist/scrollbars/index.d.ts +8 -8
- package/dist/scrollbars/index.js.map +1 -1
- package/dist/scrollbars/internal.js.map +1 -1
- package/dist/shared/element-height/index.js.map +1 -1
- package/dist/shared/focus/pointer-focus.js.map +1 -1
- package/dist/shared/group-management/index.js +5 -1
- package/dist/shared/group-management/index.js.map +1 -1
- package/dist/shared/label-emptiness/index.js.map +1 -1
- package/dist/shared/menu-behavior/index.js.map +1 -1
- package/dist/shared/menu-portal/menu-portal.js.map +1 -1
- package/dist/shared/menu-portal/menu-tree.js.map +1 -1
- package/dist/shared/option-portal/combobox-shell.js.map +1 -1
- package/dist/shared/option-portal/index.js.map +1 -1
- package/dist/shared/overlay/anchored-panel.js.map +1 -1
- package/dist/shared/overlay/native-dialog-presence.js.map +1 -1
- package/dist/shared/overlay/nested-drawer-layers.js.map +1 -1
- package/dist/shared/overlay/open-overlay.js.map +1 -1
- package/dist/shared/overlay/overlay.js.map +1 -1
- package/dist/shared/overlay/portal.js.map +1 -1
- package/dist/shared/overlay/presence.js.map +1 -1
- package/dist/shared/scroll-lock/scroll-lock.js.map +1 -1
- package/dist/shared/theme/root-sync.js.map +1 -1
- package/package.json +2 -2
- package/dist/icons/generated/heroicons-arrow-down-16-solid.d.ts +0 -6
- package/dist/icons/generated/heroicons-arrow-up-16-solid.d.ts +0 -6
- package/dist/icons/generated/jam-volume.d.ts +0 -8
- package/dist/icons/generated/jam-watch-f.d.ts +0 -8
- package/dist/icons/generated/ooui-check.d.ts +0 -6
- package/dist/icons/generated/radix-icons-check.d.ts +0 -6
- package/dist/icons/generated/si-check-alt-fill.d.ts +0 -6
|
@@ -33,7 +33,7 @@ var m = ["horizontal", "vertical"], h = class extends t(c) {
|
|
|
33
33
|
composed: !0
|
|
34
34
|
}));
|
|
35
35
|
}
|
|
36
|
-
}).make()), this._formAssociation = r({
|
|
36
|
+
}).make(), { afterSync: () => this._pruneRemovedMembers() }), this._knownMemberValues = /* @__PURE__ */ new Set(), this._formAssociation = r({
|
|
37
37
|
host: this,
|
|
38
38
|
initialize: () => {
|
|
39
39
|
let e = this.getAttribute("value");
|
|
@@ -69,6 +69,12 @@ var m = ["horizontal", "vertical"], h = class extends t(c) {
|
|
|
69
69
|
let t = this._direction;
|
|
70
70
|
this._direction = o(e, m, "vertical"), this.requestUpdate("direction", t);
|
|
71
71
|
}
|
|
72
|
+
_pruneRemovedMembers() {
|
|
73
|
+
let e = [...this.querySelectorAll("web-ui-checkbox")];
|
|
74
|
+
if (e.length === 0) return;
|
|
75
|
+
let t = new Set(e.map((e) => e.value)), n = this._value.filter((e) => t.has(e) || !this._knownMemberValues.has(e));
|
|
76
|
+
this._knownMemberValues = t, n.length !== this._value.length && (this.value = n);
|
|
77
|
+
}
|
|
72
78
|
get value() {
|
|
73
79
|
return this._value;
|
|
74
80
|
}
|
|
@@ -89,7 +95,7 @@ var m = ["horizontal", "vertical"], h = class extends t(c) {
|
|
|
89
95
|
e.setValidity({});
|
|
90
96
|
return;
|
|
91
97
|
}
|
|
92
|
-
e.setValidity({ valueMissing: !0 }, "
|
|
98
|
+
e.setValidity({ valueMissing: !0 }, "Select at least one option");
|
|
93
99
|
}
|
|
94
100
|
}
|
|
95
101
|
render() {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/checkbox-group/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport { defineGroupCoordinator, GroupController } from '@/shared/group-management'\nimport { normalizeLiteral } from '@/shared/normalize'\n\nimport type { WebUiCheckbox } from '../checkbox'\n\nimport style from './style.css?inline'\n\nconst ALLOWED_DIRECTIONS = ['horizontal', 'vertical'] as const\n\n@customElement('web-ui-checkbox-group')\nexport class WebUiCheckboxGroup extends FormAssociated(LitElement) {\n static override styles = unsafeCSS(style)\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 @property({ type: String, reflect: true })\n get direction(): 'horizontal' | 'vertical' {\n return this._direction\n }\n set direction(v: string) {\n const old = this._direction\n this._direction = normalizeLiteral(v, ALLOWED_DIRECTIONS, 'vertical')\n this.requestUpdate('direction', old)\n }\n private _direction: 'horizontal' | 'vertical' = 'vertical'\n\n @state() private _value: string[] = []\n\n private readonly _groupController = new GroupController(\n this,\n defineGroupCoordinator<WebUiCheckbox, string[]>({\n host: this,\n getItems: () => [...this.querySelectorAll<WebUiCheckbox>('web-ui-checkbox')],\n getValue: () => this._value,\n setValue: value => {\n this.value = value\n },\n getDisabled: () => this._isDisabled,\n isItem: (target): target is WebUiCheckbox => target instanceof HTMLElement && target.matches('web-ui-checkbox'),\n isItemSelected: (checkbox, value) => value.includes(checkbox.value),\n getNextValue: (checkbox, value) =>\n checkbox.checked ? [...new Set([...value, checkbox.value])] : value.filter(item => item !== checkbox.value),\n valuesEqual: (a, b) => a.length === b.length && a.every((value, index) => value === b[index]),\n copyValue: value => [...value],\n setItemSelected: (checkbox, selected) => {\n checkbox.checked = selected\n },\n dispatchValueChange: () => {\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n }).make()\n )\n\n get value(): string[] {\n return this._value\n }\n\n set value(v: string[]) {\n const old = this._value\n this._value = [...v]\n this._formAssociation.sync()\n this.requestUpdate('value', old)\n }\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n private readonly _formAssociation = defineFormAssociation<string[]>({\n host: this,\n initialize: () => {\n const attributeValue = this.getAttribute('value')\n if (attributeValue !== null) this.value = attributeValue.split(',').filter(Boolean)\n },\n getState: () => this._value,\n setState: value => {\n this.value = value\n },\n copyState: value => [...value],\n getFormValue: () => {\n if (!this.name || this._value.length === 0) return null\n const formData = new FormData()\n this._value.forEach(value => formData.append(this.name, value))\n return formData\n },\n getFormState: () => JSON.stringify(this._value),\n restoreState: state => {\n if (typeof state !== 'string') return\n try {\n const value = JSON.parse(state)\n if (Array.isArray(value) && value.every(item => typeof item === 'string')) this.value = value\n } catch {\n // 忽略无法解析的历史表单状态。\n }\n },\n syncValidity: () => this._syncValidity()\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n override formDisabledCallback(disabled: boolean) {\n this._formAssociation.setDisabled(disabled)\n this._groupController.sync()\n }\n\n private _syncValidity() {\n const internals = this._formAssociation.getInternals()\n if (!internals || typeof internals.setValidity !== 'function') return\n if (this._isDisabled || !this.required || this._value.length > 0) {\n internals.setValidity({})\n return\n }\n internals.setValidity({ valueMissing: true }, '
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/checkbox-group/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, state } from 'lit/decorators.js'\n\nimport { FormAssociated, defineFormAssociation, FormAssociationController } from '@/shared/form-association'\nimport { defineGroupCoordinator, GroupController } from '@/shared/group-management'\nimport { normalizeLiteral } from '@/shared/normalize'\n\nimport type { WebUiCheckbox } from '../checkbox'\n\nimport style from './style.css?inline'\n\nconst ALLOWED_DIRECTIONS = ['horizontal', 'vertical'] as const\n\n@customElement('web-ui-checkbox-group')\nexport class WebUiCheckboxGroup extends FormAssociated(LitElement) {\n static override styles = unsafeCSS(style)\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 @property({ type: String, reflect: true })\n get direction(): 'horizontal' | 'vertical' {\n return this._direction\n }\n set direction(v: string) {\n const old = this._direction\n this._direction = normalizeLiteral(v, ALLOWED_DIRECTIONS, 'vertical')\n this.requestUpdate('direction', old)\n }\n private _direction: 'horizontal' | 'vertical' = 'vertical'\n\n @state() private _value: string[] = []\n\n private readonly _groupController = new GroupController(\n this,\n defineGroupCoordinator<WebUiCheckbox, string[]>({\n host: this,\n getItems: () => [...this.querySelectorAll<WebUiCheckbox>('web-ui-checkbox')],\n getValue: () => this._value,\n setValue: value => {\n this.value = value\n },\n getDisabled: () => this._isDisabled,\n isItem: (target): target is WebUiCheckbox => target instanceof HTMLElement && target.matches('web-ui-checkbox'),\n isItemSelected: (checkbox, value) => value.includes(checkbox.value),\n getNextValue: (checkbox, value) =>\n checkbox.checked ? [...new Set([...value, checkbox.value])] : value.filter(item => item !== checkbox.value),\n valuesEqual: (a, b) => a.length === b.length && a.every((value, index) => value === b[index]),\n copyValue: value => [...value],\n setItemSelected: (checkbox, selected) => {\n checkbox.checked = selected\n },\n dispatchValueChange: () => {\n this.dispatchEvent(new Event('input', { bubbles: true, composed: true }))\n this.dispatchEvent(new Event('change', { bubbles: true, composed: true }))\n }\n }).make(),\n // 每次成员集合同步后把组值剪到现存成员范围内\n { afterSync: () => this._pruneRemovedMembers() }\n )\n\n /**\n * 成员被移除后,它的 value 会作为不可见、也取消不掉的幽灵值留在组值里,\n * 并能继续进 FormData。这里以「曾经出现在成员里、现在消失了」为剪枝条件。\n *\n * 不能用「不在现存成员里就剪」:v-if / 异步渲染下组值常常先于成员出现,\n * 那样会把合法的预设值当成幽灵值清掉。\n */\n private _knownMemberValues = new Set<string>()\n\n private _pruneRemovedMembers() {\n const items = [...this.querySelectorAll<WebUiCheckbox>('web-ui-checkbox')]\n if (items.length === 0) return\n const alive = new Set(items.map(item => item.value))\n const next = this._value.filter(value => alive.has(value) || !this._knownMemberValues.has(value))\n this._knownMemberValues = alive\n if (next.length !== this._value.length) this.value = next\n }\n\n get value(): string[] {\n return this._value\n }\n\n set value(v: string[]) {\n const old = this._value\n this._value = [...v]\n this._formAssociation.sync()\n this.requestUpdate('value', old)\n }\n\n private get _isDisabled(): boolean {\n return this.disabled || this._formAssociation.isFormDisabled()\n }\n\n private readonly _formAssociation = defineFormAssociation<string[]>({\n host: this,\n initialize: () => {\n const attributeValue = this.getAttribute('value')\n if (attributeValue !== null) this.value = attributeValue.split(',').filter(Boolean)\n },\n getState: () => this._value,\n setState: value => {\n this.value = value\n },\n copyState: value => [...value],\n getFormValue: () => {\n if (!this.name || this._value.length === 0) return null\n const formData = new FormData()\n this._value.forEach(value => formData.append(this.name, value))\n return formData\n },\n getFormState: () => JSON.stringify(this._value),\n restoreState: state => {\n if (typeof state !== 'string') return\n try {\n const value = JSON.parse(state)\n if (Array.isArray(value) && value.every(item => typeof item === 'string')) this.value = value\n } catch {\n // 忽略无法解析的历史表单状态。\n }\n },\n syncValidity: () => this._syncValidity()\n }).make()\n\n private readonly _formAssociationController = new FormAssociationController(this, this._formAssociation)\n\n override formDisabledCallback(disabled: boolean) {\n this._formAssociation.setDisabled(disabled)\n this._groupController.sync()\n }\n\n private _syncValidity() {\n const internals = this._formAssociation.getInternals()\n if (!internals || typeof internals.setValidity !== 'function') return\n if (this._isDisabled || !this.required || this._value.length > 0) {\n internals.setValidity({})\n return\n }\n internals.setValidity({ valueMissing: true }, 'Select at least one option')\n }\n\n override render() {\n return html`<div class=\"wui-checkbox-group\"><slot></slot></div>`\n }\n\n declare readonly $events: {\n input: Event\n change: Event\n }\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'web-ui-checkbox-group': WebUiCheckboxGroup\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { LitElement } from 'lit';
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* 文档流内 collapse panel:trigger 经默认 slot 提供,内容经 `slot="content"` 提供。
|
|
4
4
|
*
|
|
5
5
|
* trigger 语义完全来自 slot 进来的交互元素(原生 button / web-ui-button 等):
|
|
6
6
|
* Enter/Space 激活走该元素的原生 click,不做 role="button" 补齐;组件把
|
|
@@ -12,12 +12,12 @@ import { LitElement } from 'lit';
|
|
|
12
12
|
* 选型保留)。关闭稳态三态(消费者 light DOM 永不移动,hidden 全部命令式管理,
|
|
13
13
|
* 避免渲染绑定在关闭动画起点抢先 display:none):
|
|
14
14
|
* - 默认:content 容器 `hidden`(display:none,脱离渲染与可访问性树);
|
|
15
|
-
* - `keep-mounted`:inner `inert`(保留在 0fr
|
|
16
|
-
* - `peek`:inner `inert
|
|
15
|
+
* - `keep-mounted`:inner `inert`(保留在 0fr track 内,滚动位置与布局可测量);
|
|
16
|
+
* - `peek`:inner `inert`,track 停在固定长度的裁剪窗口,只露出内容头部;
|
|
17
17
|
* - 动画进行中:inner `inert` + track pointer-events 禁用,结束后进入对应稳态。
|
|
18
18
|
*
|
|
19
19
|
* `peek` 与展开态之间是固定长度与内容自适应高度的过渡,CSS 无法插值;这两个
|
|
20
|
-
*
|
|
20
|
+
* 方向的动画改为读出像素后以显式长度写入 track,落稳态时清除内联值交回 CSS 规则。
|
|
21
21
|
*/
|
|
22
22
|
export declare class WebUiCollapse extends LitElement {
|
|
23
23
|
static styles: import('lit').CSSResult;
|
|
@@ -33,7 +33,7 @@ export declare class WebUiCollapse extends LitElement {
|
|
|
33
33
|
* 关闭稳态露出的尺寸(CSS 长度,如 `120px`、`6rem`),沿动画轴生效:默认是高度,
|
|
34
34
|
* `horizontal` 时为宽度;未设置时保持完全收起。
|
|
35
35
|
*
|
|
36
|
-
* 设置后关闭稳态语义等同 `keep-mounted
|
|
36
|
+
* 设置后关闭稳态语义等同 `keep-mounted`(内容留在 track 内、`inert` 阻断交互),
|
|
37
37
|
* 只露出 `peek` 范围内的部分;内容本身不足 `peek` 时按内容实际尺寸收起。
|
|
38
38
|
* 注意 `peek` 是固定长度与内容自适应高度之间的过渡:CSS 无法在两者之间插值,
|
|
39
39
|
* 因此这两个方向的动画由本组件读出内容尺寸后以显式长度驱动(其余路径仍是
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/collapse/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\n\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { getTransitionDuration } from '@/shared/overlay/presence'\n\nimport style from './style.css?inline'\n\nconst EXIT_FALLBACK_BUFFER = 80\n\n/** peek 边缘渐隐的活动长度:关闭/收起态取由 peek 推导的长度,展开态归 0。 */\nconst PEEK_EDGE_ACTIVE = '--wui-collapse-peek-edge-active'\n\n/*\n * 注册成 `<length>` 才能被 transition 插值(未注册的自定义属性是离散值,只能瞬变)。\n *\n * 必须用 JS 的 `CSS.registerProperty` 而不是 CSS 里的 `@property`:实测后者写在\n * shadow root 的样式表里不产生注册(getComputedStyle 读打开态得到空串而非\n * initial-value),而自定义属性的注册本质上是 document 级的。\n * 注册失败(jsdom、旧浏览器、重复注册)时降级为离散过渡——渐变带长度瞬变,\n * 但位置百分比仍跟随裁剪边缘,行为依旧正确。\n *\n * `inherits` 必须是 true:晕染伪元素(style.css 的 ::after)读同一个值来调制模糊,\n * 继承为 false 时伪元素读到的是 initialValue 0px——mask 的各色标随之塌缩到 100%,\n * 模糊层静默消失且其余渲染一切照旧,只少一层效果。\n */\nfunction registerPeekEdgeActive() {\n // jsdom 与不支持的环境直接跳过;样式里的 var() fallback 保证声明仍然有效。\n if (typeof CSS === 'undefined' || typeof CSS.registerProperty !== 'function') return\n try {\n CSS.registerProperty({\n name: PEEK_EDGE_ACTIVE,\n syntax: '<length>',\n inherits: true,\n initialValue: '0px'\n })\n } catch {\n // 已注册过(HMR、多份副本)或定义冲突:保持现有注册即可。\n }\n}\n\nregisterPeekEdgeActive()\n\nlet collapseIdCounter = 0\n\n/**\n * 文档流内折叠面板:trigger 经默认 slot 提供,内容经 `slot=\"content\"` 提供。\n *\n * trigger 语义完全来自 slot 进来的交互元素(原生 button / web-ui-button 等):\n * Enter/Space 激活走该元素的原生 click,不做 role=\"button\" 补齐;组件把\n * `aria-expanded` / `aria-controls` / `aria-disabled` 回写到首个 assigned\n * element(与 popover trigger 的既定回写模式一致)。wrapper 仅负责点击代理,\n * 不可聚焦,不承载 ARIA。\n *\n * 内容区在 shadow 内以 grid `0fr ↔ 1fr` track 过渡驱动高度/宽度动画(ADR-0030\n * 选型保留)。关闭稳态三态(消费者 light DOM 永不移动,hidden 全部命令式管理,\n * 避免渲染绑定在关闭动画起点抢先 display:none):\n * - 默认:content 容器 `hidden`(display:none,脱离渲染与可访问性树);\n * - `keep-mounted`:inner `inert`(保留在 0fr 轨道内,滚动位置与布局可测量);\n * - `peek`:inner `inert`,轨道停在固定长度的裁剪窗口,只露出内容头部;\n * - 动画进行中:inner `inert` + track pointer-events 禁用,结束后进入对应稳态。\n *\n * `peek` 与展开态之间是固定长度与内容自适应高度的过渡,CSS 无法插值;这两个\n * 方向的动画改为读出像素后以显式长度写入轨道,落稳态时清除内联值交回 CSS 规则。\n */\n@customElement('web-ui-collapse')\nexport class WebUiCollapse extends LitElement {\n static override styles = unsafeCSS(style)\n\n /** 展开状态;严格受控,唯一状态源。 */\n @property({ type: Boolean, reflect: true }) open = false\n\n /** 禁用触发:点击忽略、aria-disabled 写入 trigger 元素;已展开内容保持现状。 */\n @property({ type: Boolean, reflect: true }) disabled = false\n\n /** 水平方向:true 时沿宽度展开,默认沿高度。 */\n @property({ type: Boolean, reflect: true }) horizontal = false\n\n /** 关闭稳态保持内容 `inert` 而非 `hidden`(保留滚动位置,可测量)。 */\n @property({ type: Boolean, reflect: true, attribute: 'keep-mounted' }) keepMounted = false\n\n /**\n * 关闭稳态露出的尺寸(CSS 长度,如 `120px`、`6rem`),沿动画轴生效:默认是高度,\n * `horizontal` 时为宽度;未设置时保持完全收起。\n *\n * 设置后关闭稳态语义等同 `keep-mounted`(内容留在轨道内、`inert` 阻断交互),\n * 只露出 `peek` 范围内的部分;内容本身不足 `peek` 时按内容实际尺寸收起。\n * 注意 `peek` 是固定长度与内容自适应高度之间的过渡:CSS 无法在两者之间插值,\n * 因此这两个方向的动画由本组件读出内容尺寸后以显式长度驱动(其余路径仍是\n * 零测量的 grid fr 过渡)。\n *\n * 露出区域末端自带一段边缘渐隐(提示下方还有内容),长度由 `peek` 按比例推导,\n * 无需也不接受调用方单独指定——见 style.css 中 `--wui-collapse-peek-edge-*`\n * 系列变量(比例、上限、末端颜色可用 CSS 覆盖,属于主题级调优而非组件 API)。\n */\n @property({ type: String, reflect: true }) peek: string | null = null\n\n @query('.wui-collapse-trigger-wrapper') private _triggerWrapper!: HTMLElement\n\n private _contentId = `wui-collapse-content-${++collapseIdCounter}`\n\n private readonly _userOpenChange = new UserChangeController()\n\n private _exitTimer?: ReturnType<typeof setTimeout>\n // 动画代际:每次管线启动自增,旧管线的 transitionend/兜底定时器/rAF 全部失效。\n private _generation = 0\n private _lastOpen?: boolean\n\n override connectedCallback() {\n super.connectedCallback()\n // 点击监听与生命周期同寿(宿主移除即整体销毁),无需 disconnected 清理。\n this.addEventListener('click', this._onTriggerClick)\n // 断连可能打断动画管线(瞬态残留:presence='closing'、容器未 hidden)。\n // 首次连接不必处理(初始 render 走 _lastOpen===undefined 落稳态,请求多余\n // 渲染会触发「scheduled an update after update completed」告警);重连时\n // 重置同步状态并强制一轮 update,由首次同步分支重新落稳态、丢弃在途管线。\n if (this.hasUpdated) {\n this._generation++\n this._clearExitTimer()\n this._lastOpen = undefined\n this.requestUpdate()\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._clearExitTimer()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // ARIA 回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerAria()\n // peek 的稳态长度与动画写入共用轨道内联样式,先同步再落稳态,避免被稳态清除。\n this._syncPeek()\n\n // 首次同步(含 open 为默认值的场景,changed map 不含默认值属性):\n // 直接落到正确稳态,不播动画(popover 初始 open 同款)。\n if (this._lastOpen === undefined) {\n this._lastOpen = this.open\n if (this.open) this._applyOpenSteadyState()\n else this._applyClosedSteadyState()\n return\n }\n\n if ((changed.has('keepMounted') || changed.has('peek')) && !this.open) {\n // 运行态切换 keep-mounted / peek:关闭稳态需在 hidden / inert / 裁剪长度间重新落地。\n this._applyClosedSteadyState()\n }\n if (!changed.has('open') || this.open === this._lastOpen) return\n\n this._lastOpen = this.open\n if (this._userOpenChange.consume()) this._dispatchChange(this.open)\n if (this.open) this._beginExpand()\n else this._beginCollapse()\n }\n\n show() {\n if (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 _dispatchChange(open: boolean) {\n dispatchOpenChangeEvent(this, open)\n }\n\n /*\n * 点击代理:冒泡路径穿过 trigger wrapper 的 click 一律切换。语义由 slot 内\n * 的交互元素(原生 button / web-ui-button)提供;slot 放纯文本时无激活语义\n * (README 约定)。内容区与嵌套内层 collapse 的 click 不经过本 wrapper,天然隔离。\n */\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n if (!event.composedPath().includes(this._triggerWrapper)) return\n this._userOpenChange.mark()\n this.toggle()\n }\n\n /*\n * Trigger 元素取首个 assigned element(与 popover `_queryTrigger` 同款惰性查询,\n * 不依赖 slotchange——jsdom 不派发该事件,无法用状态缓存)。slot 为空时无回写目标。\n */\n private _queryTrigger(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('.wui-collapse-trigger-wrapper slot')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n /*\n * slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,由 updated() 的\n * ARIA 回写覆盖晚到的 trigger 元素(slotchange 在 jsdom 不派发,无法监听)。\n */\n private readonly _onTriggerSlotChange = () => {\n this.requestUpdate()\n }\n\n // ARIA 状态回写到 trigger 元素本身(wrapper 不可聚焦,AT 读不到)。\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._contentId)\n if (this.disabled) trigger.setAttribute('aria-disabled', 'true')\n else trigger.removeAttribute('aria-disabled')\n }\n\n // ===== 内容区动画管线(三态语义自 ADR-0030 迁移) =====\n\n /** 关闭稳态是否露出部分内容;空值与非空判断避免 `peek=\"\"` 被当作有效长度。 */\n private get _peeks(): boolean {\n return typeof this.peek === 'string' && this.peek.trim() !== ''\n }\n\n private _applyOpenSteadyState() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (container) container.hidden = false\n if (track) track.dataset.wuiPresence = 'open'\n this._clearInnerClamp()\n this._setInnerInert(false)\n }\n\n private _applyClosedSteadyState() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (track) delete track.dataset.wuiPresence\n this._clearInnerClamp()\n if (this.keepMounted || this._peeks) {\n if (container) container.hidden = false\n this._setInnerInert(true)\n return\n }\n this._setInnerInert(false)\n if (container) container.hidden = true\n }\n\n private _beginExpand() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (!container || !track) return\n\n const generation = this._generation\n // 中断关闭动画时 inner 已 inert:先恢复可交互,内容随展开重新可见。\n this._setInnerInert(false)\n // 从 display:none 进入时无先前计算样式可过渡:提交 0fr 起点后再翻转。\n container.hidden = false\n\n if (this._peeks) {\n const inner = this._contentInner()\n // 无过渡时长(reduced motion、jsdom)或无 inner 时不做测量,直接落展开稳态。\n if (!inner || getTransitionDuration(inner, 0) === 0) {\n this._applyOpenSteadyState()\n return\n }\n /*\n * peek 的裁剪长度是固定值,展开目标是内容自适应高度,两者无法由 CSS 插值,\n * 动画必须由两个显式长度驱动:起点是当前露出的长度,目标是内容完整尺寸。\n * 起点要在同任务内先写入并提交回流,否则过渡会退化成跳变(实测分帧写入\n * 反而完全不参与插值判定);目标读 inner 的 scroll 尺寸,不写探针样式。\n */\n const from = this._measureInnerSize(inner)\n const to = this._measureContentSize(inner)\n this._setInnerClamp(inner, `${from}px`)\n void this._measureInnerSize(inner)\n track.dataset.wuiPresence = 'open'\n this._setInnerClamp(inner, `${to}px`)\n this._awaitTransition(inner, generation, () => this._settle(true), [this._clampProperty()])\n return\n }\n\n void track.offsetWidth\n\n requestAnimationFrame(() => {\n if (generation !== this._generation || !this.open) return\n track.dataset.wuiPresence = 'open'\n this._awaitTransition(track, generation, () => this._settle(true))\n })\n }\n\n private _beginCollapse() {\n this._generation++\n this._clearExitTimer()\n const track = this._contentTrack()\n if (!track) return\n\n // 收起期间立即阻断交互,防止焦点落入正在消失的区域。\n this._setInnerInert(true)\n const generation = this._generation\n\n if (this._peeks) {\n const inner = this._contentInner()\n // 无过渡时长(reduced motion、jsdom)或无 inner 时不做测量,直接落裁剪稳态。\n if (!inner || getTransitionDuration(inner, 0) === 0) {\n this._applyClosedSteadyState()\n return\n }\n /*\n * 收起目标是 `peek` 长度本身:与内容尺寸的较小值由 CSS 稳态规则解析,\n * 动画只需把当前展开长度过渡到这个长度。起点同样要先写入并提交回流,\n * 顺序与 _beginExpand 一致。\n */\n const from = this._measureInnerSize(inner)\n this._setInnerClamp(inner, `${from}px`)\n void this._measureInnerSize(inner)\n track.dataset.wuiPresence = 'closing'\n this._setInnerClamp(inner, `${this.peek}`)\n this._awaitTransition(inner, generation, () => this._settle(false), [this._clampProperty()])\n return\n }\n\n track.dataset.wuiPresence = 'closing'\n this._awaitTransition(track, generation, () => this._settle(false))\n }\n\n // 关闭稳态:keep-mounted → 内容保留在 0fr 轨道内并 inert;peek → 保留在裁剪\n // 窗口内并 inert;默认 → 容器 hidden。\n private _settle(open: boolean) {\n this._clearExitTimer()\n const track = this._contentTrack()\n // peek 过渡以显式长度写入 inner:落稳态一律清除,交回 CSS 稳态规则(长度声明)。\n this._clearInnerClamp()\n if (open) {\n this._setInnerInert(false)\n return\n }\n // 落稳态必须清除 closing 瞬态标记:否则 track 永久保留 pointer-events:none\n // 与 exit 过渡配置(keep-mounted 分支同样清除,避免残留)。\n if (track) delete track.dataset.wuiPresence\n if (this.keepMounted || this._peeks) return\n const container = this._contentContainer()\n this._setInnerInert(false)\n if (container) container.hidden = true\n }\n\n // 等待动画属性过渡结束;时长为 0(reduced motion、jsdom)直接稳态,\n // transitionend 不触发时按时长 + 缓冲兜底。默认等待 grid 轨道过渡;peek 的\n // 过渡落在 inner 的裁剪长度上,由调用方传入对应属性。\n private _awaitTransition(\n animated: HTMLElement,\n generation: number,\n onDone: () => void,\n properties: readonly string[] = ['grid-template-rows', 'grid-template-columns']\n ) {\n const duration = getTransitionDuration(animated, 0)\n if (duration === 0) {\n if (generation === this._generation) onDone()\n return\n }\n\n let settled = false\n const finish = () => {\n animated.removeEventListener('transitionend', onTransitionEnd)\n if (this._exitTimer === timer) this._exitTimer = undefined\n clearTimeout(timer)\n // 过渡可能被属性匹配的后续动画触发(如中断重开):代际不符时静默退场。\n if (settled || generation !== this._generation) return\n settled = true\n onDone()\n }\n const timer = setTimeout(finish, duration + EXIT_FALLBACK_BUFFER)\n this._exitTimer = timer\n\n const onTransitionEnd = (event: TransitionEvent) => {\n if (event.target === animated && properties.includes(event.propertyName)) finish()\n }\n animated.addEventListener('transitionend', onTransitionEnd)\n }\n\n private _clearExitTimer() {\n if (this._exitTimer !== undefined) {\n clearTimeout(this._exitTimer)\n this._exitTimer = undefined\n }\n }\n\n // ===== peek 尺寸读写(裁剪长度落在 inner 上,轨道只负责 1fr 与裁剪窗口) =====\n\n /*\n * peek 长度经自定义属性下发给 shadow 样式规则,而不是直接写 inner 的裁剪长度:\n * 关闭稳态由 CSS 声明(长度与内容尺寸的较小值由 CSS 原生解析),动画写入的内联\n * 长度在其上覆盖,落稳态时清除即可回到声明值。\n */\n private _syncPeek() {\n const track = this._contentTrack()\n if (!track) return\n track.toggleAttribute('data-wui-peek', this._peeks)\n if (this._peeks) {\n track.style.setProperty('--wui-collapse-peek', this.peek ?? '')\n } else {\n track.style.removeProperty('--wui-collapse-peek')\n }\n }\n\n private _clampProperty(): 'max-height' | 'max-width' {\n return this.horizontal ? 'max-width' : 'max-height'\n }\n\n private _measureInnerSize(inner: HTMLElement): number {\n return this.horizontal ? inner.offsetWidth : inner.offsetHeight\n }\n\n /*\n * 内容在动画轴上的完整尺寸(px):裁剪只作用在 inner 的 max-height/max-width 上,\n * 被测内容的布局盒子仍是完整尺寸,故读 scroll 尺寸即可,不写任何样式——写样式\n * 会被 grid 过渡机制当作过渡起点读取,必须避开。\n */\n private _measureContentSize(inner: HTMLElement): number {\n return this.horizontal ? inner.scrollWidth : inner.scrollHeight\n }\n\n private _setInnerClamp(inner: HTMLElement, size: string) {\n inner.style.setProperty(this._clampProperty(), size)\n }\n\n private _clearInnerClamp() {\n const inner = this._contentInner()\n if (!inner) return\n inner.style.removeProperty('max-height')\n inner.style.removeProperty('max-width')\n }\n\n private _setInnerInert(inert: boolean) {\n this._contentInner()?.toggleAttribute('inert', inert)\n }\n\n private _contentInner(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-inner') ?? null\n }\n\n private _contentContainer(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-content') ?? null\n }\n\n private _contentTrack(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-track') ?? null\n }\n\n override render() {\n // 三态由 data-wui-presence 驱动(open/closing),缺省即关闭稳态。\n const trackClass = {\n 'wui-collapse-track': true,\n 'is-horizontal': this.horizontal\n }\n\n return html`\n <div class=\"wui-collapse-trigger-wrapper\" @slotchange=${this._onTriggerSlotChange}><slot></slot></div>\n <div class=\"wui-collapse-content\">\n <div class=${classMap(trackClass)} id=${this._contentId}>\n <div class=\"wui-collapse-inner\">\n <slot name=\"content\"></slot>\n </div>\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-collapse': WebUiCollapse\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../../src/components/collapse/index.ts"],"sourcesContent":["import { html, LitElement, unsafeCSS } from 'lit'\nimport { customElement, property, query } from 'lit/decorators.js'\nimport { classMap } from 'lit/directives/class-map.js'\n\nimport { UserChangeController } from '@/shared/events/user-change'\nimport { dispatchOpenChangeEvent } from '@/shared/open-state'\nimport { getTransitionDuration } from '@/shared/overlay/presence'\n\nimport style from './style.css?inline'\n\nconst EXIT_FALLBACK_BUFFER = 80\n\n/** peek 边缘渐隐的活动长度:关闭/收起态取由 peek 推导的长度,展开态归 0。 */\nconst PEEK_EDGE_ACTIVE = '--wui-collapse-peek-edge-active'\n\n/*\n * 注册成 `<length>` 才能被 transition 插值(未注册的自定义属性是离散值,只能瞬变)。\n *\n * 必须用 JS 的 `CSS.registerProperty` 而不是 CSS 里的 `@property`:实测后者写在\n * shadow root 的样式表里不产生注册(getComputedStyle 读打开态得到空串而非\n * initial-value),而自定义属性的注册本质上是 document 级的。\n * 注册失败(jsdom、旧浏览器、重复注册)时降级为离散过渡——渐变带长度瞬变,\n * 但位置百分比仍跟随裁剪边缘,行为依旧正确。\n *\n * `inherits` 必须是 true:晕染伪元素(style.css 的 ::after)读同一个值来调制模糊,\n * 继承为 false 时伪元素读到的是 initialValue 0px——mask 的各色标随之塌缩到 100%,\n * 模糊层静默消失且其余渲染一切照旧,只少一层效果。\n */\nfunction registerPeekEdgeActive() {\n // jsdom 与不支持的环境直接跳过;样式里的 var() fallback 保证声明仍然有效。\n if (typeof CSS === 'undefined' || typeof CSS.registerProperty !== 'function') return\n try {\n CSS.registerProperty({\n name: PEEK_EDGE_ACTIVE,\n syntax: '<length>',\n inherits: true,\n initialValue: '0px'\n })\n } catch {\n // 已注册过(HMR、多份副本)或定义冲突:保持现有注册即可。\n }\n}\n\nregisterPeekEdgeActive()\n\nlet collapseIdCounter = 0\n\n/**\n * 文档流内 collapse panel:trigger 经默认 slot 提供,内容经 `slot=\"content\"` 提供。\n *\n * trigger 语义完全来自 slot 进来的交互元素(原生 button / web-ui-button 等):\n * Enter/Space 激活走该元素的原生 click,不做 role=\"button\" 补齐;组件把\n * `aria-expanded` / `aria-controls` / `aria-disabled` 回写到首个 assigned\n * element(与 popover trigger 的既定回写模式一致)。wrapper 仅负责点击代理,\n * 不可聚焦,不承载 ARIA。\n *\n * 内容区在 shadow 内以 grid `0fr ↔ 1fr` track 过渡驱动高度/宽度动画(ADR-0030\n * 选型保留)。关闭稳态三态(消费者 light DOM 永不移动,hidden 全部命令式管理,\n * 避免渲染绑定在关闭动画起点抢先 display:none):\n * - 默认:content 容器 `hidden`(display:none,脱离渲染与可访问性树);\n * - `keep-mounted`:inner `inert`(保留在 0fr track 内,滚动位置与布局可测量);\n * - `peek`:inner `inert`,track 停在固定长度的裁剪窗口,只露出内容头部;\n * - 动画进行中:inner `inert` + track pointer-events 禁用,结束后进入对应稳态。\n *\n * `peek` 与展开态之间是固定长度与内容自适应高度的过渡,CSS 无法插值;这两个\n * 方向的动画改为读出像素后以显式长度写入 track,落稳态时清除内联值交回 CSS 规则。\n */\n@customElement('web-ui-collapse')\nexport class WebUiCollapse extends LitElement {\n static override styles = unsafeCSS(style)\n\n /** 展开状态;严格受控,唯一状态源。 */\n @property({ type: Boolean, reflect: true }) open = false\n\n /** 禁用触发:点击忽略、aria-disabled 写入 trigger 元素;已展开内容保持现状。 */\n @property({ type: Boolean, reflect: true }) disabled = false\n\n /** 水平方向:true 时沿宽度展开,默认沿高度。 */\n @property({ type: Boolean, reflect: true }) horizontal = false\n\n /** 关闭稳态保持内容 `inert` 而非 `hidden`(保留滚动位置,可测量)。 */\n @property({ type: Boolean, reflect: true, attribute: 'keep-mounted' }) keepMounted = false\n\n /**\n * 关闭稳态露出的尺寸(CSS 长度,如 `120px`、`6rem`),沿动画轴生效:默认是高度,\n * `horizontal` 时为宽度;未设置时保持完全收起。\n *\n * 设置后关闭稳态语义等同 `keep-mounted`(内容留在 track 内、`inert` 阻断交互),\n * 只露出 `peek` 范围内的部分;内容本身不足 `peek` 时按内容实际尺寸收起。\n * 注意 `peek` 是固定长度与内容自适应高度之间的过渡:CSS 无法在两者之间插值,\n * 因此这两个方向的动画由本组件读出内容尺寸后以显式长度驱动(其余路径仍是\n * 零测量的 grid fr 过渡)。\n *\n * 露出区域末端自带一段边缘渐隐(提示下方还有内容),长度由 `peek` 按比例推导,\n * 无需也不接受调用方单独指定——见 style.css 中 `--wui-collapse-peek-edge-*`\n * 系列变量(比例、上限、末端颜色可用 CSS 覆盖,属于主题级调优而非组件 API)。\n */\n @property({ type: String, reflect: true }) peek: string | null = null\n\n @query('.wui-collapse-trigger-wrapper') private _triggerWrapper!: HTMLElement\n\n private _contentId = `wui-collapse-content-${++collapseIdCounter}`\n\n private readonly _userOpenChange = new UserChangeController()\n\n private _exitTimer?: ReturnType<typeof setTimeout>\n // 动画代际:每次管线启动自增,旧管线的 transitionend/兜底定时器/rAF 全部失效。\n private _generation = 0\n private _lastOpen?: boolean\n\n override connectedCallback() {\n super.connectedCallback()\n // 点击监听与生命周期同寿(宿主移除即整体销毁),无需 disconnected 清理。\n this.addEventListener('click', this._onTriggerClick)\n // 断连可能打断动画管线(瞬态残留:presence='closing'、容器未 hidden)。\n // 首次连接不必处理(初始 render 走 _lastOpen===undefined 落稳态,请求多余\n // 渲染会触发「scheduled an update after update completed」告警);重连时\n // 重置同步状态并强制一轮 update,由首次同步分支重新落稳态、丢弃在途管线。\n if (this.hasUpdated) {\n this._generation++\n this._clearExitTimer()\n this._lastOpen = undefined\n this.requestUpdate()\n }\n }\n\n override disconnectedCallback() {\n super.disconnectedCallback()\n this._clearExitTimer()\n }\n\n protected override updated(changed: Map<string, unknown>) {\n // ARIA 回写不依赖 open 分支,任何渲染后都保持与宿主状态同步。\n this._syncTriggerAria()\n // peek 的稳态长度与动画写入共用 track 内联样式,先同步再落稳态,避免被稳态清除。\n this._syncPeek()\n\n // 首次同步(含 open 为默认值的场景,changed map 不含默认值属性):\n // 直接落到正确稳态,不播动画(popover 初始 open 同款)。\n if (this._lastOpen === undefined) {\n this._lastOpen = this.open\n if (this.open) this._applyOpenSteadyState()\n else this._applyClosedSteadyState()\n return\n }\n\n if ((changed.has('keepMounted') || changed.has('peek')) && !this.open) {\n // 运行态切换 keep-mounted / peek:关闭稳态需在 hidden / inert / 裁剪长度间重新落地。\n this._applyClosedSteadyState()\n }\n if (!changed.has('open') || this.open === this._lastOpen) return\n\n this._lastOpen = this.open\n if (this._userOpenChange.consume()) this._dispatchChange(this.open)\n if (this.open) this._beginExpand()\n else this._beginCollapse()\n }\n\n show() {\n if (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 _dispatchChange(open: boolean) {\n dispatchOpenChangeEvent(this, open)\n }\n\n /*\n * 点击代理:冒泡路径穿过 trigger wrapper 的 click 一律切换。语义由 slot 内\n * 的交互元素(原生 button / web-ui-button)提供;slot 放纯文本时无激活语义\n * (README 约定)。内容区与嵌套内层 collapse 的 click 不经过本 wrapper,天然隔离。\n */\n private _onTriggerClick = (event: MouseEvent) => {\n if (this.disabled) return\n if (!event.composedPath().includes(this._triggerWrapper)) return\n this._userOpenChange.mark()\n this.toggle()\n }\n\n /*\n * Trigger 元素取首个 assigned element(与 popover `_queryTrigger` 同款惰性查询,\n * 不依赖 slotchange——jsdom 不派发该事件,无法用状态缓存)。slot 为空时无回写目标。\n */\n private _queryTrigger(): HTMLElement | null {\n const slot = this.shadowRoot?.querySelector<HTMLSlotElement>('.wui-collapse-trigger-wrapper slot')\n const el = slot?.assignedElements()[0]\n return el instanceof HTMLElement ? el : null\n }\n\n /*\n * slot 内容增删不触发宿主响应式更新:显式请求一轮渲染,由 updated() 的\n * ARIA 回写覆盖晚到的 trigger 元素(slotchange 在 jsdom 不派发,无法监听)。\n */\n private readonly _onTriggerSlotChange = () => {\n this.requestUpdate()\n }\n\n // ARIA 状态回写到 trigger 元素本身(wrapper 不可聚焦,AT 读不到)。\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._contentId)\n if (this.disabled) trigger.setAttribute('aria-disabled', 'true')\n else trigger.removeAttribute('aria-disabled')\n }\n\n // ===== 内容区动画管线(三态语义自 ADR-0030 迁移) =====\n\n /** 关闭稳态是否露出部分内容;空值与非空判断避免 `peek=\"\"` 被当作有效长度。 */\n private get _peeks(): boolean {\n return typeof this.peek === 'string' && this.peek.trim() !== ''\n }\n\n private _applyOpenSteadyState() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (container) container.hidden = false\n if (track) track.dataset.wuiPresence = 'open'\n this._clearInnerClamp()\n this._setInnerInert(false)\n }\n\n private _applyClosedSteadyState() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (track) delete track.dataset.wuiPresence\n this._clearInnerClamp()\n if (this.keepMounted || this._peeks) {\n if (container) container.hidden = false\n this._setInnerInert(true)\n return\n }\n this._setInnerInert(false)\n if (container) container.hidden = true\n }\n\n private _beginExpand() {\n this._generation++\n this._clearExitTimer()\n const container = this._contentContainer()\n const track = this._contentTrack()\n if (!container || !track) return\n\n const generation = this._generation\n // 中断关闭动画时 inner 已 inert:先恢复可交互,内容随展开重新可见。\n this._setInnerInert(false)\n // 从 display:none 进入时无先前计算样式可过渡:提交 0fr 起点后再翻转。\n container.hidden = false\n\n if (this._peeks) {\n const inner = this._contentInner()\n // 无过渡时长(reduced motion、jsdom)或无 inner 时不做测量,直接落展开稳态。\n if (!inner || getTransitionDuration(inner, 0) === 0) {\n this._applyOpenSteadyState()\n return\n }\n /*\n * peek 的裁剪长度是固定值,展开目标是内容自适应高度,两者无法由 CSS 插值,\n * 动画必须由两个显式长度驱动:起点是当前露出的长度,目标是内容完整尺寸。\n * 起点要在同任务内先写入并提交回流,否则过渡会退化成跳变(实测分帧写入\n * 反而完全不参与插值判定);目标读 inner 的 scroll 尺寸,不写探针样式。\n */\n const from = this._measureInnerSize(inner)\n const to = this._measureContentSize(inner)\n this._setInnerClamp(inner, `${from}px`)\n void this._measureInnerSize(inner)\n track.dataset.wuiPresence = 'open'\n this._setInnerClamp(inner, `${to}px`)\n this._awaitTransition(inner, generation, () => this._settle(true), [this._clampProperty()])\n return\n }\n\n void track.offsetWidth\n\n requestAnimationFrame(() => {\n if (generation !== this._generation || !this.open) return\n track.dataset.wuiPresence = 'open'\n this._awaitTransition(track, generation, () => this._settle(true))\n })\n }\n\n private _beginCollapse() {\n this._generation++\n this._clearExitTimer()\n const track = this._contentTrack()\n if (!track) return\n\n // 收起期间立即阻断交互,防止焦点落入正在消失的区域。\n this._setInnerInert(true)\n const generation = this._generation\n\n if (this._peeks) {\n const inner = this._contentInner()\n // 无过渡时长(reduced motion、jsdom)或无 inner 时不做测量,直接落裁剪稳态。\n if (!inner || getTransitionDuration(inner, 0) === 0) {\n this._applyClosedSteadyState()\n return\n }\n /*\n * 收起目标是 `peek` 长度本身:与内容尺寸的较小值由 CSS 稳态规则解析,\n * 动画只需把当前展开长度过渡到这个长度。起点同样要先写入并提交回流,\n * 顺序与 _beginExpand 一致。\n */\n const from = this._measureInnerSize(inner)\n this._setInnerClamp(inner, `${from}px`)\n void this._measureInnerSize(inner)\n track.dataset.wuiPresence = 'closing'\n this._setInnerClamp(inner, `${this.peek}`)\n this._awaitTransition(inner, generation, () => this._settle(false), [this._clampProperty()])\n return\n }\n\n track.dataset.wuiPresence = 'closing'\n this._awaitTransition(track, generation, () => this._settle(false))\n }\n\n // 关闭稳态:keep-mounted → 内容保留在 0fr track 内并 inert;peek → 保留在裁剪\n // 窗口内并 inert;默认 → 容器 hidden。\n private _settle(open: boolean) {\n this._clearExitTimer()\n const track = this._contentTrack()\n // peek 过渡以显式长度写入 inner:落稳态一律清除,交回 CSS 稳态规则(长度声明)。\n this._clearInnerClamp()\n if (open) {\n this._setInnerInert(false)\n return\n }\n // 落稳态必须清除 closing 瞬态标记:否则 track 永久保留 pointer-events:none\n // 与 exit 过渡配置(keep-mounted 分支同样清除,避免残留)。\n if (track) delete track.dataset.wuiPresence\n if (this.keepMounted || this._peeks) return\n const container = this._contentContainer()\n this._setInnerInert(false)\n if (container) container.hidden = true\n }\n\n // 等待动画属性过渡结束;时长为 0(reduced motion、jsdom)直接稳态,\n // transitionend 不触发时按时长 + 缓冲兜底。默认等待 grid track 过渡;peek 的\n // 过渡落在 inner 的裁剪长度上,由调用方传入对应属性。\n private _awaitTransition(\n animated: HTMLElement,\n generation: number,\n onDone: () => void,\n properties: readonly string[] = ['grid-template-rows', 'grid-template-columns']\n ) {\n const duration = getTransitionDuration(animated, 0)\n if (duration === 0) {\n if (generation === this._generation) onDone()\n return\n }\n\n let settled = false\n const finish = () => {\n animated.removeEventListener('transitionend', onTransitionEnd)\n if (this._exitTimer === timer) this._exitTimer = undefined\n clearTimeout(timer)\n // 过渡可能被属性匹配的后续动画触发(如中断重开):代际不符时静默退场。\n if (settled || generation !== this._generation) return\n settled = true\n onDone()\n }\n const timer = setTimeout(finish, duration + EXIT_FALLBACK_BUFFER)\n this._exitTimer = timer\n\n const onTransitionEnd = (event: TransitionEvent) => {\n if (event.target === animated && properties.includes(event.propertyName)) finish()\n }\n animated.addEventListener('transitionend', onTransitionEnd)\n }\n\n private _clearExitTimer() {\n if (this._exitTimer !== undefined) {\n clearTimeout(this._exitTimer)\n this._exitTimer = undefined\n }\n }\n\n // ===== peek 尺寸读写(裁剪长度落在 inner 上,track 只负责 1fr 与裁剪窗口) =====\n\n /*\n * peek 长度经自定义属性下发给 shadow 样式规则,而不是直接写 inner 的裁剪长度:\n * 关闭稳态由 CSS 声明(长度与内容尺寸的较小值由 CSS 原生解析),动画写入的内联\n * 长度在其上覆盖,落稳态时清除即可回到声明值。\n */\n private _syncPeek() {\n const track = this._contentTrack()\n if (!track) return\n track.toggleAttribute('data-wui-peek', this._peeks)\n if (this._peeks) {\n track.style.setProperty('--wui-collapse-peek', this.peek ?? '')\n } else {\n track.style.removeProperty('--wui-collapse-peek')\n }\n }\n\n private _clampProperty(): 'max-height' | 'max-width' {\n return this.horizontal ? 'max-width' : 'max-height'\n }\n\n private _measureInnerSize(inner: HTMLElement): number {\n return this.horizontal ? inner.offsetWidth : inner.offsetHeight\n }\n\n /*\n * 内容在动画轴上的完整尺寸(px):裁剪只作用在 inner 的 max-height/max-width 上,\n * 被测内容的布局盒子仍是完整尺寸,故读 scroll 尺寸即可,不写任何样式——写样式\n * 会被 grid 过渡机制当作过渡起点读取,必须避开。\n */\n private _measureContentSize(inner: HTMLElement): number {\n return this.horizontal ? inner.scrollWidth : inner.scrollHeight\n }\n\n private _setInnerClamp(inner: HTMLElement, size: string) {\n inner.style.setProperty(this._clampProperty(), size)\n }\n\n private _clearInnerClamp() {\n const inner = this._contentInner()\n if (!inner) return\n inner.style.removeProperty('max-height')\n inner.style.removeProperty('max-width')\n }\n\n private _setInnerInert(inert: boolean) {\n this._contentInner()?.toggleAttribute('inert', inert)\n }\n\n private _contentInner(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-inner') ?? null\n }\n\n private _contentContainer(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-content') ?? null\n }\n\n private _contentTrack(): HTMLElement | null {\n return this.shadowRoot?.querySelector<HTMLElement>('.wui-collapse-track') ?? null\n }\n\n override render() {\n // 三态由 data-wui-presence 驱动(open/closing),缺省即关闭稳态。\n const trackClass = {\n 'wui-collapse-track': true,\n 'is-horizontal': this.horizontal\n }\n\n return html`\n <div class=\"wui-collapse-trigger-wrapper\" @slotchange=${this._onTriggerSlotChange}><slot></slot></div>\n <div class=\"wui-collapse-content\">\n <div class=${classMap(trackClass)} id=${this._contentId}>\n <div class=\"wui-collapse-inner\">\n <slot name=\"content\"></slot>\n </div>\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-collapse': WebUiCollapse\n }\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/collapse/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n/*\n * :host 仅限 display/contain(ADR-0003)。\n * headless 内核(ADR-0039):不携带视觉样式,trigger 与内容的排版由消费者\n * 提供;这里只保留动画与三态关闭稳态必需的结构规则。\n */\n:host {\n display: block;\n}\n\n/*\n * grid 0fr↔1fr 高度动画(ADR-0038 选型):\n *
|
|
1
|
+
{"version":3,"file":"style.js","names":[],"sources":["../../../src/components/collapse/style.css?inline"],"sourcesContent":["*,\n*::before,\n*::after {\n box-sizing: border-box;\n}\n\n/*\n * :host 仅限 display/contain(ADR-0003)。\n * headless 内核(ADR-0039):不携带视觉样式,trigger 与内容的排版由消费者\n * 提供;这里只保留动画与三态关闭稳态必需的结构规则。\n */\n:host {\n display: block;\n}\n\n/*\n * grid 0fr↔1fr 高度动画(ADR-0038 选型):\n * track 行高在 0fr(收起)与 1fr(展开)间过渡,内容高度自适应、零 JS 测量;\n * inner 的 min-height/min-width 归零是 track 能收缩到 0 的必要条件,\n * overflow hidden 负责裁剪收起中的内容。\n */\n.wui-collapse-track {\n display: grid;\n grid-template-rows: 0fr;\n transition: grid-template-rows var(--wui-duration-collapse-enter, 200ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n /*\n * 水平方向:块级 grid 默认撑满可用宽度,1fr 列会解析为剩余空间而非内容宽度;\n * width: max-content 让 1fr 列按内容 max-content 贡献解析(与行向 auto 高度同理),\n * 0fr 收缩依赖 inner 的 min-width: 0。\n */\n &.is-horizontal {\n grid-template-columns: 0fr;\n grid-template-rows: none;\n width: max-content;\n transition: grid-template-columns var(--wui-duration-collapse-enter, 200ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n }\n}\n\n.wui-collapse-track[data-wui-presence='open'] {\n grid-template-rows: 1fr;\n\n &.is-horizontal {\n grid-template-columns: 1fr;\n grid-template-rows: none;\n }\n}\n\n.wui-collapse-track[data-wui-presence='closing'] {\n pointer-events: none;\n transition: grid-template-rows var(--wui-duration-collapse-exit, 160ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n\n &.is-horizontal {\n transition: grid-template-columns var(--wui-duration-collapse-exit, 160ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n }\n}\n\n/*\n * peek 模式下 track 恒为 1fr(高度始终由内容驱动),裁剪长度交给 inner 的\n * max-height/max-width:这样 `peek` 与内容尺寸的较小值由 CSS 原生解析——内容不足\n * `peek` 时不残留空白,也不会被撑到 `peek`——而动画两端都是长度,可被浏览器插值\n * (fit-content 或 minmax 的关态与内容高度之间无法插值,实测会退化成跳变)。\n *\n * 注意不能限定 `:not([data-wui-presence])`:收起动画期间 presence='closing',\n * track 会掉回 0fr 先把内容压成 0 再弹回,动画被毁。\n */\n.wui-collapse-track[data-wui-peek] {\n grid-template-rows: 1fr;\n\n &.is-horizontal {\n grid-template-columns: 1fr;\n grid-template-rows: none;\n }\n}\n\n/*\n * peek 边缘渐隐的「活动长度」(替代直接读推导长度):\n * 关闭/收起态取推导值,展开态归 0(fallback)——渐变带因此跟随开合动画\n * 平滑出现/消失,而不是在某一帧突然出现(旧实现把 mask 限定在关闭稳态,收起动画\n * 结束才显示,表现为明显的延迟)。\n *\n * 插值依赖 `CSS.registerProperty` 把它注册成 `<length>`(见 index.ts):未注册的\n * 自定义属性是离散值、无法过渡,且不注册时「未定义」会让整条 mask-image 声明\n * 在打开态失效回退到 none,因此这里必须给 var() 显式 fallback。\n */\n.wui-collapse-inner {\n overflow: hidden;\n min-height: 0;\n\n /*\n * peek 展开/收起的过渡落在裁剪长度上;track 静止在 1fr,只跟随 inner 的高度变化。\n * 活动长度与裁剪长度同步过渡,渐变带才能跟着开合一起淡入淡出。\n */\n transition:\n max-height var(--wui-duration-collapse-enter, 200ms) var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1)),\n --wui-collapse-peek-edge-active var(--wui-duration-collapse-enter, 200ms)\n var(--wui-ease-enter, cubic-bezier(0.23, 1, 0.32, 1));\n}\n\n.is-horizontal .wui-collapse-inner {\n min-width: 0;\n\n /* 覆盖轴向,但保留活动长度的过渡——否则水平模式下渐变带不淡入淡出。 */\n transition-property: max-width, --wui-collapse-peek-edge-active;\n}\n\n.wui-collapse-track[data-wui-presence='closing'] .wui-collapse-inner {\n transition-duration: var(--wui-duration-collapse-exit, 160ms);\n}\n\n/*\n * peek 露出区域的边缘渐隐(mask-image):\n * 沿动画轴末端(默认底边、horizontal 时右边)做 alpha 渐变,让\"被裁掉的部分\"与\n * \"露出的部分\"之间柔和过渡,提示下方还有内容。设置 `peek` 即启用——渐隐长度由\n * `peek` 推导,不是独立入参。\n *\n * 长度推导:`peek * --wui-collapse-peek-edge-ratio`(默认 0.4),并夹在\n * `--wui-collapse-peek-edge-max`(默认 112px)以内,避免大 peek 算出过长的虚化带。\n * 纯 CSS 的 `calc(length * number)` —— 不需要 JS 解析单位,peek 用 rem/em 时\n * 渐隐长度自动跟着字号缩放。需要绝对长度时直接设 `--wui-collapse-peek-edge`\n * 覆盖(它优先于推导值),把比例设成 0 即可关掉渐隐。\n *\n * 渐变是四段「先陡后缓」而非线性:主体全黑延伸到渐变带起点后,前 30% 距离就\n * 掉到 0.6 alpha,65% 处 0.26,末端落到 edge-color(默认全透明)。线性渐变的\n * alpha 曲线前 40% 几乎贴着 1.0,人眼感知到的虚化远短于声明长度——实测同样\n * 50px 的带子,线性只让人觉得\"末尾一小截发虚\",先陡后缓从带子起点就明显发虚,\n * 感知长度接近翻倍。\n *\n * 渐变带的位置用百分比而非绝对长度:百分比相对 inner 的**当前渲染高度**,而该高度\n * 在动画期间随 max-height 逐帧变化——mask 字符串本身不需要插值,渲染结果会自动\n * 跟随裁剪边缘移动(实测:mask 字符串恒定,容器高度变化时渐变带跟着走)。\n * 渐变长度由活动长度变量控制,其过渡见上方说明。\n * 末端颜色由 `--wui-collapse-peek-edge-color` 覆盖(需带 alpha,mask 按 alpha 解析)。\n *\n * 这条 mask 管的是「内容的可见度」;同一条渐变带上还叠了一层真实的背景模糊,\n * 见下方 ::after。\n */\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner {\n /* 晕染伪元素以本盒为定位基准,见下方 ::after 的说明。 */\n position: relative;\n mask-image: linear-gradient(\n to bottom,\n black 0%,\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n rgb(0 0 0 / 0.6) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.7),\n rgb(0 0 0 / 0.26) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n var(--wui-collapse-peek-edge-color, transparent) 100%\n );\n}\n\n/*\n * peek 边缘晕染:叠一层真实的背景模糊(#221)\n * ==========================================\n *\n * mask 只把内容按 alpha 变透明,边缘仍是**锐的**——被裁掉一半的字与 icon 看得见轮廓,\n * 读起来像「贴了张半透明胶片」,不像 macOS 那种磨砂。header 的晕染(见\n * layout/style.css「Header 的装饰性背景晕染」)用 backdrop-filter 把背后的画面真的糊掉,\n * 这里对同一段渐变带补上同一层模糊,两处观感因此一致。\n *\n * 模糊强度用 mask 沿渐变带调制:`backdrop-filter` 只有一个全局半径,逐像素的强弱只能\n * 由本盒的 mask alpha 给。曲线两端都归 0——起点不归 0 的话,渐变带的上沿会出现一条\n * 「突然开始模糊」的接缝;末端不归 0 则会在裁剪边缘糊到盒外(本盒 overflow: hidden,\n * 盒外那段本就不可见,但模糊会在盒边界留下一圈亮边)。\n *\n * 活动长度为 0(展开态)时整条 mask 的各色标塌缩到 100%,取末位色标即全透明,\n * 伪元素自然什么都不画——不需要额外的 toggle 规则,也不会在展开态留下一层灰纱。\n * 这也是**不**改用「页面色渐变盖住内容」那层的原因:那层在长度为 0 时塌缩成不透明页面色,\n * 会把展开后的内容整片盖住,要防住就得再加一个可过渡的 toggle 量。\n *\n * 位置相对 inner(`position: relative`):本盒就是被裁剪到 peek 长度的那一个,\n * 伪元素随它一起被裁剪,不需要第二层定位。z-index 让它压在 slot 内容之上。\n * --wui-collapse-peek-edge-active 必须以 inherits: true 注册(见 index.ts):\n * 继承为 false 时伪元素读到的是 initialValue 0px,整条 mask 塌缩、模糊静默消失。\n */\n.wui-collapse-track[data-wui-peek] .wui-collapse-inner::after {\n pointer-events: none;\n content: '';\n\n position: absolute;\n z-index: 1;\n inset: 0;\n\n backdrop-filter: blur(var(--wui-collapse-peek-edge-blur, 4px));\n\n mask-image: linear-gradient(\n to bottom,\n transparent calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n.wui-collapse-track[data-wui-peek].is-horizontal .wui-collapse-inner {\n mask-image: linear-gradient(\n to right,\n black 0%,\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n rgb(0 0 0 / 0.6) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.7),\n rgb(0 0 0 / 0.26) calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n var(--wui-collapse-peek-edge-color, transparent) 100%\n );\n}\n\n.wui-collapse-track[data-wui-peek].is-horizontal .wui-collapse-inner::after {\n mask-image: linear-gradient(\n to right,\n transparent calc(100% - var(--wui-collapse-peek-edge-active, 0px)),\n black calc(100% - var(--wui-collapse-peek-edge-active, 0px) * 0.35),\n transparent 100%\n );\n}\n\n/*\n * 裁剪长度只在关闭稳态由 CSS 声明;动画期间由内联长度覆盖,落稳态清除后回到这里。\n * 下面几条 peek 规则按选择器特异性升序排列(`no-descending-specificity`):\n * 不带 presence 限定的两条 mask 在最前,horizontal 的裁剪(多一个类)在最后。\n */\n.wui-collapse-track[data-wui-peek]:not([data-wui-presence]) .wui-collapse-inner {\n max-height: var(--wui-collapse-peek, 0);\n}\n\n/*\n * 非展开态(关闭稳态 + 收起动画进行中)让活动长度取推导值:\n * 收起动画一开始渐变带就开始淡入,与裁剪长度同步,而不是落稳态才出现。\n * 展开态(presence='open')不命中 → 活动长度回落到 0 → mask 整段不透明,\n * 完全展开的内容不再有底部淡出。\n *\n * 推导写成 `var(--wui-collapse-peek-edge, <推导式>)`:显式设了长度就听调用方的,\n * 没设才按比例算——逃生舱仍然是纯 CSS 变量,不占组件属性名额。\n */\n.wui-collapse-track[data-wui-peek]:not([data-wui-presence='open']) .wui-collapse-inner {\n --wui-collapse-peek-edge-active: var(\n --wui-collapse-peek-edge,\n clamp(\n 0px,\n calc(var(--wui-collapse-peek, 0px) * var(--wui-collapse-peek-edge-ratio, 0.4)),\n var(--wui-collapse-peek-edge-max, 112px)\n )\n );\n}\n\n.wui-collapse-track[data-wui-peek]:not([data-wui-presence]).is-horizontal .wui-collapse-inner {\n max-width: var(--wui-collapse-peek, 0);\n}\n"],"mappings":""}
|
|
@@ -3,14 +3,14 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
3
3
|
static styles: import('lit').CSSResult;
|
|
4
4
|
disabled: boolean;
|
|
5
5
|
noScrollLock: boolean;
|
|
6
|
-
/** opt-in
|
|
6
|
+
/** opt-in:触屏长按打开 menu。默认关闭,鼠标路径靠原生 contextmenu,无需此属性。 */
|
|
7
7
|
longPress: boolean;
|
|
8
8
|
private _longPressDelay;
|
|
9
9
|
/**
|
|
10
10
|
* 走包内既有的 `normalizeNumber` 通道(与 `dropdown.offset`、`popover.offset` 同形)。
|
|
11
11
|
* 不能只靠 `Math.max(0, …)` 挡负数:`Math.max(0, NaN) === NaN`,而属性写错
|
|
12
12
|
* (`long-press-delay="abc"`)时 Lit 的 Number converter 给的正是 NaN,
|
|
13
|
-
* `setTimeout(fn, NaN)` 会立刻触发,长按就在 pointerdown
|
|
13
|
+
* `setTimeout(fn, NaN)` 会立刻触发,长按就在 pointerdown 同一帧开 menu、绕过全部时长语义。
|
|
14
14
|
*/
|
|
15
15
|
get longPressDelay(): number;
|
|
16
16
|
set longPressDelay(v: number);
|
|
@@ -22,13 +22,13 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
22
22
|
private _longPressTimer;
|
|
23
23
|
private _longPressOrigin;
|
|
24
24
|
/**
|
|
25
|
-
*
|
|
25
|
+
* 长按自行开 menu 的时刻。浏览器会为**同一次触摸**补发两个事件,二者都属于
|
|
26
26
|
* 那一次意图,不是两次独立操作:
|
|
27
27
|
*
|
|
28
28
|
* - 原生 `contextmenu`:长按被识别时立刻派发,会重复打开(`_openAt` 走已开分支)。
|
|
29
|
-
* - 合成 `click`:`touchend`
|
|
30
|
-
* 实测真实触控管线下的顺序为 pointerdown(9ms) →
|
|
31
|
-
* touchend(1019ms) → click(1022ms)
|
|
29
|
+
* - 合成 `click`:`touchend` 之后由引擎补发,会把刚打开的 menu 立刻 light-dismiss。
|
|
30
|
+
* 实测真实触控管线下的顺序为 pointerdown(9ms) → 长按开 menu(≈509ms) →
|
|
31
|
+
* touchend(1019ms) → click(1022ms),缺这一步 menu 开了就被自己的补发事件关掉。
|
|
32
32
|
*
|
|
33
33
|
* 因此在窗口内吸收这两个补发事件;窗口由下一次 `pointerdown` 关闭。
|
|
34
34
|
*/
|
|
@@ -36,17 +36,17 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
36
36
|
/**
|
|
37
37
|
* 长按手势结束(pointerup)的时刻,补发事件窗口的起点。
|
|
38
38
|
*
|
|
39
|
-
* 为 null
|
|
39
|
+
* 为 null 表示「这次长按的手势还没结束」——窗口退回以 menu 打开时刻为起点。手势被
|
|
40
40
|
* pointercancel 打断、或组件在抬手前就被卸载时都会走到这条兜底,行为与此前一致。
|
|
41
41
|
*/
|
|
42
42
|
private _longPressGestureEndedAt;
|
|
43
43
|
private readonly _submenuPositionEpochs;
|
|
44
44
|
private _menu?;
|
|
45
45
|
/**
|
|
46
|
-
* 模态 scrim
|
|
46
|
+
* 模态 scrim。menu 打开期间存在并处于 `showModal()` 状态,关闭时随 panel 一起移除。
|
|
47
47
|
*
|
|
48
|
-
*
|
|
49
|
-
* 查的是 target 的**祖先**里有无已打开的 dialog,而 scrim
|
|
48
|
+
* panel 必须**显式**挂进它(`createMenuPortalOverlay` 的第三参):`resolveOverlayContainer`
|
|
49
|
+
* 查的是 target 的**祖先**里有无已打开的 dialog,而 scrim 是 panel 的反方向,祖先发现查不到。
|
|
50
50
|
*/
|
|
51
51
|
private _scrim?;
|
|
52
52
|
/**
|
|
@@ -56,13 +56,13 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
56
56
|
* 同一套 `computePosition` + `flip` / `shift`,垂直契约(上缘对齐触发点、向下展开、
|
|
57
57
|
* 放不下翻到上方)完全一致。
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
* 正是 iOS / Android
|
|
59
|
+
* 触屏要居中:手指按下去的位置是「这块内容在这里」的意思,menu 在它正下方居中展开,
|
|
60
|
+
* 正是 iOS / Android 原生长按 menu 的形态;左对齐会让 menu 整体偏到手指右侧,看起来像
|
|
61
61
|
* 弹在了别的东西上面。
|
|
62
62
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
63
|
+
* 鼠标不居中:桌面右键的肌肉记忆是 menu**左上角**出现在光标处。更要紧的是居中会让光标
|
|
64
|
+
* 落在 panel 上缘正中 —— 也就是落在第一项的命中区里。后果是 menu 一开就有项处于 hover 高亮,
|
|
65
|
+
* 并且紧接着再右键一次(想开 submenu)会打在 menu item 上而不是空白处。`overlay-in-dialog`
|
|
66
66
|
* 里 `openAt(100, 400)` 那条用例钉的 `'top left'` 正是这条契约。
|
|
67
67
|
*
|
|
68
68
|
* 判据是**开启来源**(调用点显式传入),不靠运行时嗅探 pointerType 或视口宽度:
|
|
@@ -73,19 +73,19 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
73
73
|
/**
|
|
74
74
|
* 本次关闭的来源,决定焦点归还走哪条路。语义见 `close()` 里的说明。
|
|
75
75
|
*
|
|
76
|
-
*
|
|
76
|
+
* 只有 menu item 激活这一条需要抢在退场之前出顶层;点外面 / Escape / 程序式关闭一律
|
|
77
77
|
* 保留 dialog 默认效果 —— 那正是用户「回到原处」的期望。
|
|
78
78
|
*/
|
|
79
79
|
private _closeSource;
|
|
80
80
|
/**
|
|
81
|
-
*
|
|
81
|
+
* 触发「menu item 激活」的那次事件对象;作用域与理由见 `_onMenuItemActivation`。
|
|
82
82
|
*
|
|
83
83
|
* 存**事件**而不是布尔标志:判定「这次 `close()` 是不是发生在这一次派发里」可以问事件自己
|
|
84
84
|
* (`eventPhase !== 0`),不必用定时器去近似。布尔标志只能靠「派发结束时清掉」来划界,
|
|
85
85
|
* 而划界的那一步必须与派发末尾落在同一个任务、同一个微任务检查点之后——那是个会错的假设。
|
|
86
86
|
*
|
|
87
87
|
* 存在的理由是 `close()` 的出顶层判定不能只看 `_closeSource`:消费方完全可以在自己的
|
|
88
|
-
*
|
|
88
|
+
* menu item 回调里直接调 `close()`(interweave 的右键重命名就是 `emit` 之后自己关),
|
|
89
89
|
* 那时 `_closeSource` 还是 `'default'`,scrim 就一直留在 top layer 里没退出,
|
|
90
90
|
* 消费方随后把焦点送进编辑器的动作落在 inert 文档上被浏览器静默丢弃。
|
|
91
91
|
*
|
|
@@ -93,11 +93,11 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
93
93
|
* `'menu-item'`,两条判定同向,不会互相抵消。
|
|
94
94
|
*/
|
|
95
95
|
private _menuItemActivationEvent;
|
|
96
|
-
/**
|
|
96
|
+
/** 根 panel 的开启会话 handle;未开启时为 null。查询走它。 */
|
|
97
97
|
private _menuHandle;
|
|
98
98
|
/**
|
|
99
|
-
*
|
|
100
|
-
*
|
|
99
|
+
* submenu panel → 会话 handle。子层经 `adopt` 进根 handle 子树(默认不成为独立候选),
|
|
100
|
+
* 但仍各自持有 handle:整层关闭或收尾动画结束时按 panel 精确撤销。
|
|
101
101
|
*/
|
|
102
102
|
private readonly _submenuHandles;
|
|
103
103
|
private readonly _outsideClickGuard;
|
|
@@ -119,7 +119,7 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
119
119
|
disconnectedCallback(): void;
|
|
120
120
|
protected updated(changed: Map<string, unknown>): void;
|
|
121
121
|
/**
|
|
122
|
-
*
|
|
122
|
+
* 在指定视口坐标打开 menu。
|
|
123
123
|
* @param x 水平坐标(px)。
|
|
124
124
|
* @param y 垂直坐标(px)。
|
|
125
125
|
* @returns 无返回值;命令式打开不派发 `open-change` 事件。
|
|
@@ -127,19 +127,19 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
127
127
|
openAt(x: number, y: number): void;
|
|
128
128
|
private _openAt;
|
|
129
129
|
/**
|
|
130
|
-
*
|
|
130
|
+
* 关闭 menu。
|
|
131
131
|
* @returns 无返回值;命令式关闭不派发 `open-change` 事件。
|
|
132
132
|
*/
|
|
133
133
|
close(): void;
|
|
134
134
|
/**
|
|
135
|
-
*
|
|
135
|
+
* 「当前是否正处在一次可激活 menu item 的点击派发里」。
|
|
136
136
|
*
|
|
137
137
|
* 判据是 `eventPhase !== 0`(DOM 规范:派发开始前置 CAPTURING_PHASE,结束后由派发算法的
|
|
138
138
|
* terminating 步骤置回 NONE),即**事件自己知道它此刻有没有在派发中**。于是作用域精确等于
|
|
139
139
|
* 「这一次派发」,既不需要定时器也不需要猜微任务检查点的位置:
|
|
140
140
|
*
|
|
141
141
|
* - 消费方在条目自己的监听器里同步调 `close()` → 派发未结束 → 判定为真(要的就是这个)。
|
|
142
|
-
* - 消费方 `stopPropagation()`
|
|
142
|
+
* - 消费方 `stopPropagation()` 让 menu 留着 → 派发照样结束 → 之后的 Escape / 点外面 / 程序式
|
|
143
143
|
* `close()` 一律判定为假,README 承诺的「那三条关闭不抢焦点」不会被这一次点击污染。
|
|
144
144
|
* - 消费方把 `close()` 推到下一个任务 → 判定为假,退回不提前出顶层(与没有这个标记时一致)。
|
|
145
145
|
*/
|
|
@@ -151,19 +151,19 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
151
151
|
* 定位:**触屏长按与鼠标右键共用这一条 Floating UI 路径**。
|
|
152
152
|
*
|
|
153
153
|
* 两条入口现在只差「触发点的 x/y 从哪来」,布局契约完全一致:
|
|
154
|
-
* `bottom-start`
|
|
154
|
+
* `bottom-start` 表达「panel 上缘 == 触发点 y、向下展开」,水平以触发点 x 为基准。
|
|
155
155
|
* 之前有一条手写的贴底分支(先按点、再按视口下缘算 `top`),那是把「贴视口底」硬编码
|
|
156
|
-
*
|
|
156
|
+
* 成算术;真机反馈证明它错(menu 跑到屏幕最底部,与手指按下的位置完全脱节)。现在
|
|
157
157
|
* 落点交给 `flip` / `shift` 按**实际空间**决定,`placement` 只表达意图。
|
|
158
158
|
*
|
|
159
|
-
*
|
|
159
|
+
* 不再按「是否触屏」分流:窄视口的鼠标右键同样期望 menu 贴着光标出现,按输入类型或视口
|
|
160
160
|
* 宽度分流会让同一块屏幕上的两种输入拿到两套布局。分流所需的 `_anchorAtPressPoint`
|
|
161
161
|
* 连同 `_positionMenuInViewport` / `_positionMenuAtPressPoint` 两个方法一起删掉 ——
|
|
162
|
-
*
|
|
162
|
+
* panel 恒为 open scrim `<dialog>` 的直接子节点(`createMenuPortalOverlay` 第三参传入
|
|
163
163
|
* scrim,`menu-portal.ts` 里 `container = scrim ?? …`),所以旧代码那条
|
|
164
164
|
* 「非 dialog 容器」的分支对 context-menu 本来就不可达。
|
|
165
165
|
*
|
|
166
|
-
* 算出来的 left/top
|
|
166
|
+
* 算出来的 left/top 是**视口坐标**。panel 是 `position: fixed`,而 scrim 刻意不带
|
|
167
167
|
* transform / filter / contain(见 style.css),因此不构成 fixed 的包含块 ——
|
|
168
168
|
* 坐标直接相对视口解析。这条等价关系由 style.css 的满屏声明保证,并由 R6 的
|
|
169
169
|
* 「scrim 无 transform/filter/contain」+ viewport-fit 的几何断言兜底。
|
|
@@ -172,8 +172,8 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
172
172
|
/**
|
|
173
173
|
* 视口下缘的安全区高度(px)。
|
|
174
174
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
175
|
+
* 改按压点展开之后它**仍然必需**,而且不再只服务触屏:menu 默认从触发点向下长,触发点
|
|
176
|
+
* 落在屏幕下半部时 panel 下缘会进入 home indicator 区域。它现在折进 flip 与 shift 共用的
|
|
177
177
|
* 边界 padding(见 `_positionMenu`),不再需要单独一条 `maxBottom` 算术。
|
|
178
178
|
*
|
|
179
179
|
* 读宿主的自定义属性而不是 `env()`:自定义属性读回的是**计算后**的解析值,
|
|
@@ -182,30 +182,30 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
182
182
|
*/
|
|
183
183
|
private _safeAreaBottom;
|
|
184
184
|
/**
|
|
185
|
-
*
|
|
185
|
+
* 写下 panel 坐标与进场 transform 的原点。
|
|
186
186
|
*
|
|
187
187
|
* 两个 origin 必传且**不**再回退成坐标比较:flip 会改变垂直方向,回退表达式
|
|
188
|
-
* `y < this._y ? 'bottom' : 'top'`
|
|
188
|
+
* `y < this._y ? 'bottom' : 'top'` 在「panel 顶边与触发点重合」时两边都成立,等于随机取。
|
|
189
189
|
* 唯一调用方从 flip 后的 placement 与 shift 位移推出真实侧边,见 `_positionMenu`。
|
|
190
190
|
*/
|
|
191
191
|
private _applyMenuPosition;
|
|
192
192
|
/**
|
|
193
|
-
*
|
|
193
|
+
* 把焦点带进 menu 的第一项。
|
|
194
194
|
*
|
|
195
|
-
* `attempt`
|
|
195
|
+
* `attempt` 是重试计数:menu item 是刚被 `reconcileManagedMenuItems` 搬进来的自定义元素,
|
|
196
196
|
* 它们的 shadow(`.item-inner`)由各自的 Lit 更新**异步**渲染。`focusMenuItem()` 委托给
|
|
197
197
|
* 项的 `focusItem()`,那一刻 `.item-inner` 还不存在,于是**静默无操作** —— 不抛错、
|
|
198
|
-
*
|
|
198
|
+
* 也不留痕,只表现为「menu 开了但焦点没进去」。
|
|
199
199
|
*
|
|
200
200
|
* 所以这里不假设一次成功:调用后用 `findFocusedMenuItem` 复核,没落上就在下一帧再试,
|
|
201
|
-
* 最多 `FOCUS_FIRST_ITEM_ATTEMPTS` 次。上界是必要的 ——
|
|
202
|
-
*
|
|
201
|
+
* 最多 `FOCUS_FIRST_ITEM_ATTEMPTS` 次。上界是必要的 —— menu item 若始终渲染不出来
|
|
202
|
+
* (空 menu、内容被外部换掉),不能让重试链一直跑下去。
|
|
203
203
|
*/
|
|
204
204
|
private _focusFirstItem;
|
|
205
205
|
private _setupMenuItems;
|
|
206
206
|
private _returnItemsToSlot;
|
|
207
207
|
/**
|
|
208
|
-
* 建 scrim
|
|
208
|
+
* 建 scrim 并让它进入模态态,返回容器供 panel 挂载。
|
|
209
209
|
*
|
|
210
210
|
* 每次开启都新建而不是复用:scrim 的生命周期必须与「这一次开启」严格同进同出。
|
|
211
211
|
* 复用一个 dialog 意味着上一轮的 `close()` 事件、`open` 属性与 UA 记下的焦点还原目标
|
|
@@ -213,15 +213,15 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
213
213
|
*/
|
|
214
214
|
private _openScrim;
|
|
215
215
|
/**
|
|
216
|
-
* scrim
|
|
217
|
-
* overlay root。scrim
|
|
216
|
+
* scrim 的挂载点:**与 panel 在没有 scrim 时会落到的容器同源** —— 最近主题的 theme-owned
|
|
217
|
+
* overlay root。scrim 就是那个容器的替身(panel 必须挂进 scrim,见 `updated()` 的顺序注释),
|
|
218
218
|
* 所以它得站在容器该站的位置上。
|
|
219
219
|
*
|
|
220
220
|
* 为什么不能在整棵 DOM 里随便挑位置:`--wui-color-*` 由 `<web-ui-theme>` 写在自己的 `:host`
|
|
221
221
|
* 上,只沿 DOM 树向下继承。scrim 若挂到 `document.body`,而 `<web-ui-theme>` 是 body 的**子节点**,
|
|
222
|
-
* 这条继承链就断了 ——
|
|
223
|
-
*
|
|
224
|
-
*
|
|
222
|
+
* 这条继承链就断了 —— panel 与 scrim 双双落回 `var(--wui-color-*, <浅色字面量>)` 的 fallback,
|
|
223
|
+
* 深色外观下 menu 是一块浅色玻璃(app 侧的 `dark:` 变体也够不到它)。挂进 theme overlay root
|
|
224
|
+
* 之后,panel 与 scrim 跟 dropdown / select 一样吃同一份 token。
|
|
225
225
|
*
|
|
226
226
|
* 无主题时保持历史行为挂在 `document.body`:那条路径本来就没有 token 作用域可继承,改用
|
|
227
227
|
* fallback overlay root 只会改变既有测试与消费者的观察面,不换来任何东西。
|
|
@@ -230,38 +230,38 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
230
230
|
/**
|
|
231
231
|
* 关掉并摘除 scrim。幂等:关闭分支与 `disconnectedCallback` 都会调。
|
|
232
232
|
*
|
|
233
|
-
* `close()` 触发 UA
|
|
233
|
+
* `close()` 触发 UA 的焦点归还,**必须先于 panel `remove()`** —— 理由见 `_closeMenuAfterPresence`。
|
|
234
234
|
*/
|
|
235
235
|
private _closeScrim;
|
|
236
236
|
/**
|
|
237
|
-
* 只让 scrim
|
|
237
|
+
* 只让 scrim 出顶层,不摘除元素。panel 挂在 scrim 里,摘了就把退场动画一起摘掉。
|
|
238
238
|
*
|
|
239
239
|
* 提前出顶层是为了焦点(理由见 `close()`)。代价是这个尾巴上 scrim 已经在 top layer
|
|
240
|
-
*
|
|
240
|
+
* 之外:下层恢复可命中,且 panel 的层级退回普通流 —— 在「menu 开在 drawer/dialog 之上」
|
|
241
241
|
* 的场景里,退场动画会被下层的 top layer 盖住。
|
|
242
242
|
*
|
|
243
|
-
* 这条尾巴只有一次退场过渡的时长(默认 160ms
|
|
244
|
-
* 摘除仍在 `_closeScrim()
|
|
243
|
+
* 这条尾巴只有一次退场过渡的时长(默认 160ms),且只发生在 menu item 激活时。
|
|
244
|
+
* 摘除仍在 `_closeScrim()`,那时 panel 已经走完。
|
|
245
245
|
*/
|
|
246
246
|
private _releaseScrimModality;
|
|
247
247
|
/**
|
|
248
|
-
* 点 scrim =
|
|
249
|
-
* 只有落点就是 scrim
|
|
248
|
+
* 点 scrim = 关 menu(Q12:必关,不给 prop option),与 dialog / drawer 的实现同形:
|
|
249
|
+
* 只有落点就是 scrim 本身才算「点外面」,落在 panel(scrim 的后代)上不算。
|
|
250
250
|
*
|
|
251
|
-
*
|
|
252
|
-
* 落在 scrim
|
|
251
|
+
* 落点判据之所以只能是这两个,是模态给的:menu 打开期间下层收不到命中,事件只可能
|
|
252
|
+
* 落在 scrim 或 panel 上,两者都由本组件判定。
|
|
253
253
|
*
|
|
254
|
-
* 更早的实现在 document 捕获阶段挂 click
|
|
255
|
-
*
|
|
254
|
+
* 更早的实现在 document 捕获阶段挂 click 监听,把「点外面」理解成「命中测试落在 menu
|
|
255
|
+
* panel 之外」—— 宿主 light DOM 的每一行都满足这个条件,于是判定与行激活成了同一个
|
|
256
256
|
* click 的两个后果,中间没有任何仲裁,下层目标先被激活。那条监听器连同它的实现已一并
|
|
257
257
|
* 删除:模态化之后它没有任何还能生效的场景,别再挂回来。
|
|
258
258
|
*/
|
|
259
259
|
private _onScrimClick;
|
|
260
260
|
/**
|
|
261
261
|
* Escape 的**唯一**关闭路径仍是共享仲裁者(`_overlay.requestClose` → `closeDeepestOrAll`),
|
|
262
|
-
*
|
|
262
|
+
* 它按层深决定是关最深 submenu 还是关整张 menu。原生 dialog 自己也监听 Escape 并派发
|
|
263
263
|
* `cancel`,若放任默认行为,UA 会直接关掉 scrim —— 那条路径不经过组件状态机,
|
|
264
|
-
* 既可能漏派 `open-change
|
|
264
|
+
* 既可能漏派 `open-change`,也会把「关最深一层」的语义降级成「整张 menu 没了」。
|
|
265
265
|
*
|
|
266
266
|
* 因此这里只吞掉默认行为,不重复关闭。仲裁者那条路径已经会走 `_closeFromUser()`。
|
|
267
267
|
*/
|
|
@@ -279,7 +279,7 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
279
279
|
private _onPointerUp;
|
|
280
280
|
private _onPointerCancel;
|
|
281
281
|
private _cancelLongPress;
|
|
282
|
-
/**
|
|
282
|
+
/** 该事件是否是长按开 menu 之后、浏览器为同一次触摸补发的事件。 */
|
|
283
283
|
private _isLongPressFollowUp;
|
|
284
284
|
/** 结束「同一次手势的补发事件」窗口。与 _longPressOpenedAt 的清零同源,两处都要清。 */
|
|
285
285
|
private _clearLongPressFollowUp;
|
|
@@ -296,7 +296,7 @@ export declare class WebUiContextMenu extends LitElement {
|
|
|
296
296
|
private _closeLastSubmenuOrMenu;
|
|
297
297
|
private _preventBackgroundScroll;
|
|
298
298
|
private _syncScrollLock;
|
|
299
|
-
/** 按 panel
|
|
299
|
+
/** 按 panel 撤销一层 submenu 的登记;handle 已随根层递归撤销时幂等无操作。 */
|
|
300
300
|
private _releaseSubmenu;
|
|
301
301
|
private _isMenuPanelEvent;
|
|
302
302
|
render(): import('lit').TemplateResult<1>;
|