LeCodesdocs

UIButton, UIInput & UITextArea

The three elements that take user input. UIButton is the only tappable container — touch handlers exist solely on UIButton, UIScreen, and UIWidget (see Pointer events); to make anything clickable (a card, a list row, an icon), wrap it in a UIButton. UIInput is a single-line text field, UITextArea its multi-line sibling.

At a glance

TypeScript
let input: UIInput

const form = UIColumn(
  input = UIInput()
    .style({ height: 40, px: 12, borderRadius: 8, bgColor: "white", color: "black",
             placeholder: "Your name", placeholderColor: "#999" })
    .onChange(v => console.log("typing:", v)),
  UIButton(UIText("Submit").style({ color: "white" }))
    .style({ height: 40, justifyContent: "center", bgColor: "#FF4032", borderRadius: 8,
             onPressed: { opacity: 0.7 }, rippleColor: "default" })
    .onClick(() => console.log("submitted:", input.value)),
).style({ gap: 8 })

UIButton

TypeScript
UIButton(...children: (UINodeChild | UINodeChild[])[]): UIButton

A container that defaults to flexDirection: "row" with children centered on both axes (justifyContent + alignItems "center") — the icon+label button lays out correctly with no styling. Full container surface: .append / .insert / .remove / .setContent, .style, .animateTo / .animateFrom, .onLayout. Use flexDirection: "column" for a card-shaped button.

TypeScript
button.onClick(ev => …)        // pointer-up over the button; ev: ClickEvent
button.onTouchStart(ev => …)   // pointer-down; ev.track({...}) starts a drag gesture
button.onLongPress(ev => …)    // finger held ~0.5s; ev.track({...}) to drag; ev: LongPressEvent
button.isPressed(): boolean    // true while a finger is currently down on the button

onClick / onTouchStart / onLongPress events carry clientX / clientY / pointerId (logical px). Drags — ev.track(), claim — work exactly as described in Pointer events.

onLongPress fires when the finger is held on the button past the long-press threshold (~0.5s) without sliding away. A handled long-press swallows the click that would otherwise follow the release, so onClick and onLongPress coexist cleanly. Like onTouchStart, the event can ev.track({...}) — so "press-and-hold, then drag to move" is a single handler:

TypeScript
button.onLongPress(ev => {
  device.vibrate("medium")                 // confirm the hold registered
  ev.track({
    claim: true,
    onMove: ({ deltaX, deltaY }) => moveElement(deltaX, deltaY),
    onEnd:  () => commit(),
  })
})
Note

buttons render no chrome of their own — style them like any container (bgColor, borderRadius, padding). Give a button an explicit height: on its own it is only as tall as its text.

Press feedback — opt-in

TypeScript
button.style({
  onPressed: { opacity: 0.7, bgColor: "#c22", duration: 150 },  // style while pressed
  rippleColor: "default",                                       // Android ripple; or any Color
})

Nothing happens visually on press unless you ask. onPressed accepts drawable/base styles (bgColor, opacity, border*, transform, …) applied while the finger is down, plus an optional transition duration (ms). rippleColor is Android-only and replaces the onPressed visual there — so setting both gives a ripple on Android and the onPressed style on iOS.

Prefer the reserved class $pressed in new code: same feedback on the button itself, and it cascades — children (icon, label) can declare their own $pressed blocks and react to the button's press. onPressed stays the strictly per-element form (it never fires from an ancestor's press — right for nested interactives) and wins over $pressed when both are declared. See Style classes.

For a persistent visual state you toggle yourself (selected, checked, active) rather than one the system drives, declare a style class ($name) and flip it via el.class.name = … — see Style classes. Press feedback always wins over a class while pressed.

UIInput & UITextArea

TypeScript
UIInput(): UIInput          // single-line field
UITextArea(): UITextArea    // multi-line
TypeScript
input.value                          // get/set the current text (a content property, not a style)
input.onChange(cb: (value: string) => void): this   // every edit
input.onFocus(cb: () => void): this
input.onBlur(cb: () => void): this
input.onSubmit(cb: (value: string) => void): this   // return key pressed (UIInput only)
input.focus()                        // focus programmatically (opens the keyboard); no-op before mount
input.blur()                         // release focus (dismisses the keyboard)

Pre-filling a field

value is writable before the screen opens — that's how you build a settings screen or an edit form: fill the fields while you compose the tree, then open it.

TypeScript
const name = UIInput().style({ placeholder: "Name" })
const bio = UITextArea().style({ maxHeight: 120 })

name.value = user.name           // set before open — the field comes up filled
bio.value = user.bio             // a textarea comes up already grown to fit

UIScreen(UIColumn(name, bio, UIButton("Save").onClick(() => save(name.value, bio.value)))).open()

Reading back gives you the live text while the screen is open, and the last value you set while it isn't. What the user typed is not carried across a close/open — a re-opened screen is rebuilt, so it comes up with the values your code set. Use screen.keepAlive() when a half-filled form must survive navigating away.

Input-specific styles, on top of the usual element + text styles (fontSize, color, fontFamily, …):

TypeScript
input.style({
  placeholder: "Search…",
  placeholderColor: "#999",
  type: "search",         // "text" (default) | "password" | "search" | "phone" | "email"
                          //   | "number" | "decimal" | "url" | "date" | "time"
  enterKey: "search",     // return-key label: "done" | "go" | "next" | "search" | "send" (UIInput only)
  maxLength: 32,          // hard cap, enforced by the host while typing/pasting
  autocapitalize: "none", // "none" | "words" | "sentences" | "characters"; omitted = platform default
  autocorrect: false,     // platform autocorrection/suggestions; omitted = platform default
  onFocused: { borderColor: "#FF4032", duration: 150 },   // style while focused (like onPressed;
                                                          // cascading form: $focused)
})
Warning

