From 4845caab1339d66e52ccaab8c4bbcbedc759f7a2 Mon Sep 17 00:00:00 2001 From: Zhong Date: Mon, 8 Sep 2025 18:30:05 +0800 Subject: [PATCH] fix(components): [mention] correct cursor position (#22070) * fix(components): [mention] correct cursor position * test: add test * fix: use wrapper as a reference --- .../mention/__tests__/mention.test.tsx | 70 +++++++++++++++++++ packages/components/mention/src/mention.vue | 6 +- 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/packages/components/mention/__tests__/mention.test.tsx b/packages/components/mention/__tests__/mention.test.tsx index 3285c8c8f3..f7982a6d72 100644 --- a/packages/components/mention/__tests__/mention.test.tsx +++ b/packages/components/mention/__tests__/mention.test.tsx @@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' import Form from '@element-plus/components/form' import Mention from '../src/mention.vue' +import * as helper from '../src/helper' describe('Mention.vue', () => { beforeEach(() => { @@ -146,4 +147,73 @@ describe('Mention.vue', () => { expect(option.attributes('aria-disabled')).toBe('true') expect(option.classes()).toContain('is-disabled') }) + + test('should ensure the cursor position is correct', async () => { + vi.spyOn(helper, 'getCursorPosition').mockReturnValue({ + top: 7, + left: 14, + height: 21, + }) + + const wrapper = mount(Mention, { + attachTo: document.body, + props: { options, style: { marginTop: '100px', marginLeft: '100px' } }, + }) + + const elInputEl = wrapper.find('.el-input').element + const inputEl = wrapper.find('input').element + + const mockBoundingClientRect = ( + el: Element, + rect: Partial = {} + ) => { + const defaultRect: DOMRect = { + x: 0, + y: 0, + width: 100, + height: 100, + top: 0, + left: 0, + bottom: 0, + right: 0, + toJSON: () => {}, + } + + return vi + .spyOn(el, 'getBoundingClientRect') + .mockReturnValue(Object.assign(defaultRect, rect)) + } + + // Actual information obtained on the browser + mockBoundingClientRect(elInputEl, { + width: 320, + height: 32, + left: 100, + x: 100, + top: 100, + y: 100, + }) + mockBoundingClientRect(inputEl, { + width: 298, + height: 30, + left: 111, + x: 111, + top: 101, + y: 101, + }) + + inputEl.focus() + await wrapper.find('input').setValue('@') + vi.advanceTimersByTime(150) + await nextTick() + + const cursorStyles = wrapper + .find('.el-tooltip__trigger') + .attributes('style') + + expect(cursorStyles).toContain('left: 125px') + expect(cursorStyles).toContain('top: 108px') + + vi.restoreAllMocks() + }) }) diff --git a/packages/components/mention/src/mention.vue b/packages/components/mention/src/mention.vue index b742ce18e7..845c4b891c 100644 --- a/packages/components/mention/src/mention.vue +++ b/packages/components/mention/src/mention.vue @@ -259,14 +259,14 @@ const syncCursor = () => { const caretPosition = getCursorPosition(inputEl) const inputRect = inputEl.getBoundingClientRect() - const elInputRect = elInputRef.value!.$el.getBoundingClientRect() + const wrapperRect = wrapperRef.value!.getBoundingClientRect() cursorStyle.value = { position: 'absolute', width: 0, height: `${caretPosition.height}px`, - left: `${caretPosition.left + inputRect.left - elInputRect.left}px`, - top: `${caretPosition.top + inputRect.top - elInputRect.top}px`, + left: `${caretPosition.left + inputRect.left - wrapperRect.left}px`, + top: `${caretPosition.top + inputRect.top - wrapperRect.top}px`, } }