Skip to main content

Input allowClear + visual cases (DES-126)

Version: 0.2.00.2.2 · Type: ✨ Feature

2026-06-17 Input clear button gets a focus-visible focus ring (a11y)

PR #2489 review feedback

Changed Files

  • src/components/Input/styles.ts
  • src/components/Input/__tests__/Input.test.tsx

Changes

  • INPUT_CLEAR_CLASS gains focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-(--Labels-Primary), aligning with the existing keyboard focus-ring convention of ui/input, ui/button, Calendar, and Tree (ring-1 + Labels/Primary), giving the keyboard-focused clear button a consistent visible indicator (WCAG 2.4.7).
  • Added a unit-test assertion that the clear button carries the focus-ring class.

Root Cause

The clear button is a focusable <button>; it previously relied only on the browser default focus ring and lacked a token-based focus ring consistent with the design system.


2026-06-17 Extract useClearableInput to reuse the Input / InputSearch clear logic

DES-126 (sub issue of DES-125 Input); all of this file's Input-family changes for the day (allowClear / visual case / semantic class / useClearableInput refactor) are attributed to this sub issue

Changed Files

  • src/components/Input/use-clearable-input.ts (new)
  • src/components/Input/Input.tsx
  • src/components/Input/InputSearch.tsx

Changes

  • Extracted the line-by-line duplicated clear logic in Input (allowClear) and InputSearch (controlled/uncontrolled value hosting, focus state, clear-button visibility, synthetic change event writing back the empty value, ref forwarding) into a shared hook useClearableInput, eliminating about 50 lines of duplication.
  • The hook adds an enabled switch: when Input only has prefix/suffix (no allowClear), it no longer hosts an internal value, avoiding an ineffective setInternalValue and re-render on every keystroke.
  • Pure structural refactor; the public API, render output, and visual snapshots are unchanged; all 86 unit tests of the Input family pass.

2026-06-17 Input adds allowClear ability + supplementary visual cases

Changed Files

  • src/components/Input/Input.tsx
  • src/components/Input/styles.ts
  • src/components/Input/InputDesignSpec.md
  • src/components/Input/__tests__/Input.test.tsx
  • src/components/Input/__tests__/Input.ct.spec.tsx
  • src/components/Input/__tests__/InputFixture.visual.tsx (new)
  • scripts/visual/visual-diff-config.ts
  • packages/design-site/docs/components/atomic/input.mdx

Changes

  • Input adds allowClear?: boolean and onClear?: () => void: when focused, with content, and not disabled, it renders an × clear button before the suffix; when blurred or empty, it hides it. Supports controlled / uncontrolled; clicking writes back the empty value via onChange + onClear and re-focuses.
  • The clear button uses onMouseDown preventDefault to keep the input from blurring first on click, which would otherwise make the button disappear.
  • Added INPUT_CLEAR_CLASS: a 28×28 clickable area (20px XmarkIcon + 4px padding), 5px border radius, icon Labels/Primary, light-gray background on hover.
  • When allowClear is set, rendering uniformly goes through the wrapper path (consistent with prefix/suffix); when there is no prefix/suffix/allowClear, the original pass-through path is kept unchanged.
  • Added visual cases: covering all 12 variant groups of the Figma Input component set (21028:552) (State × Filled × Status), comparing each variant 1:1 with its Figma node. Each case reproduces the Figma example content — InputFixture.visual.tsx inlines the Figma icon_user (node 21039:5202) as the prefix and HelpIcon as a 28×28 suffix button, with Focused+Filled additionally showing the allowClear clear button, and registers 12 node mappings in visual-diff-config.ts.
  • design-site adds a Clearable example and allowClear / onClear Props rows.
  • Fixed getInputWrapperClass (prefix/suffix path) where the error-state focus border was overridden by black: on error, the focus border now stays Status/Destructive (red), aligning with the Figma focused+error variant and the behavior of the slot-less path getInputTokenClass.

Root Cause

The Figma Input component set (node 21028:552) was updated; the Filled=True + Focused variants (21028:557 Default / 21069:7015 Error) added a Clear instance (icon_xmark_for_panel, 28×28). The design layer fills in the corresponding ability and aligns the visuals.


2026-06-17 InputSearch / InputPassword supplementary visual cases + Search clear changed to focus gating

Changed Files

  • src/components/Input/InputSearch.tsx
  • src/components/Input/__tests__/InputSearch.test.tsx
  • src/components/Input/__tests__/InputSearch.ct.spec.tsx (new)
  • src/components/Input/__tests__/InputPassword.ct.spec.tsx (new)
  • scripts/visual/visual-diff-config.ts
  • scripts/visual/generate-visual-gallery.ts
  • packages/design-site/docs/components/atomic/input.mdx