type values are keyboard hints, not validatorsnumber shows the number pad but does not block pasted letters; validate in onChange yourself. The phone-pad value is phone (LeCodes has no HTML-style tel).

Note

give inputs an explicit height and a flexGrow: 1 (in a row) or width — an input collapses to its placeholder's width and its text's height, it does not stretch like a web <input>.

Date & time pickers (type: "date" | "time")

date and time are picker kinds, not keyboards: focusing the field opens the platform's native picker in the keyboard slot (iOS wheels, browser calendar) and free typing is disabled — the value can only come from the picker or from code. The field itself stays a normal, fully styleable input.

  • input.value is always canonical: "YYYY-MM-DD" for date, "HH:MM" (24-hour) for time — that's what onChange receives and what you set programmatically. What the user sees in the field is a localized string the host formats; never parse the display.
  • An empty value shows the placeholder. On iOS, confirming with the Done bar adopts the wheels' current position even if the user didn't spin them (open → Done = today); tapping dead space just dismisses without adopting.
  • All the usual machinery applies unchanged: onFocused styling, focus()/blur(), keyboardShrink, the dismissal doctrine.
TypeScript
const birthday = UIInput().style({ type: "date", placeholder: "Date of birth" })
birthday.onChange((v) => console.log(v))   // "1990-04-27"
birthday.value = "1990-04-27"              // canonical in, localized display out

Auto-grow (UITextArea)

A UITextArea with no explicit height measures its own content and grows line by line as the user types. Clamp the range with minHeight / maxHeight — past maxHeight the text scrolls inside the field. A fixed height opts out (internal scrolling from the start). This is the chat-composer recipe:

TypeScript
const composer = UITextArea().style({
  placeholder: "Message", fontSize: 16,
  maxHeight: 108,          // ~5 lines, then it scrolls inside
  keyboardDismiss: false,  // chat: tapping the transcript doesn't hide the keyboard
  // no height — grows with the content; width comes from the wrapper's stretch
})
UIRow(
  UIColumn(composer).style({ flexGrow: 1, flexShrink: 1, flexBase: 0, px: 14, py: 11, borderRadius: 22 }),
  sendButton,
).style({ alignItems: "flex-end", gap: 10 })   // the send button stays anchored while it grows

Don't give the textarea itself flexGrow/flexBase — a flex basis overrides the intrinsic content height and the field stops growing.

Submit & the form flow

onSubmit fires when the user presses the keyboard's return key (UIInput only — in a UITextArea Enter is a newline, period). On submit the keyboard dismisses, unless enterKey: "next" — then you are expected to move focus yourself, and the keyboard stays up:

TypeScript
const name = UIInput().style({ enterKey: "next" }).onSubmit(() => email.focus())
const email = UIInput().style({ type: "email", enterKey: "done" })
  .onSubmit(() => submitForm())

focus() also covers focus-on-open search screens and focus-the-invalid-field validation.

Keyboard policy: keyboardShrink

TypeScript
input.style({ keyboardShrink: false })   // keyboard overlays the UI — no relayout

What the layout does while the keyboard is up is decided by the focused input. true (default): the layout viewport shrinks to the area above the keyboard (one relayout). false: the keyboard overlays the UI with no layout recalculation — for chat composers that manage their own inset and full-screen canvases where a reflow is worse than overlap. In overlay mode, read app.keyboardHeight / the "keyboard" event to make room yourself:

TypeScript
// chat composer: overlay mode + own inset — pb rides the keyboard
input.style({ keyboardShrink: false })
app.addEventListener("keyboard", (height) => {
  composerRow.style.pb = Math.max(24, height)    // 24 ≈ the safe-area bottom
})

Everything else about the keyboard is the host's job: it scrolls the focused input into view inside its nearest scroll container, and dismissal follows one universal doctrine:

  • A tap on a control never dismisses. Tapping a UIButton fires it with the keyboard still up; tapping another input just moves focus. (A send button next to a composer works without the keyboard blinking — call input.blur() from a handler when you do want to dismiss.)
  • Scrolling never dismisses — unless the scrollable opts in. So a suggestion dropdown or autocomplete list under a focused input can be scrolled and its options tapped with the keyboard up — no special casing. On iOS a downward drag that reaches the keyboard itself dismisses it interactively (the iMessage gesture); ordinary scrolling doesn't. A scrollable can change its own policy with the keyboardDismissMode style (containers.md): "scroll" makes any drag in it dismiss the moment it starts — the right feel for a search-results list — and "none" turns even the interactive drag off.
  • A tap on a non-interactive area dismisses — background, text, images. This is the one part an input can opt out of with keyboardDismiss: false (chat composers — then taps never dismiss and only blur(), the return key, and the iOS drag-down gesture remain).
  • The return key dismisses per enterKey (except "next"), and iOS shows a Done accessory bar on the return-key-less pads (number / decimal / phone). input.blur() stays the programmatic path.

An app normally ships no keyboard code beyond enterKey: "next"next.focus() chaining — plus keyboardDismiss: false on a chat composer.

Pitfalls

TypeScript
// ✗ touch handler on a plain container
UIColumn(...).onTouchStart(cb)              // no such method — only UIButton/UIScreen/UIWidget
// ✓ wrap in a UIButton
UIButton(...).onTouchStart(cb)

// ✗ button next to a 40px input shrinks to its text height
UIRow(input.style({ height: 40 }), UIButton(icon))
// ✓ give controls the same explicit height
UIRow(input.style({ height: 40 }), UIButton(icon).style({ height: 40 }))

See also