diff --git a/docs/assets/demo-react/en/functional-components/arco-select-editor.md b/docs/assets/demo-react/en/functional-components/arco-select-editor.md index a6bb930ee6..1b533b0de9 100644 --- a/docs/assets/demo-react/en/functional-components/arco-select-editor.md +++ b/docs/assets/demo-react/en/functional-components/arco-select-editor.md @@ -20,6 +20,8 @@ https://arco.design/react/components/select ```javascript livedemo template=vtable-react // import * as ReactVTable from '@visactor/react-vtable'; +const selectEditorPopupClassName = 'vtable-editor-select-popup'; + class ArcoListEditor { constructor() { this.root = null; @@ -48,6 +50,7 @@ class ArcoListEditor {
{ this.currentValue = value; @@ -86,19 +89,23 @@ class ArcoListEditor { } isEditorElement(target) { - // cascader创建时时在cavas后追加一个dom,而popup append在body尾部。不论popup还是dom,都应该被认为是点击到了editor区域 + // The popup is appended to body and is outside the editor container. + // Clicking the popup should still be treated as clicking inside the editor. return this.element.contains(target) || this.isClickPopUp(target); } isClickPopUp(target) { while (target) { - if (target.classList && target.classList.contains('arco-select-vtable')) { + if ( + target.classList && + (target.classList.contains(selectEditorPopupClassName) || target.classList.contains('arco-select-vtable')) + ) { return true; } - // 如果到达了DOM树的顶部,则停止搜索 + // Stop searching when reaching the top of the DOM tree. target = target.parentNode; } - // 如果遍历结束也没有找到符合条件的父元素,则返回false + // Return false if no matching parent element is found. return false; } } diff --git a/docs/assets/demo-react/zh/functional-components/arco-select-editor.md b/docs/assets/demo-react/zh/functional-components/arco-select-editor.md index c4edc624b4..9b9d4abfb4 100644 --- a/docs/assets/demo-react/zh/functional-components/arco-select-editor.md +++ b/docs/assets/demo-react/zh/functional-components/arco-select-editor.md @@ -20,6 +20,8 @@ https://arco.design/react/components/select ```javascript livedemo template=vtable-react // import * as ReactVTable from '@visactor/react-vtable'; +const selectEditorPopupClassName = 'vtable-editor-select-popup'; + class ArcoListEditor { constructor() { this.root = null; @@ -48,6 +50,7 @@ class ArcoListEditor {
{ this.currentValue = value; @@ -86,13 +89,16 @@ class ArcoListEditor { } isEditorElement(target) { - // cascader创建时时在cavas后追加一个dom,而popup append在body尾部。不论popup还是dom,都应该被认为是点击到了editor区域 + // popup append 在 body 尾部,不在编辑器容器内;点击 popup 时也应该被认为仍在编辑器区域。 return this.element.contains(target) || this.isClickPopUp(target); } isClickPopUp(target) { while (target) { - if (target.classList && target.classList.contains('arco-select-vtable')) { + if ( + target.classList && + (target.classList.contains(selectEditorPopupClassName) || target.classList.contains('arco-select-vtable')) + ) { return true; } // 如果到达了DOM树的顶部,则停止搜索 diff --git a/docs/assets/demo-vue/en/edit-data/arco-select-editor.md b/docs/assets/demo-vue/en/edit-data/arco-select-editor.md index 6d830f64f7..f58df2d8d5 100644 --- a/docs/assets/demo-vue/en/edit-data/arco-select-editor.md +++ b/docs/assets/demo-vue/en/edit-data/arco-select-editor.md @@ -19,6 +19,8 @@ https://arco.design/vue/components/select ## Code Demonstration ```javascript livedemo template=vtable-vue +const selectEditorPopupClassName = 'vtable-editor-select-popup'; + class ArcoListEditor { root = null; element = null; @@ -64,6 +66,9 @@ class ArcoListEditor { style: { height: '32px' }, placeholder: 'Select city', modelValue: this.currentValue, + triggerProps: { + contentClass: selectEditorPopupClassName + }, 'onUpdate:modelValue': value => { this.currentValue = value; self.setValue(value); @@ -110,12 +115,18 @@ class ArcoListEditor { } isEditorElement(target) { + // The popup is appended to body and is outside the editor container. + // Clicking the popup should still be treated as clicking inside the editor. return this.element?.contains(target) || this.isClickPopUp(target); } isClickPopUp(target) { while (target) { - if (target.classList && target.classList.contains('arco-select-vtable')) { + if ( + target.classList && + (target.classList.contains(selectEditorPopupClassName) || + target.classList.contains('arco-select-vtable')) + ) { return true; } target = target.parentNode; diff --git a/docs/assets/demo-vue/zh/edit-data/arco-select-editor.md b/docs/assets/demo-vue/zh/edit-data/arco-select-editor.md index c4e66c1264..e1c0a2f5cf 100644 --- a/docs/assets/demo-vue/zh/edit-data/arco-select-editor.md +++ b/docs/assets/demo-vue/zh/edit-data/arco-select-editor.md @@ -19,6 +19,8 @@ https://arco.design/vue/components/select ## 代码演示 ```javascript livedemo template=vtable-vue +const selectEditorPopupClassName = 'vtable-editor-select-popup'; + class ArcoListEditor { root = null; element = null; @@ -64,6 +66,9 @@ class ArcoListEditor { style: { height: '32px' }, placeholder: 'Select city', modelValue: this.currentValue, + triggerProps: { + contentClass: selectEditorPopupClassName + }, 'onUpdate:modelValue': value => { this.currentValue = value; self.setValue(value); @@ -110,12 +115,17 @@ class ArcoListEditor { } isEditorElement(target) { + // popup append 在 body 尾部,不在编辑器容器内;点击 popup 时也应该被认为仍在编辑器区域。 return this.element?.contains(target) || this.isClickPopUp(target); } isClickPopUp(target) { while (target) { - if (target.classList && target.classList.contains('arco-select-vtable')) { + if ( + target.classList && + (target.classList.contains(selectEditorPopupClassName) || + target.classList.contains('arco-select-vtable')) + ) { return true; } target = target.parentNode; diff --git a/docs/assets/guide/en/edit/edit_cell.md b/docs/assets/guide/en/edit/edit_cell.md index a8f2e0d7e6..3b0bbc049e 100644 --- a/docs/assets/guide/en/edit/edit_cell.md +++ b/docs/assets/guide/en/edit/edit_cell.md @@ -184,6 +184,35 @@ VTable.register.editor('custom-date', custom_date_editor); In the above example, we created a custom editor named `DateEditor` and implemented the methods required by the `IEditor` interface. Then, we register the custom editor into the VTable through the `VTable.register.editor` method for use in the table. +### Handling popup DOM from third-party components + +When a custom editor uses third-party components such as Select, Cascader, DatePicker, or Tooltip, the popup DOM may not be mounted inside the editor container. Many component libraries append popups to `body` through portal/teleport. In this case, when the user clicks a popup option, the click target is outside the editor container from VTable's perspective, and the editor may exit too early. + +It is recommended to configure a stable class on the popup root element and let `isEditorElement` check both the editor container and the popup DOM: + +```ts +const editorPopupClassName = 'vtable-editor-popup'; + +isEditorElement(target: HTMLElement) { + return this.element.contains(target) || this.isClickPopUp(target); +} + +isClickPopUp(target: HTMLElement) { + let current: HTMLElement | null = target; + while (current) { + if (current.classList?.contains(editorPopupClassName)) { + return true; + } + current = current.parentNode as HTMLElement | null; + } + return false; +} +``` + +Different component libraries expose different props for popup class names. For example, React Arco Select can use `dropdownMenuClassName`, Vue Arco Select can use `triggerProps.contentClass`, and Ant Design Select can use `classNames.popup.root` or `popupClassName` in older versions. See the React and Vue Arco Select custom editor demos for complete examples. + +If the third-party component has a close animation after selection, or if its internal implementation affects the order of `onChange` and blur events, update the editor's current value in the component change callback before calling `endEdit`. This prevents `getValue` from reading the old value. + `IEditor` [definition](https://github.com/VisActor/VTable/blob/main/packages/vtable-editors/src/types.ts): ```ts diff --git a/docs/assets/guide/zh/edit/edit_cell.md b/docs/assets/guide/zh/edit/edit_cell.md index 131974bee5..9b55dcc85b 100644 --- a/docs/assets/guide/zh/edit/edit_cell.md +++ b/docs/assets/guide/zh/edit/edit_cell.md @@ -189,6 +189,35 @@ VTable.register.editor('custom-date', custom_date_editor); 在上面的示例中,我们创建了一个名为`DateEditor`的自定义编辑器,并实现了`IEditor`接口所要求的方法。然后,我们通过`VTable.register.editor`方法将自定义编辑器注册到 VTable 中,以便在表格中使用。 +### 处理第三方组件的弹层 DOM + +当自定义编辑器使用 Select、Cascader、DatePicker、Tooltip 等第三方组件时,弹层 DOM 可能不会挂载在编辑器容器内部,而是通过 portal/teleport 追加到 `body`。这种情况下,用户点击弹层选项时,VTable 看到的点击目标不在编辑器容器内,可能会触发编辑器提前结束。 + +建议为第三方组件的弹层根节点配置一个稳定的 class,并在 `isEditorElement` 中同时判断编辑器容器和弹层 DOM: + +```ts +const editorPopupClassName = 'vtable-editor-popup'; + +isEditorElement(target: HTMLElement) { + return this.element.contains(target) || this.isClickPopUp(target); +} + +isClickPopUp(target: HTMLElement) { + let current: HTMLElement | null = target; + while (current) { + if (current.classList?.contains(editorPopupClassName)) { + return true; + } + current = current.parentNode as HTMLElement | null; + } + return false; +} +``` + +不同组件库暴露的弹层 class 配置项不同,例如 React Arco Select 可以通过 `dropdownMenuClassName` 配置,Vue Arco Select 可以通过 `triggerProps.contentClass` 配置,Ant Design Select 可以通过 `classNames.popup.root` 或旧版本的 `popupClassName` 配置。完整示例可参考 React 和 Vue 的 Arco Select 自定义编辑器 demo。 + +如果第三方组件在选中后存在关闭动画,或者 `onChange` 与失焦事件的触发顺序受组件内部实现影响,需要先在组件的变更回调中更新编辑器当前值,再按需调用 `endEdit` 结束编辑,避免 `getValue` 读取到旧值。 + `IEditor` 接口[定义](https://github.com/VisActor/VTable/blob/main/packages/vtable-editors/src/types.ts): ```ts