Input allowClear + visual cases (DES-126)
Version: 0.2.0–0.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.tssrc/components/Input/__tests__/Input.test.tsx
Changes
INPUT_CLEAR_CLASSgainsfocus-visible:outline-none focus-visible:ring-1 focus-visible:ring-(--Labels-Primary), aligning with the existing keyboard focus-ring convention ofui/input,ui/button,Calendar, andTree(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.tsxsrc/components/Input/InputSearch.tsx
Changes
- Extracted the line-by-line duplicated clear logic in
Input(allowClear) andInputSearch(controlled/uncontrolled value hosting, focus state, clear-button visibility, synthetic change event writing back the empty value, ref forwarding) into a shared hookuseClearableInput, eliminating about 50 lines of duplication. - The hook adds an
enabledswitch: whenInputonly hasprefix/suffix(no allowClear), it no longer hosts an internal value, avoiding an ineffectivesetInternalValueand 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.tsxsrc/components/Input/styles.tssrc/components/Input/InputDesignSpec.mdsrc/components/Input/__tests__/Input.test.tsxsrc/components/Input/__tests__/Input.ct.spec.tsxsrc/components/Input/__tests__/InputFixture.visual.tsx(new)scripts/visual/visual-diff-config.tspackages/design-site/docs/components/atomic/input.mdx
Changes
InputaddsallowClear?: booleanandonClear?: () => 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 viaonChange+onClearand re-focuses.- The clear button uses
onMouseDownpreventDefault 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 (20pxXmarkIcon+ 4px padding), 5px border radius, iconLabels/Primary, light-gray background on hover. - When
allowClearis 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.tsxinlines the Figmaicon_user(node21039:5202) as the prefix andHelpIconas a 28×28 suffix button, with Focused+Filled additionally showing the allowClear clear button, and registers 12 node mappings invisual-diff-config.ts. - design-site adds a Clearable example and
allowClear/onClearProps rows. - Fixed
getInputWrapperClass(prefix/suffix path) where the error-state focus border was overridden by black: on error, the focus border now staysStatus/Destructive(red), aligning with the Figma focused+error variant and the behavior of the slot-less pathgetInputTokenClass.
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.tsxsrc/components/Input/__tests__/InputSearch.test.tsxsrc/components/Input/__tests__/InputSearch.ct.spec.tsx(new)src/components/Input/__tests__/InputPassword.ct.spec.tsx(new)scripts/visual/visual-diff-config.tsscripts/visual/generate-visual-gallery.tspackages/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
onMouseDownpreventDefault to prevent blur on click; the clear icon was switched fromCloseIconPlaceholderto the contract iconXmarkIcon, 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.tsregisters 16 node mappings each for InputSearch / InputPassword; added an optionalscreenshotDirfield so that sub-component screenshots are placed under theInputdirectory, andgenerate-visual-gallery.tsresolves 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.tssrc/components/Input/InputSearch.tsx(viainput-search-styles.ts),input-search-styles.tssrc/components/Input/InputPassword.tsx,input-password-styles.tssrc/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
- Input:
- 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
inputevent, letting the React synthetic-event pipeline run normally:Consumers now receive a standard SyntheticEvent with complete fields; both controlled and uncontrolled modes receive the empty value viaObject.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(input, '');input.dispatchEvent(new Event('input', { bubbles: true }));onChange(handleChange), with no need to manually maintain internal state (removedsetInternalValue('')and the manualonChangeconstruction). - The two controlled-mode unit tests were changed to read
e.target.valuesynchronously insideonChange: theevent.targetof 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.tssrc/components/Input/Input.tsx,InputSearch.tsxsrc/components/Input/__tests__/Input.test.tsx,InputSearch.test.tsxpackages/design-site/docs/components/atomic/input.mdx(including zh-CN)
Changes
- [critical] InputSearch clear button gets a focus-visible focus ring:
INPUT_SEARCH_CLEAR_CLASShad not yet been synced with the focus ring added for Input in commit52389cdc6, so tabbing to the clear button via keyboard had no visible indicator (violating WCAG 2.4.7). Addedfocus-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/onChangepreviously always bound the hook-version handlers, so every focus/blur calledsetFocusedand triggered an ineffective re-render (showClearalways false). Changed to use the hook-version handlers only whenallowClear === 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.