Skip to content

UN-4287 [FEAT] Add the v1 text inputs on one field frame - #2416

Draft
athul-rs wants to merge 7 commits into
UN-4299-design-tokens-themesfrom
UN-4287-v1-text-inputs
Draft

athul-rs wants to merge 7 commits into
UN-4299-design-tokens-themesfrom
UN-4287-v1-text-inputs

Conversation

@athul-rs

Copy link
Copy Markdown
Contributor

What

  • Frontend v1 text inputs (P1-T03), in frontend/src/v1/components/ui/form/, all drawn by one FieldFrame (border, radius, padding, focus ring and the hover, invalid, disabled and read-only states):
    • Input: height 32, with leading and trailing icons and prefix and suffix text.
    • Textarea, and PromptTextarea: at least 104 tall, grows with its text to a maximum, then scrolls. Enter submits, Shift+Enter breaks the line, and an IME composition never submits.
    • SearchField: Escape clears it when it has a value; on an empty field Escape reaches the dialog or popover around it. A Clear button does the same for the mouse.
    • SecretField: masked by default; Reveal (aria-pressed) and Copy, which copies the real value and reports the result through onCopyResult.
    • Field, Label, HelperText, ErrorText: Field wires the label, aria-describedby and aria-invalid for whichever control it holds; ErrorText is announced (role="alert").
  • lib/compose-refs.ts, which merges a component's own ref with the caller's, including React 19 ref cleanups.

Why

  • P1-T03 (UN-4287): the shared text controls every v1 form uses (P1-T06 form system, settings, prompt cards).

How

  • Built on the design tokens PR UN-4299 [FEAT] v1 design tokens, themes and style isolation (P0-T04) #2399: ui: classes only, no raw colours or lengths. Styling follows the kit's Input Group frames (216:8125) and the search and prompt spec frames (676:9483, 676:9513, 676:9543).
  • SearchField inside a Radix dialog: Radix calls preventDefault on Escape in the capture phase when the dialog chooses not to close. The field ignores that, so a dialog that skips dismissal for targets carrying data-escape-clears gets "Escape clears the field, then closes the dialog", which a test with a real Radix Dialog checks. Only a preventDefault from the caller's own onKeyDown cancels the clear.
  • SecretField sets autoComplete="new-password", data-1p-ignore, data-lpignore and turns off spellcheck, autocapitalise and autocorrect, so password managers and browser spellcheck services leave the value alone; callers cannot override these. Copy on an empty field writes nothing and reports {status: "empty"}.
  • onCopyResult is required: no toast component exists yet (P1-T13 builds it), so the caller raises the toast. This is the seam to swap when it lands.
  • Where field-sizing: content is unsupported (Firefox), PromptTextarea sizes itself from scrollHeight through the --prompt-height custom property, and re-fits after a form reset.

Can this PR break any existing features. If yes, please list possible items. If no, please explain why. (PS: Admins do not merge the PR without this section filled)

  • No. Everything is new under frontend/src/v1/, which no v0 route renders. No v0 file, dependency or build file changes.

Database Migrations

  • None.

Env Config

  • None.

Relevant Docs

  • Task spec: P1-T03 on the Frontend v1 Confluence space. Designs repo pinned at 83e4fff.

Related Issues or PRs

Dependencies Versions

  • None.

Notes on Testing

  • lint:v1, typecheck:v1 and biome check src/v1 clean. test:v1: 118 vitest tests (83 on the base) and 59 boundary fixtures. The same checks pass on a merge with the newest scaffold commits (65 fixtures).
  • Every behavioural guard was mutation-checked (Shift+Enter, IME composition including Safari's keyCode 229, Escape clear and bubbling, the Radix dialog case, read-only and disabled locks from props and from Field, copy result handling, empty copy, privacy attributes, ref cleanups, the fallback re-fit): removing each one fails a test.
  • Reviewed locally with the team's 19-lens review (no Critical or High; the Mediums are fixed in this branch).

Design questions (no frame answers them)

  1. Input value text and radius: the dev-spec board says 13/18 and radius 8, the kit and search frame 14/20 and radius 10, the prompt frame 13/20 and radius 8. This PR uses 14/20, radius 8.
  2. Dark-mode field fill: the kit names dark:input/30, which no token carries. This PR uses --background in every theme.
  3. Invalid focus ring: --destructive at 50% (one focus treatment) or the kit's raw red at 20%? Disabled fill #e5e5e540 has no token.
  4. Hover and read-only states for inputs, the helper and error text under a control, a required marker, SecretField's Reveal and Copy, and SearchField's Clear have no frame.
  5. PromptTextarea's maximum height has no frame; 240 is a placeholder.

Screenshots

  • None yet; no v1 route renders these components.

Checklist

I have read and understood the Contribution Guidelines.

🤖 Generated with Claude Code

athul-rs and others added 7 commits October 29, 2026 22:23
A component that needs its own node and also forwards the caller's ref
feeds both through one callback ref.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FieldFrame gives every text control the same border, radius, padding
and focus ring, with hover, invalid, disabled and read-only states as
data attributes. On it: Input (32 tall, leading/trailing icons and
prefix/suffix text), Textarea, PromptTextarea (104 minimum, grows to a
maximum then scrolls; Enter submits, Shift+Enter breaks the line, an
IME composition never submits), SearchField (Escape clears a value and
stops; on an empty field it bubbles) and SecretField (masked, Reveal
with aria-pressed, Copy writes the real value and reports the outcome
to the caller for the toast).

Field wires Label, HelperText and ErrorText to the control: the label
names it, aria-describedby lists the rendered texts, and an error sets
aria-invalid and renders as role=alert.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A caller that passes id now gets stable describedby ids, and the markup
snapshot no longer depends on useId render order.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…up frames

Fill is the background token (216:8126), inputs pad 10 with a 6 gap
(676:9487), the prompt pads 10 vertically (676:9547), and a label sits 6
above its control (639:5296).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The composed callback now returns a cleanup, so React calls it instead
of the callback with null: a ref that returned its own cleanup gets it
run, and every other ref is set to null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
SearchField: a Radix dialog that skips dismissal for data-escape-clears
calls preventDefault in the capture phase, so the clear now ignores a
preventDefault made before it; only the caller's own onKeyDown cancels
it. The lock reads disabled and read-only from props and Field alike,
and a controlled field with no onValueChange lets Escape through.

PromptTextarea: Enter does not submit when read-only. Where
field-sizing is missing, it re-fits after its form resets and keeps the
height automatic while scrollHeight is 0.

SecretField: onCopyResult is required and is called once, after the
clipboard write, so a throwing handler is not reported as a failed
copy. An empty value is not written and reports status empty.
Reveal and Copy follow a disabled Field and stay usable when
read-only. new-password, the password-manager ignore hints and
spellcheck off are set after the props spread.

Tests cover each fix, a real Radix Dialog, and the wiring of all five
controls to Field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Object and callback refs reach the control, and are released on
unmount, for the three controls that compose refs. FieldAction inside a
form clicks without submitting it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant