Profile bio textarea: auto-grow to fit content (caret in view, bounded max height) #419

Closed
opened 2026-09-12 22:55:02 +00:00 by crueber · 3 comments
Owner

Profile bio textarea: auto-grow to fit content

What's requested

The markdown bio textarea on the owner profile edit form is a fixed rows="6" box: typing or pasting more than ~6 lines forces an inner scrollbar instead of the field growing to show the content, and the caret can scroll out of view exactly when the user is typing. Requested behavior:

  • The textarea grows (and shrinks) to fit its content, starting from its current rows="6" height.
  • On every input — including Enter/newline — the caret line stays scrolled into view.
  • A sane max bound so a very long bio doesn't grow past the viewport: cap the grown height (e.g. max-height around 50vh with overflow-y: auto beyond it); exact cap is implementer's choice, but it must be stated in the implementation.

Evidence

  • Owner profile edit form: web/src/pages/Repos.jsx, ProfileForm — <textarea id="profile-bio" class="input w-full font-mono text-sm" rows="6" …> (around line 172). Fixed rows, no grow logic.
  • Same fixed-rows pattern on the org profile edit form: web/src/pages/Org.jsx:216 (rows="5").

Architecture notes

  • No auto-grow/autosize helper exists in the tree today (grep -rn 'auto-resize\|autogrow\|field-sizing' web/ is empty).
  • Repo convention for shared headless logic is a pure module in web/src/lib/ (see settingsNav, attachUpload) — a small web/src/lib/autogrow.js (e.g. grow(textarea, { minRows, maxRows }) using scrollHeight, exported for unit tests) fits; a pure-CSS field-sizing: content route is acceptable only if the fallback story for non-supporting browsers is noted. Implementer's call; one shared helper consumed by both textareas, not per-page copies.
  • If the caret-scroll behavior needs the textarea on the page, keep the DOM-touching function separate from any measurable pure logic (height computation from value can be headless-tested; pixel scroll handling is DOM-side).

Scope

  • Apply to the owner profile bio textarea (Repos.jsx) and the org profile bio textarea (Org.jsx) — same component shape, same fix. No other textareas change in this ticket.
  • No backend change: bio_markdown write path (repos.owners.updateProfile, profileSaveBody) is untouched.

Acceptance criteria

  • Typing more than 6 lines in the owner profile bio grows the textarea instead of showing an inner scrollbar; deleting lines shrinks it back toward rows="6".
  • After pressing Enter at the end of the bio, the caret line is visible (not scrolled out of the box).
  • A very long bio (e.g. 60 lines) is bounded by a stated max height with the textarea scrolling internally past it.
  • The org profile bio textarea behaves identically.
  • Both textareas consume one shared helper (or one documented CSS approach), not duplicated per-page logic.
