Skip to content

input — Input

The input is the cornerstone of forms. core/input handles four things: display and computation of word counts, truncation of overlong input, inline styles per size tier, and the accessibility attribute bag for inputs. It deliberately does not touch validation (that is core/form’s job) or debouncing (that is the UI layer’s reactivity concern).

  • Each port implements LhInput (including the textarea variant: the --textarea modifier class).
  • You need to reuse the same word-count and aria attributes on a custom input control.
  • Should not use it: numeric input uses core/input-number (has stepping and clamping), time uses core/time-picker, dates use core/date-picker.
import {
getWordCount,
shouldShowWordLimit,
clampString,
getInputStyle,
getInputAria,
} from '@lh-ui/share/core/input'
import type { LhInputState } from '@lh-ui/share/core/input'

Both edges of getWordCount come from real cross-port differences:

  • When value is undefined, the count falls back to 0 — React/uniapp controlled values may briefly be undefined before mount, and undefined.length would blow up outright.
  • When maxlength is undefined, the limit shows ∞ instead of 'undefined'. This symbol makes “no limit set” visually explicit rather than looking like a bug.

getInputStyle’s size styles are inline rather than class names: lh-ui inputs support global sizing via ConfigProvider, and switching the global size should not require recomputing classes — inline styles simply recompute from props. Note that the size mapping here differs from buttons (buttons use classes); this is the result of each component’s historical evolution.

Function Signature Returns Purpose
getWordCount (state: Pick<LhInputState, 'value' | 'maxlength'>) => string "used/limit" Word-count string
shouldShowWordLimit (state: Pick<LhInputState, 'maxlength' | 'minlength'>) => boolean boolean Show the count when maxlength or minlength is set
clampString (value: string, maxlength?: number) => string string Truncate overlong input
getInputStyle (size: string) => Record<string, string> inline styles Map padding/font-size per size tier
getInputAria (options: { label?: string; describedby?: string; invalid?: boolean; required?: boolean }) => LhAriaAttrs aria attribute bag Emits four attributes on demand

getInputStyle tier mapping: mini padding 4px 8px / font-size 12, small 6px 10px / 12, medium 8px 12px / 14, large 12px 16px / 16; unknown sizes fall back to medium.

export interface LhInputState {
value: string
size: LhComponentSize
maxlength: number | undefined
minlength: number | undefined
}

The four attributes of getInputAria are all mounted on demand (emitted when truthy, key absent when falsy):

getInputAria({ label: 'Username', required: true })
// { 'aria-label': 'Username', 'aria-required': true }
getInputAria({ invalid: true, describedby: 'lh-form-error-username' })
// { 'aria-invalid': true, 'aria-describedby': 'lh-form-error-username' }

The integration with core/form is the key usage: LhFormItem renders the error message element (the id is generated by getFormFieldErrorId(prop)) and passes that id in via describedby, so screen readers announce “invalid input, the error is…”. See form.

import { getWordCount, clampString, getInputAria } from '@lh-ui/share/core/input'
getWordCount({ value: 'hello', maxlength: 10 }) // '5/10'
getWordCount({ value: 'hello', maxlength: undefined }) // '5/∞'
getWordCount({ value: undefined, maxlength: 10 }) // '0/10'
clampString('abcdef', 3) // 'abc'
clampString('abcdef') // 'abcdef' (no limit, returned as-is)
getInputAria({ label: 'Search' }) // { 'aria-label': 'Search' }
<!-- Vue3 -->
<div :class="['lh-input', `lh-input--${size}`, { 'lh-input--textarea': type === 'textarea' }]">
<div class="lh-input__wrapper">
<input
class="lh-input__inner"
:style="getInputStyle(size)"
v-bind="getInputAria({ label, describedby, invalid: !!error, required })"
/>
</div>
<span v-if="shouldShowWordLimit({ maxlength, minlength })" class="lh-input__count">
{{ getWordCount({ value, maxlength }) }}
</span>
</div>
// React
const aria = getInputAria({ label, describedby, invalid: !!error, required })
return (
<div className={`lh-input lh-input--${size}`}>
<div className="lh-input__wrapper">
<input className="lh-input__inner" style={getInputStyle(size)} {...aria} />
</div>
</div>
)
// uniapp: <input> is a built-in component; both class and style bindings are supported;
// textarea uses the <textarea> component, and on non-h5 targets word counting is handled
// by the component's built-in count attribute
Class Semantics
.lh-input Base class
.lh-input--{mini|small|medium|large} Size tiers
.lh-input--textarea textarea variant
.lh-input--disabled / .lh-input--prefix / .lh-input--suffix States and slots
.lh-input__wrapper Wrapper (border, focus state)
.lh-input__wrapper--focused / --disabled / --has-label Wrapper states
.lh-input__inner Native input element
.lh-input__label / --float Floating label (material style)
.lh-input__prefix / __suffix Prefix/suffix slots (icons, buttons)
.lh-input__clear / __count Clear button / word count

Focus border color uses var(--lh-primary-color), error state uses var(--lh-danger-color).

  • Asymmetric padding of --has-label: with a floating label, padding-top is 16px (to leave room for the label); other tiers are symmetric. This is a pending-unification item recorded in the “control-height tokens” initiative.
  • Inconsistent control heights: input 37px / select 32px / cascader 27→32px, etc. The root cause is that var.scss lacks --lh-control-height-* tokens (same as above, deferred to a later changeset).
  • The semantics of “characters” in maxlength: clampString uses slice, counting UTF-16 code units; emoji in the supplementary planes count as 2. If the UI layer needs code-point counting, handle it yourself.
  • Do not overwrite sx with the return value of getInputStyle — sx has higher precedence; the two should be merged, not overwrite each other.
@lh-ui/share/core/input → getWordCount( )
返回值
(填好参数后点运行)

Copyright © 2024-2026 FengMo WenXue