Changes

  • InputSearch clear button changed to focus gating: was "shown whenever there is content", changed to "shown when focused, with content, and not disabled", aligning with the updated Figma Search Field (Normal+Filled has no ×, Focused+Filled has ×) and matching the Input behavior. Added onMouseDown preventDefault to prevent blur on click; the clear icon was switched from CloseIconPlaceholder to the contract icon XmarkIcon, unified with the Input clear button.
  • Added InputSearch visual cases: covering all 16 variant groups of the Search Field component set (277:24750) (Size × Style × Filled × State), comparing each variant 1:1 with Figma.
  • Added InputPassword visual cases: covering all 16 variant groups of the Password component set (21054:763) (State × Filled × Visible × Status). Masked renders dots + eye-close; Visible, after a click toggle, renders plaintext + eye-open; Normal+Visible restores after blurring following a toggle.
  • visual-diff-config.ts registers 16 node mappings each for InputSearch / InputPassword; added an optional screenshotDir field so that sub-component screenshots are placed under the Input directory, and generate-visual-gallery.ts resolves the screenshot path accordingly.
  • The design-site Search "Controlled + Clear" example description was updated to "shown when focused and with content".

Root Cause

Filling in the visual coverage of the Input family (Search / Password); aligning with the updated Figma Search Field clear-button focus-gating behavior (the user confirmed adopting the align-with-Figma approach).


2026-06-17 Input family adds semantic class identifiers (BEM)

Changed Files

  • src/components/Input/Input.tsx, styles.ts
  • src/components/Input/InputSearch.tsx (via input-search-styles.ts), input-search-styles.ts
  • src/components/Input/InputPassword.tsx, input-password-styles.ts
  • src/components/Input/__tests__/Input.test.tsx, InputSearch.test.tsx, InputPassword.test.tsx

Changes

  • Added semantic classes to all DOM nodes of the Input family (plaud-<component> block + plaud-<component>__<element> child elements, placed at the head of the class string, following the existing Toast/Skeleton convention), for test selectors and style targeting, to avoid nodes that have only Tailwind utility classes:
    • Input: plaud-input (bare input / affix container), plaud-input__prefix / __content / __field / __clear / __suffix
    • InputSearch: plaud-input-search (container), plaud-input-search__icon / __field / __clear
    • InputPassword: plaud-input-password (root), plaud-input-password__toggle
  • Each of the three components adds semantic-class assertions to lock them in.
  • The semantic classes carry no CSS rules; rendering and visual snapshots are unchanged.

2026-06-17 Clear synthetic event switched to native input dispatch (fixes PR review finding)

Related: PR #2489 review finding (use-clearable-input.ts:99)

Problem

The synthetic ChangeEvent that handleClear built via Object.create only contained target / currentTarget and lacked standard SyntheticEvent fields such as nativeEvent / type / bubbles / preventDefault. Consumers (React Hook Form / Formik, etc.) reading e.nativeEvent would get undefined and fail silently; e.type would read the input's type attribute value (such as 'text') instead of the event type 'change'.

Changes

  • Switched to using the native value setter to write the empty value and dispatch a real input event, letting the React synthetic-event pipeline run normally:
    Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(input, '');
    input.dispatchEvent(new Event('input', { bubbles: true }));
    Consumers now receive a standard SyntheticEvent with complete fields; both controlled and uncontrolled modes receive the empty value via onChange (handleChange), with no need to manually maintain internal state (removed setInternalValue('') and the manual onChange construction).
  • The two controlled-mode unit tests were changed to read e.target.value synchronously inside onChange: the event.target of a real event is the live DOM node, and in controlled mode when the parent does not write back the value, React restores it to the original value after the event, so the original assertion's asynchronous read would get the restored value; the synchronous read matches the timing of real consumers and faithfully reflects the controlled behavior.

Root Cause

The original synthetic event was a minimal "good-enough" implementation that did not conform to the React SyntheticEvent contract, posing a silent-failure risk for form libraries that depend on nativeEvent / type.


2026-06-17 Input family review finding second-round fixes (focus-visible / focus-state re-render / aria-label localization)

Related: PR #2489 review findings

Changed Files

  • src/components/Input/input-search-styles.ts
  • src/components/Input/Input.tsx, InputSearch.tsx
  • src/components/Input/__tests__/Input.test.tsx, InputSearch.test.tsx
  • packages/design-site/docs/components/atomic/input.mdx (including zh-CN)

Changes

  • [critical] InputSearch clear button gets a focus-visible focus ring: INPUT_SEARCH_CLEAR_CLASS had not yet been synced with the focus ring added for Input in commit 52389cdc6, so tabbing to the clear button via keyboard had no visible indicator (violating WCAG 2.4.7). Added focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-(--Labels-Primary) consistent with Input; added a focus-ring unit test to lock it in.
  • [perf] Input no longer hosts focus state when there is no allowClear: in affix mode (only prefix/suffix, allowClear=false), onFocus/onBlur/onChange previously always bound the hook-version handlers, so every focus/blur called setFocused and triggered an ineffective re-render (showClear always false). Changed to use the hook-version handlers only when allowClear === true, otherwise passing the original callbacks straight through, eliminating the performance regression relative to the old code.
  • [convention] Clear-button aria-label supports localization: Input / InputSearch add clearButtonAriaLabel?: string, defaulting to '清空输入' / '清空搜索' respectively, which multilingual consumers can override; added unit tests for the default and custom values, and registered the prop in the design-site Props table (both English and Chinese).

Root Cause

The first round of the allowClear implementation missed syncing the InputSearch focus ring, introduced an ineffective re-render from hosting focus state in the affix scenario, and hard-coded a Chinese aria-label at the component-library layer, which is unfriendly to multilingual consumers.