# Profile bio textarea: auto-grow to fit content ## What's requested The markdown bio textarea on the owner profile edit form is a fixed `rows="6"` box: typing or pasting more than ~6 lines forces an inner scrollbar instead of the field growing to show the content, and the caret can scroll out of view exactly when the user is typing. Requested behavior: - The textarea grows (and shrinks) to fit its content, starting from its current `rows="6"` height. - On every input — including Enter/newline — the caret line stays scrolled into view. - A sane max bound so a very long bio doesn't grow past the viewport: cap the grown height (e.g. `max-height` around 50vh with `overflow-y: auto` beyond it); exact cap is implementer's choice, but it must be stated in the implementation. ## Evidence - Owner profile edit form: `web/src/pages/Repos.jsx`, `ProfileForm` — `<textarea id="profile-bio" class="input w-full font-mono text-sm" rows="6" …>` (around line 172). Fixed rows, no grow logic. - Same fixed-rows pattern on the org profile edit form: `web/src/pages/Org.jsx:216` (`rows="5"`). ## Architecture notes - No auto-grow/autosize helper exists in the tree today (`grep -rn 'auto-resize\|autogrow\|field-sizing' web/` is empty). - Repo convention for shared headless logic is a pure module in `web/src/lib/` (see `settingsNav`, `attachUpload`) — a small `web/src/lib/autogrow.js` (e.g. `grow(textarea, { minRows, maxRows })` using `scrollHeight`, exported for unit tests) fits; a pure-CSS `field-sizing: content` route is acceptable only if the fallback story for non-supporting browsers is noted. Implementer's call; one shared helper consumed by both textareas, not per-page copies. - If the caret-scroll behavior needs the textarea on the page, keep the DOM-touching function separate from any measurable pure logic (height computation from value can be headless-tested; pixel scroll handling is DOM-side). ## Scope - Apply to the owner profile bio textarea (`Repos.jsx`) and the org profile bio textarea (`Org.jsx`) — same component shape, same fix. No other textareas change in this ticket. - No backend change: bio_markdown write path (`repos.owners.updateProfile`, `profileSaveBody`) is untouched. ## Acceptance criteria - [ ] Typing more than 6 lines in the owner profile bio grows the textarea instead of showing an inner scrollbar; deleting lines shrinks it back toward `rows="6"`. - [ ] After pressing Enter at the end of the bio, the caret line is visible (not scrolled out of the box). - [ ] A very long bio (e.g. 60 lines) is bounded by a stated max height with the textarea scrolling internally past it. - [ ] The org profile bio textarea behaves identically. - [ ] Both textareas consume one shared helper (or one documented CSS approach), not duplicated per-page logic.
crueber added this to the v1 milestone 2026-09-12 22:55:20 +00:00
Author
Owner

Fixed by PR #425 (#425) — shared web/src/lib/autogrow.js consumed by both bio textareas, cap exactly 50vh, 18 new headless tests green, vite build clean, no backend change.

Fixed by PR #425 (https://git.packden.us/crueber/walhub/pulls/425) — shared web/src/lib/autogrow.js consumed by both bio textareas, cap exactly 50vh, 18 new headless tests green, vite build clean, no backend change.
Author
Owner

REVIEW PR #425 (fix/issue-419, bio autogrow) — verified in scratch worktree, all 8 checks pass. No browser per task scope (node tests + reasoning; no browser-facing serving change — static textarea wiring only).

(1) Shared helper, both textareas: PASS. web/src/lib/autogrow.js (new, 136 lines) imported by both web/src/pages/Repos.jsx:35 and web/src/pages/Org.jsx:16; wiring identical (ref→initAutogrow, onInput→setBio+growTextarea, createEffect→getBio+growTextarea). No per-page copies. Each page has exactly one <textarea (the bio one) — no other textareas touched.
(2) Grow+shrink: PASS. growTextarea sets height:auto BEFORE reading scrollHeight (autogrow.js:112) — the classic no-shrink bug avoided; floor recorded from mounted rows height via initAutogrow offsetHeight/clientHeight (autogrow.js:74-88); rows=6/5 stay as no-JS fallback.
(3) Caret in view: PASS, sound. While fitted the box IS the content (overflow hidden, no inner scroll — caret necessarily visible); past the cap only the trailing-caret case is pinned (scrollTop=scrollHeight, autogrow.js:123-129), mid-text carets correctly left to native scroll.
(4) Cap exact 50vh + overflow auto: PASS, stated. AUTOGROW_MAX_VH=50 (autogrow.js:30), stated in header comment + PR body; overflowY auto iff overflowing, hidden while fitted (no flicker).
(5) Seeded/programmatic refit: PASS. Both pages have createEffect tracking getBio + bioRef guard (covers mount + async Org seedAll); onInput covers keystrokes/paste; refit idempotent so double-fire harmless. Org ProfileTab mounts only under Show when={getTab()==="Profile"} (Org.jsx:785), so the floor is measured while visible, not against a hidden tab.
(6) DOM/pure separation: PASS. Pure autogrowMaxPx/clampAutogrowHeight headless-tested; DOM initAutogrow/growTextarea run against stub elements in node --test — matches attachUpload/settingsNav convention.
(7) Scope/hygiene: PASS. 4 files only (autogrow.js, Repos.jsx, Org.jsx, autogrow.test.js); no backend change; no package/manifest change — law 1 (dependency budget) holds.
(8) Law-12 no-doc call: REASONABLE. No doc pins textarea sizing, so code/docs do not disagree; bio_markdown write path untouched.

