Skip to content

feat(ui): add form controls for webview parameter forms - #1101

Open
EhabY wants to merge 4 commits into
mainfrom
feat/ui-form-controls
Open

feat(ui): add form controls for webview parameter forms#1101
EhabY wants to merge 4 commits into
mainfrom
feat/ui-form-controls

Conversation

@EhabY

@EhabY EhabY commented Aug 28, 2026

Copy link
Copy Markdown
Collaborator

Adds the form-control vocabulary packages/ui needs for the #972 dynamic-parameter webview form: Input (text / number / password with reveal toggle), Textarea, Checkbox, Select (Radix), and Field/Label.

Design principle

Render like VS Code, not like the dashboard. VS Code's settings editor covers every configuration shape with text fields, number fields, checkboxes, and dropdowns, so the library ships exactly that vocabulary and the future form layer maps the richer dashboard form_types onto it:

form_type Rendered with
input (+ mask) Input (text / number / password + reveal)
textarea Textarea
dropdown, radio Select
checkbox, switch Checkbox
slider Input type="number" with min/max
multi-select Stacked Checkboxes in a Field (composition)
tag-select Input, comma-separated (form-layer split)

No Switch, Slider, RadioGroup, or MultiSelect widgets: VS Code has none, and every shipped control has a direct @vscode-elements parity reference, so the "Known gaps" list doesn't grow.

Implementation notes

  • Input/Textarea use controlled value + onChange(next), and Checkbox uses checked + onChange(next). Select preserves Radix controlled/uncontrolled modes and flat compound exports like DropdownMenu.
  • text-control.css paints Input, SearchInput, and Textarea from one set of ui-text-control rules; component CSS files hold only component-specific rules. The inner native input restores user-select: text, which the .ui-control wrapper turns off.
  • New tokens: --ui-checkbox-*, --ui-dropdown-*, and --ui-list-focus-{background,foreground} (quick-input list colors, matching native selectBoxStyles); all with fallback chains incl. high contrast. Highlighted select rows reuse the existing --ui-list-selection-outline (contrastActiveBorder, transparent outside high contrast).
  • The select trigger sets line-height: normal: the .ui-control line-height of 1 gives the ellipsized value a 13px box that clips 17px text, while the native dropdown draws an 18px line.
  • The select list renders without animation, like the native dropdown, which also keeps Pixel snapshots stable.
  • @radix-ui/react-select added via the workspace catalog (catalogMode: strict).
  • Stories drive state for Pixel snapshots only (no assertions in play). Each form control has a States story (unfocused) and a Focused story that focuses its first control for the focus-border snapshot. Behavior is covered by one unit test file per component, sharing test/webview/ui/helpers.tsx for the controlled-value and className/style pass-through checks.
Implementation plan

Why

Issue #972's dynamic-parameter flow returns server-evaluated parameters with form_types the QuickPick prompter cannot render reactively; a webview form built from packages/ui is required. The package has the full quality pipeline (tokens, four-theme Pixel snapshots, parity stories, webview tests, isolation lint) but had no form controls — only SearchInput and menu items.

Scope — five components

Component Approach New dep
Input Native <input>; SearchInput precedent; type passthrough incl. number (min/max) and password with reveal toggle
Textarea Native <textarea>, same tokens, vertical resize
Checkbox Native input painted with --vscode-checkbox-* geometry, codicon check; children as label
Select Radix react-select; flat compound exports matching the DropdownMenu* convention; portal on .ui-overlay; option rows support a description slot @radix-ui/react-select
Field + Label One directory, two exports: Label (htmlFor + children), thin Field (label + control + description/error slot)

Deferred to the #972 form layer: form_type mapping, diagnostics, immutable/ephemeral badges, JSON list(string) encoding, debounce, WebSocket session, IPC, panel provider.

Extraction-readiness (future standalone repo)

