fix: anchor ai mode caret after wikilinks
This commit is contained in:
@@ -176,6 +176,8 @@ export function InlineWikilinkEditorField({
|
||||
segments: InlineWikilinkSegment[]
|
||||
typeEntryMap: Record<string, VaultEntry>
|
||||
}) {
|
||||
const needsTrailingCaretAnchor = segments[segments.length - 1]?.kind === 'chip'
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
{value.length === 0 && placeholder && (
|
||||
@@ -218,6 +220,7 @@ export function InlineWikilinkEditorField({
|
||||
/>
|
||||
)
|
||||
))}
|
||||
{needsTrailingCaretAnchor ? '\u200B' : null}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -68,7 +68,30 @@ export function readSelectionIndex(root: HTMLDivElement): number {
|
||||
}
|
||||
|
||||
function boundaryAtEditorEnd(root: HTMLDivElement): SelectionBoundary {
|
||||
return { container: root, offset: root.childNodes.length }
|
||||
const endBoundary = findTextBoundaryAtEdge(root, 'end')
|
||||
return endBoundary ?? { container: root, offset: root.childNodes.length }
|
||||
}
|
||||
|
||||
function findTextBoundaryAtEdge(
|
||||
node: Node,
|
||||
edge: 'start' | 'end',
|
||||
): SelectionBoundary | null {
|
||||
if (node.nodeType === Node.TEXT_NODE) {
|
||||
return {
|
||||
container: node,
|
||||
offset: edge === 'start' ? 0 : node.textContent?.length ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
const children = Array.from(node.childNodes)
|
||||
const orderedChildren = edge === 'start' ? children : children.toReversed()
|
||||
|
||||
for (const child of orderedChildren) {
|
||||
const boundary = findTextBoundaryAtEdge(child, edge)
|
||||
if (boundary) return boundary
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
function boundaryForTextNode(
|
||||
@@ -97,15 +120,21 @@ function boundaryForChip(
|
||||
): { boundary: SelectionBoundary | null; remaining: number } {
|
||||
const tokenLength = chipToken(child.dataset.chipTarget ?? '').length
|
||||
if (remaining <= 0) {
|
||||
const previousSibling = node.childNodes.item(index - 1)
|
||||
return {
|
||||
boundary: { container: node, offset: index },
|
||||
boundary: previousSibling
|
||||
? findTextBoundaryAtEdge(previousSibling, 'end') ?? { container: node, offset: index }
|
||||
: { container: node, offset: index },
|
||||
remaining: 0,
|
||||
}
|
||||
}
|
||||
|
||||
if (remaining <= tokenLength) {
|
||||
const nextSibling = node.childNodes.item(index + 1)
|
||||
return {
|
||||
boundary: { container: node, offset: index + 1 },
|
||||
boundary: nextSibling
|
||||
? findTextBoundaryAtEdge(nextSibling, 'start') ?? { container: node, offset: index + 1 }
|
||||
: { container: node, offset: index + 1 },
|
||||
remaining: 0,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test, expect } from '@playwright/test'
|
||||
import { test, expect, type Page } from '@playwright/test'
|
||||
import { openCommandPalette } from './helpers'
|
||||
import {
|
||||
expectNoPageErrors,
|
||||
@@ -7,6 +7,32 @@ import {
|
||||
trackPageErrors,
|
||||
} from './inlineWikilinkEditorHelpers'
|
||||
|
||||
async function readCaretGapAfterChip(page: Page) {
|
||||
return page.evaluate(() => {
|
||||
const editor = document.querySelector('[data-testid="command-palette-ai-input"]')
|
||||
if (!(editor instanceof HTMLElement)) return null
|
||||
|
||||
const chip = editor.querySelector('[data-testid="inline-wikilink-chip"]')
|
||||
if (!(chip instanceof HTMLElement)) return null
|
||||
|
||||
const selection = window.getSelection()
|
||||
if (selection === null) return null
|
||||
if (selection.rangeCount === 0) return null
|
||||
|
||||
const range = selection.getRangeAt(0).cloneRange()
|
||||
range.collapse(true)
|
||||
|
||||
const caretRect = range.getBoundingClientRect()
|
||||
if (caretRect.height === 0) return null
|
||||
|
||||
return caretRect.left - chip.getBoundingClientRect().right
|
||||
})
|
||||
}
|
||||
|
||||
async function expectCaretAfterChip(page: Page) {
|
||||
await expect.poll(() => readCaretGapAfterChip(page)).toBeLessThan(24)
|
||||
}
|
||||
|
||||
test.describe('Command palette AI mode regression', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.route('**/api/vault/ping', route => route.fulfill({ status: 503 }))
|
||||
@@ -35,6 +61,7 @@ test.describe('Command palette AI mode regression', () => {
|
||||
|
||||
await page.getByTestId('wikilink-menu').getByText('Build Laputa App').click()
|
||||
await expect(aiInput.getByTestId('inline-wikilink-chip')).toContainText('Build Laputa App')
|
||||
await expectCaretAfterChip(page)
|
||||
|
||||
await page.keyboard.type(' essay')
|
||||
await expectNormalizedEditorText(aiInput, 'edit my Build Laputa App essay')
|
||||
|
||||
Reference in New Issue
Block a user