Skip to content

feat!: add BorderedTextField; migrate fields to ControlPresentation#1039

Draft
pawelgrimm wants to merge 18 commits into
pawel/input-presentation/extract-from-text-fieldfrom
pawel/refactor/integrate-control-resentation
Draft

feat!: add BorderedTextField; migrate fields to ControlPresentation#1039
pawelgrimm wants to merge 18 commits into
pawel/input-presentation/extract-from-text-fieldfrom
pawel/refactor/integrate-control-resentation

Conversation

@pawelgrimm
Copy link
Copy Markdown
Contributor

Short description

Follows up on #1037. Migrates the existing field components to compose ControlPresentation and introduces a new public sibling, BorderedTextField, that replaces the outlined-label-inside layout previously offered by <TextField variant="bordered">.

New public component

  • BorderedTextField — outlined text-field layout: the label sits inside a rounded chrome above the input; optional endSlot rendered as a full-height side column. Composes BaseField + OutlinedControlContainer (the private chrome primitive added in feat: add ControlPresentation primitive #1037). Sibling to TextField, not a variant of it.

Field migrations

  • TextField drops variant and endSlotPosition. Inline wrapper JSX is replaced with <ControlPresentation>. Local handleClick and useMergeRefs are removed (CP/OCC handle click-to-focus).
  • SelectField drops variant. The absolutely-positioned chevron moves into CP's endSlot. selectWrapper chrome CSS goes away — CP owns it.
  • TextArea drops variant. Wraps the <textarea> in OutlinedControlContainer directly (not CP — multi-line layout doesn't fit CP's fixed 32px single-row chrome). Auto-expand grid trick stays scoped to TextArea.
  • PasswordField unchanged in source but inherits the TextField refactor automatically (no more variant).

BaseField scoped down

  • Removes variant, BaseFieldVariantProps, the supportsStartAndEndSlots discriminated union, endSlot, endSlotPosition, and the bordered outer container rendering.
  • After this PR, BaseField is pure form-field scaffolding: id generation, ARIA wiring (aria-describedby / aria-invalid), optional label, character count state machine, message rendering, Stack layout, maxWidth / hidden.

Breaking changes

feat!: — major version bump. Migration is mechanical:

  • <TextField variant="bordered" .../><BorderedTextField .../>. Drop startSlot, endSlotPosition, and characterCountPosition="inline" if used — none of these are supported in the outlined layout. If you need a leading icon, use TextField (default). If you need a side action, use BorderedTextField's endSlot (full-height).
  • <TextField variant="default" .../> → drop the variant prop (no-op rename — default was always the default).
  • <SelectField variant="bordered" .../> → drop the variant. No BorderedSelectField is shipped (no demand surfaced).
  • <TextArea variant="bordered" .../> → drop the variant. No BorderedTextArea is shipped.
  • Custom components using BaseField directly: drop variant, supportsStartAndEndSlots, endSlot, endSlotPosition if used. Render slot rows via ControlPresentation instead.

What's not here

  • Anything outside the field family. CheckboxField / SwitchField are unrelated chrome and are not touched.

PR Checklist

  • Added tests for bugs / new features (BorderedTextField has its own suite; existing TextField/SelectField/TextArea tests pruned of removed behaviors)
  • Updated docs (BorderedTextField stories added; TextField/SelectField/TextArea stories updated to remove bordered examples and endSlotPosition)
  • Reviewed and approved Chromatic visual regression tests in CI

pawelgrimm and others added 18 commits May 15, 2026 09:59
A presentational layout shell for input-like controls. Provides border
chrome, focus/hover/disabled/readonly/invalid styling, and two slots
(startSlot, endSlot) flanking an arbitrary focusable control passed as
children. Forwards a ref to the wrapper and forwards click events to the
inner control (toggleable via forwardClickToControl).

State-driven styling fires from :has() selectors that match three
signaling conventions — native HTML, ARIA, and data-attr — on the inner
control. The single source of truth is the a11y attributes the control
needs anyway. :invalid is deliberately omitted (fires pre-interaction).

Spacing is explicit conditional padding: 10px outer on each side by
default; left shrinks to 6px when startSlot is present (with 6px gap to
control); right shrinks to 4px when endSlot is present (with 6px gap).

ControlActionButton ships alongside as a compact 24x24 button variant
sized to fit the chrome alongside a 16px icon glyph.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Cover the public API surface (slot rendering, click-forwarding, ref
forwarding, attribute passthrough, exceptionallySetClassName) plus each
state-styling signaling convention (native HTML, ARIA, data-attr) and
the slot-marker classes that drive the conditional outer padding.
jest-axe smoke test for accessibility.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace margin-inline-{end,start} on .startSlot / .endSlot with a single
gap: 6px declaration on the .container flex wrapper. The rendered
geometry is identical (flex gap only applies between rendered children,
so absent slots still produce no gap), but the spacing rule lives in one
place and can't drift between the two sides.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
When the wrapper background is clicked, handleWrapperClick synthesizes
control.click() to activate the inner control. That synthetic click
bubbles back up to the wrapper and re-enters the same handler, which
called onClick a second time. Track the dispatched re-entry via a ref
and short-circuit it so consumer onClick fires exactly once per user
gesture.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
30 → 12 tests. The pruned tests were either (a) asserting React's own
attribute-passthrough behavior (the entire state-styling describe block —
JSDOM doesn't evaluate the :has() CSS that gives those attributes
visual meaning), (b) locking in implementation details (role attributes,
CSS-module class names on slot wrappers), or (c) duplicating other
tests' coverage.

Visual concerns (conditional padding, :has()-driven state styling) stay
verified by Storybook/Chromatic — Jest is the wrong tool for CSS
behavior.

Added three small prop-guardrail tests so every documented prop has at
least one direct test: forwardClickToControl, onClick, ref forwarding.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tion props

Click handlers belong on the inner control, not on the wrapper. The
wrapper's click-to-focus behavior is internal — there's no real need
for consumers to disable it (forwardClickToControl) or hook into it
(onClick) for the wrap pattern.

The implementation still extracts onClick out of rest (via a type cast)
to compose with the focus-forwarding handler — this path exists only for
render-prop use, where Ariakit (or similar) forwards its own onClick to
the wrapper-as-trigger. Consumers of ControlPresentation directly won't
see onClick in autocomplete; TypeScript rejects passing it at call
sites.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace `Omit<ComponentProps<typeof Box>, 'className'>` on FCC with
explicit `onClick` + `ObfuscatedClassName`. Move `display:flex`,
`align-items:center`, `overflow:hidden` into FCC's CSS. CP keeps its
loose public type (Box props) but only forwards `onClick` to FCC;
other Box props (maxWidth, etc.) are silently dropped pending a
follow-up CP API tightening.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ghten ControlPresentation type

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Remove variant, BaseFieldVariantProps, supportsStartAndEndSlots, endSlot,
endSlotPosition, and bordered chrome from BaseField; strip .bordered.*
CSS rules; update all callers (password-field, bordered-text-field,
select-field, text-field).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@pawelgrimm pawelgrimm force-pushed the pawel/input-presentation/extract-from-text-field branch 2 times, most recently from 7687126 to 14f7734 Compare May 29, 2026 02:01
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