fix(desktop): keep the hint off the text during IME composition

Input events are deliberately skipped for the duration of an IME composition
(they carry uncommitted preedit text), so nothing clears the empty marker
until compositionend — the hint kept painting behind the hiragana the user was
composing. Drop the marker as composition starts; the normalizer restores it
if composition ends with nothing committed.

Taking the hint out of the text flow fixed the displacement half of #75960 on
its own — preedit now starts at the field's left edge either way — but the
overlap needed this too.

Co-authored-by: Ryuichi Natori <to-na@users.noreply.github.com>
This commit is contained in:
Brooklyn Nicholson 2026-08-01 16:56:48 -05:00
parent 414e5af114
commit 93ec02bf79
3 changed files with 39 additions and 1 deletions

View File

@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
beginComposerComposition,
composerPlainText,
deleteChipBeforeCaret,
normalizeComposerEditorDom,
@ -159,6 +160,26 @@ describe('an emptied composer shows its placeholder again', () => {
expect(el.matches(PLACEHOLDER_SHOWS)).toBe(false)
})
// Input events are skipped for the duration of an IME composition, so nothing
// else clears the marker until it ends — the hint would sit behind the
// hiragana the user is composing (#75960).
it('hides the placeholder before IME preedit text starts', () => {
const el = emptied()
beginComposerComposition(el)
expect(el.matches(PLACEHOLDER_SHOWS)).toBe(false)
})
it('brings the placeholder back when composition ends with nothing committed', () => {
const el = emptied()
beginComposerComposition(el)
normalizeComposerEditorDom(el)
expect(el.matches(PLACEHOLDER_SHOWS)).toBe(true)
})
})
/** A directive chip, as `refChipElement` builds it. */

View File

@ -57,6 +57,7 @@ import { ActionBadges } from './micro-actions'
import { chipTypedPathOnSpace, pathifyRefs } from './path-refs'
import { QueuePanel } from './queue-panel'
import {
beginComposerComposition,
composerPlainText,
deleteChipBeforeCaret,
deleteSelectionInEditor,
@ -967,8 +968,13 @@ export function ChatBar({
// until an unrelated edit forces a sync (#39614).
flushEditorToDraft(event.currentTarget)
}}
onCompositionStart={() => {
onCompositionStart={event => {
composingRef.current = true
// Input events are skipped for the rest of the composition, so
// nothing else would clear the empty marker until it ends — and the
// hint would sit behind the preedit text the whole time (#75960).
beginComposerComposition(event.currentTarget)
}}
onDragOver={handleInputDragOver}
onDrop={handleInputDrop}

View File

@ -71,6 +71,17 @@ export function markEditorEmptiness(editor: HTMLElement) {
}
}
/** Drop the marker as IME composition starts, before any preedit text lands.
*
* Input events during composition are deliberately skipped (they carry
* uncommitted preedit text), so nothing else clears the marker until
* `compositionend` and the hint would otherwise sit behind the hiragana the
* user is composing. `normalizeComposerEditorDom` restores it if composition
* ends with nothing committed. */
export function beginComposerComposition(editor: HTMLElement) {
delete editor.dataset.empty
}
/** @see referenceRe the shared pattern every surface recognises a reference
* with. Module-level `/g` regexes carry `lastIndex`, so call sites reset it. */
export const REF_RE = referenceRe()