fix(composer): autosize Create Worktree note on PR prefill (#10580)

* fix(composer): autosize Create Worktree note on PR prefill

The Note textarea only grew on onInput, so programmatic PR title prefills
left the box at one row with overflow hidden. Resize whenever the note
value changes and allow scroll under max-height (fixes #10575).

* review: size the composer note with field-sizing instead of a measure pass

The prefill bug is the failure mode of imperative sizing: the height is only
recomputed at the events someone remembered to hook, so a programmatic setNote
(and a pane resize, and a font reflow) leaves it stale. Let the layout engine
own the height, matching NativeChatComposerField and LinearIssueTextEditor.

Drops the extracted helper and its unit test — that test asserted the two
assignments it wrote and stayed green with the bug present. The composer test
now pins the class contract and goes red on the pre-fix markup.

---------

Co-authored-by: Brennan Benson <79079362+brennanb2025@users.noreply.github.com>
This commit is contained in:
Wooseong Kim 2026-08-10 09:47:28 +09:00 committed by GitHub
parent 1d3decdbc4
commit ac830689c4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 43 additions and 7 deletions

View File

@ -871,3 +871,42 @@ describe('NewWorkspaceComposerCard folder task source mode', () => {
expect(recipeChanges).toEqual([null])
})
})
describe('NewWorkspaceComposerCard note sizing', () => {
// Sizing is layout-driven (field-sizing) rather than a JS measure pass, and happy-dom
// has no layout engine, so these assert the class contract that produces the growth.
afterEach(() => {
act(() => current?.root.unmount())
current?.container.remove()
current = null
})
function findNoteTextarea(container: HTMLElement): HTMLTextAreaElement {
const label = [...container.querySelectorAll('label')].find(
(candidate) => candidate.textContent?.trim() === 'Note'
)
const textarea = label?.parentElement?.querySelector('textarea')
expect(textarea).toBeTruthy()
return textarea as HTMLTextAreaElement
}
it('sizes from the note value, so a PR prefill written straight to state still shows in full', () => {
// #10575: the prefill never fires an input event, so nothing but the value can drive height.
current = renderCard({
advancedOpen: true,
note: `PR #10575 — ${'a note title long enough to wrap over several lines '.repeat(3)}`
})
expect(findNoteTextarea(current.container).className).toContain('[field-sizing:content]')
})
it('keeps a note past the height cap readable instead of clipping it', () => {
current = renderCard({ advancedOpen: true, note: 'a'.repeat(4000) })
const { className } = findNoteTextarea(current.container)
expect(className).toContain('max-h-40')
expect(className).toContain('overflow-y-auto')
expect(className).toContain('scrollbar-sleek')
expect(className).not.toContain('overflow-hidden')
})
})

View File

@ -1001,18 +1001,15 @@ export default function NewWorkspaceComposerCard({
value={note}
onChange={(event) => onNoteChange(event.target.value)}
onPaste={handleNotePaste}
onInput={(event) => {
// Why: reset then size to content so short notes stay compact and long ones grow without a scrollbar until max-h clamps.
const ta = event.currentTarget
ta.style.height = 'auto'
ta.style.height = `${ta.scrollHeight}px`
}}
placeholder={translate(
'auto.components.NewWorkspaceComposerCard.090cfedeb4',
'Write a note'
)}
rows={1}
className="w-full min-w-0 resize-none overflow-hidden rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 max-h-40"
// Why (#10575): field-sizing:content grows the note with its value, so a PR/MR
// prefill written straight to state sizes like typed text — an onInput measure
// pass never saw it. Past the max-h clamp the sleek scrollbar keeps it readable.
className="w-full min-w-0 resize-none overflow-y-auto scrollbar-sleek rounded-md border border-input bg-transparent px-3 py-1.5 text-sm shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [field-sizing:content] max-h-40"
/>
</div>