No @repo/* imports (lint-enforced), react peer dep, colocated stories/parity, --ui-*-only component CSS, public root exports as the boundary, per-component test files so tests move wholesale at extraction time.

Decisions

  1. Select engine — Radix react-select (established practice; native <select> + appearance: base-select considered and rejected for Storybook portability).
  2. VS Code-like rendering — no Switch/Slider/RadioGroup/MultiSelect/TagInput components; form_types map per the table above.
  3. Test placement — repo-root test/webview/ui/ per AGENTS.md; per-file layout keeps a future move into the package mechanical.
Review decisions and simplification log
  • Keep Field as explicit composition: optional description/error IDs, consumer-owned ARIA and validation. No form context, child cloning, new dependency, or monorepo runtime coupling.
  • Preserve all controls, exports, props (incl. showLabel/hideLabel), and Radix Select modes.
  • Fix password reveal overriding subsequent input types; associate Select option descriptions without replacing consumer descriptions.
  • Constrain long selected labels, restore native scrollbars rather than adding scroll-button parts, and draw forced-color outlines only on highlighted options.
  • Tests: per-component files kept; shared helpers.tsx removes the copy-pasted controlled-value and className/style tests. Overlapping Field, Input, and Select cases merged; the long-list keyboard test (disabled-option skipping, End, Enter, restored trigger focus) moved from the story into Select.test.tsx.
  • Stories: every expect removed from play; play remains only where it drives the state to be screenshotted (Select open/scrolled, Focused variants). Layout-only checks (trigger ellipsis, last option within the scrolled listbox) are covered by the LongList Pixel snapshot.
  • CSS: text-control.css rewritten against shared ui-text-control* classes so each rule is declared once; dead ui-checkbox__label class removed.
  • README: useId example replaced by a pointer to Field.stories.tsx.
  • Second pass (parity story review): the select value was clipped vertically against the @vscode-elements reference (13px line box vs 17px text) — fixed with a trigger-only line-height: normal. --ui-list-focus-outline was declared twice in :root and the tree declaration won, painting a focus-colored outline on highlighted select rows in regular themes — the duplicate is removed and Select uses --ui-list-selection-outline. Focus in stories was inconsistent (only Field focused a control); every form control now has an unfocused States story plus a Focused story.

Note

This PR was generated by Coder Agents on behalf of @EhabY.

Adds Input (text / number / password with reveal toggle), Textarea,
Checkbox, Select (Radix), and Field/Label to packages/ui for the #972
dynamic-parameter webview form. Controls render like VS Code's settings
editor; richer dashboard form_types map onto this vocabulary.

- Input/Textarea are controlled with value + onChange(next); Checkbox
  uses checked + onChange(next). Select preserves Radix controlled and
  uncontrolled modes with flat compound exports.
- Shared text-control.css paints Input, SearchInput, and Textarea from
  one set of ui-text-control rules.
- New --ui-checkbox-*, --ui-dropdown-*, and --ui-list-focus-* tokens
  with high-contrast fallbacks.
- Stories drive state for Pixel snapshots only; behavior is covered by
  per-component unit tests sharing test/webview/ui/helpers.tsx.
@EhabY
EhabY force-pushed the feat/ui-form-controls branch from a7149a2 to b17a086 Compare September 8, 2026 12:22
EhabY and others added 2 commits September 8, 2026 13:04
The select trigger inherits line-height 1 from .ui-control, so the
ellipsized value box (13px) is shorter than the rendered text (17px) and
clips it; the native dropdown draws an 18px line. Set line-height: normal
on the trigger only.

--ui-list-focus-outline was declared twice in :root; the tree-oriented
declaration won, so highlighted select rows got a focus-colored outline in
regular themes. Use the existing --ui-list-selection-outline, which is
transparent outside high contrast, and drop the duplicate.

Every form-control States story now renders unfocused, with a separate
Focused story that focuses the first control for the focus-border
snapshot.
Move the highlightable row chrome that Menu and Select both declared
into overlay.css, keyed by --ui-overlay-highlight-* the same way
.ui-overlay already maps its border. Split the password reveal toggle
out of Input into PasswordInput, and give Input a children slot for
trailing in-field actions.

Cut CSS that only restated shared classes or browser defaults: the
disabled modifier classes become :has(> :disabled), the Checkbox box
reuses .ui-control and renders its check icon only when checked, and
the no-op flex, user-select, and min-width declarations are gone.
Rename the select highlight tokens to --ui-dropdown-list-focus-* so
they don't read as list.focusBackground.

Fold the five form control test files and their helpers into one
forms.test.tsx with it.each tables, move the SearchInput tests there,
drop the Radix keyboard navigation test that chained waitFor on
internal timers, and move the jsdom pointer-capture shims into the
shared setup file.
@linear-code

linear-code Bot commented Sep 8, 2026

Copy link
Copy Markdown

DEVEX-925

Name each it.each row with a case interface and a $name title instead
of positional tuples, so a row reads as a record and the callback
destructures only what it uses.
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