VERIFY (scratch worktree /tmp/pr425 @ 0d7200b, since removed): autogrow.test.js 18/18 pass; full suite 867 tests → 865 pass / 2 fail, and the 2 fails are smoke.test.js live-server probes which fail identically on clean main (re-ran smoke.test.js on main: 1 pass / 2 fail) — pre-existing, unrelated. vite build clean in 2.46s (only pre-existing >500kB chunk-size warning). Org.jsx already imports createEffect (line 6) — no missing-import bug. No fixes needed; nothing pushed.

MERGE RECOMMENDATION: ready to merge.

REVIEW PR #425 (fix/issue-419, bio autogrow) — verified in scratch worktree, all 8 checks pass. No browser per task scope (node tests + reasoning; no browser-facing serving change — static textarea wiring only). (1) Shared helper, both textareas: PASS. web/src/lib/autogrow.js (new, 136 lines) imported by both web/src/pages/Repos.jsx:35 and web/src/pages/Org.jsx:16; wiring identical (ref→initAutogrow, onInput→setBio+growTextarea, createEffect→getBio+growTextarea). No per-page copies. Each page has exactly one <textarea (the bio one) — no other textareas touched. (2) Grow+shrink: PASS. growTextarea sets height:auto BEFORE reading scrollHeight (autogrow.js:112) — the classic no-shrink bug avoided; floor recorded from mounted rows height via initAutogrow offsetHeight/clientHeight (autogrow.js:74-88); rows=6/5 stay as no-JS fallback. (3) Caret in view: PASS, sound. While fitted the box IS the content (overflow hidden, no inner scroll — caret necessarily visible); past the cap only the trailing-caret case is pinned (scrollTop=scrollHeight, autogrow.js:123-129), mid-text carets correctly left to native scroll. (4) Cap exact 50vh + overflow auto: PASS, stated. AUTOGROW_MAX_VH=50 (autogrow.js:30), stated in header comment + PR body; overflowY auto iff overflowing, hidden while fitted (no flicker). (5) Seeded/programmatic refit: PASS. Both pages have createEffect tracking getBio + bioRef guard (covers mount + async Org seedAll); onInput covers keystrokes/paste; refit idempotent so double-fire harmless. Org ProfileTab mounts only under Show when={getTab()==="Profile"} (Org.jsx:785), so the floor is measured while visible, not against a hidden tab. (6) DOM/pure separation: PASS. Pure autogrowMaxPx/clampAutogrowHeight headless-tested; DOM initAutogrow/growTextarea run against stub elements in node --test — matches attachUpload/settingsNav convention. (7) Scope/hygiene: PASS. 4 files only (autogrow.js, Repos.jsx, Org.jsx, autogrow.test.js); no backend change; no package/manifest change — law 1 (dependency budget) holds. (8) Law-12 no-doc call: REASONABLE. No doc pins textarea sizing, so code/docs do not disagree; bio_markdown write path untouched. VERIFY (scratch worktree /tmp/pr425 @ 0d7200b, since removed): autogrow.test.js 18/18 pass; full suite 867 tests → 865 pass / 2 fail, and the 2 fails are smoke.test.js live-server probes which fail identically on clean main (re-ran smoke.test.js on main: 1 pass / 2 fail) — pre-existing, unrelated. vite build clean in 2.46s (only pre-existing >500kB chunk-size warning). Org.jsx already imports createEffect (line 6) — no missing-import bug. No fixes needed; nothing pushed. MERGE RECOMMENDATION: ready to merge.
Author
Owner

Fixed by PR #425 (review clean — all 8 checks pass, shared helper, 50vh cap, both textareas), merged. Closing.

Fixed by PR #425 (review clean — all 8 checks pass, shared helper, 50vh cap, both textareas), merged. Closing.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
crueber/walhub#419
No description provided.