LeCodesdocs

UIButton, UIInput и UITextArea

Три элемента, принимающие ввод пользователя. UIButton — единственный тапабельный контейнер — обработчики касаний существуют только на UIButton, UIScreen и UIWidget (см. События указателя); чтобы сделать что-либо кликабельным (карточку, строку списка, иконку), оберни это в UIButton. UIInput — однострочное текстовое поле, UITextArea — его многострочный собрат.

Обзор

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

Контейнер с дефолтом flexDirection: "row" и детьми, центрированными по обеим осям (justifyContent + alignItems "center") — кнопка «иконка+подпись» раскладывается правильно без стилизации. Полная поверхность контейнера: .append / .insert / .remove / .setContent, .style, .animateTo / .animateFrom, .onLayout. Используй flexDirection: "column" для кнопки в форме карточки.

TypeScript
button.onClick(ev => …)        // отпускание над кнопкой; ev: ClickEvent
button.onTouchStart(ev => …)   // нажатие; ev.track({...}) начинает жест перетаскивания
button.onLongPress(ev => …)    // палец удержан ~0.5 с; ev.track({...}) для перетаскивания; ev: LongPressEvent
button.isPressed(): boolean    // true, пока палец сейчас на кнопке

События onClick / onTouchStart / onLongPress несут clientX / clientY / pointerId (логические px). Перетаскивания — ev.track(), claim — работают ровно как описано в События указателя.

onLongPress срабатывает, когда палец удерживается на кнопке дольше порога долгого нажатия (~0.5 с), не соскальзывая. Обработанное долгое нажатие проглатывает клик, который иначе последовал бы за отпусканием, — так что onClick и onLongPress чисто сосуществуют. Как и у onTouchStart, событие может ev.track({...}) — так что «зажми, удерживай, затем тащи» — это один обработчик:

TypeScript
button.onLongPress(ev => {
  device.vibrate("medium")                 // подтверждаем, что удержание засчитано
  ev.track({
    claim: true,
    onMove: ({ deltaX, deltaY }) => moveElement(deltaX, deltaY),
    onEnd:  () => commit(),
  })
})
Note

кнопки не рисуют собственного оформления — стилизуй их как любой контейнер (bgColor, borderRadius, отступы). Дай кнопке явный height: сама по себе она высотой только со свой текст.

Отклик на нажатие — по желанию

TypeScript
button.style({
  onPressed: { opacity: 0.7, bgColor: "#c22", duration: 150 },  // стиль во время нажатия
  rippleColor: "default",                                       // Android-ripple; или любой Color
})

Визуально при нажатии ничего не происходит, пока не попросишь. onPressed принимает рисуемые/базовые стили (bgColor, opacity, border*, transform, …), применяемые, пока палец внизу, плюс необязательный переход duration (мс). rippleColorтолько Android и заменяет там визуал onPressed — так что установка обоих даёт ripple на Android и стиль onPressed на iOS.

В новом коде предпочитай зарезервированный класс $pressed: тот же отклик на самой кнопке, плюс он каскадирует — дети (иконка, подпись) могут объявить собственные блоки $pressed и реагировать на нажатие кнопки. onPressed остаётся строго поэлементной формой (он никогда не срабатывает от нажатия предка — то, что нужно для вложенных интерактивов) и побеждает $pressed, когда объявлены оба. См. Классы стилей.

Для стойкого визуального состояния, которое переключаешь сам (выбрано, отмечено, активно), а не управляемого системой, объяви класс стиля ($name) и переключай его через el.class.name = … — см. Классы стилей. Отклик на нажатие всегда бьёт класс, пока нажато.

UIInput и UITextArea

TypeScript
UIInput(): UIInput          // однострочное поле
UITextArea(): UITextArea    // многострочное
TypeScript
input.value                          // get/set текущего текста (свойство контента, не стиль)
input.onChange(cb: (value: string) => void): this   // каждое изменение
input.onFocus(cb: () => void): this
input.onBlur(cb: () => void): this
input.onSubmit(cb: (value: string) => void): this   // нажата клавиша return (только UIInput)
input.focus()                        // программный фокус (открывает клавиатуру); no-op до монтирования
input.blur()                         // снять фокус (прячет клавиатуру)

Предзаполнение поля

value можно писать до открытия экрана — так строится экран настроек или форма редактирования: заполни поля, пока собираешь дерево, затем открой его.

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

name.value = user.name           // задано до открытия — поле поднимется заполненным
bio.value = user.bio             // textarea поднимется уже выросшей под контент

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

Чтение возвращает живой текст, пока экран открыт, и последнее заданное тобой значение, пока нет. Набранное пользователем не переносится через закрытие/открытие — переоткрытый экран строится заново и поднимается со значениями, заданными твоим кодом. Используй screen.keepAlive(), когда полузаполненная форма должна пережить уход навигацией.

Специфичные для инпута стили, поверх обычных стилей элемента + текста (fontSize, color, fontFamily, …):

TypeScript
input.style({
  placeholder: "Search…",
  placeholderColor: "#999",
  type: "search",         // "text" (по умолчанию) | "password" | "search" | "phone" | "email"
                          //   | "number" | "decimal" | "url" | "date" | "time"
  enterKey: "search",     // подпись клавиши return: "done" | "go" | "next" | "search" | "send" (только UIInput)
  maxLength: 32,          // жёсткий предел, хост навязывает его при наборе/вставке
  autocapitalize: "none", // "none" | "words" | "sentences" | "characters"; не задано = дефолт платформы
  autocorrect: false,     // платформенная автокоррекция/подсказки; не задано = дефолт платформы
  onFocused: { borderColor: "#FF4032", duration: 150 },   // стиль во время фокуса (как onPressed;
                                                          // каскадная форма: $focused)
})
Warning

значения typeподсказки клавиатуры, а не валидаторыnumber показывает цифровую клавиатуру, но не блокирует вставленные буквы; валидируй в onChange сам. Значение телефонной клавиатуры — phone (в LeCodes нет HTML-подобного tel).

Note

дай инпутам явный height и flexGrow: 1 (в строке) или width — инпут схлопывается до ширины своего плейсхолдера и высоты своего текста, он не растягивается как веб-<input>.

Пикеры даты и времени (type: "date" | "time")

date и timeвиды пикеров, а не клавиатуры: фокус на поле открывает нативный пикер платформы в слоте клавиатуры (колёса iOS, календарь браузера), а свободный набор отключён — значение приходит только из пикера или из кода. Само поле остаётся обычным, полностью стилизуемым инпутом.

  • input.value всегда каноничен: "YYYY-MM-DD" для date, "HH:MM" (24-часовой формат) для time — это получает onChange и это же задаёшь программно. То, что пользователь видит в поле, — локализованная строка, которую форматирует хост; никогда не парси отображение.
  • Пустое значение показывает placeholder. На iOS подтверждение панелью Done принимает текущую позицию колёс, даже если пользователь их не крутил (открыл → Done = сегодня); тап по пустому месту просто закрывает без принятия.
  • Вся обычная механика работает без изменений: стилизация onFocused, focus()/blur(), keyboardShrink, доктрина скрытия клавиатуры.
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"              // канонично на входе, локализованное отображение на выходе

Авторост (UITextArea)

UITextArea без явного height измеряет собственный контент и растёт построчно по мере набора. Ограничь диапазон через minHeight / maxHeight — за maxHeight текст прокручивается внутри поля. Фиксированный height отключает авторост (внутренняя прокрутка с самого начала). Это рецепт композера чата:

TypeScript
const composer = UITextArea().style({
  placeholder: "Message", fontSize: 16,
  maxHeight: 108,          // ~5 строк, дальше прокрутка внутри
  keyboardDismiss: false,  // чат: тап по переписке не прячет клавиатуру
  // без height — растёт с контентом; ширина приходит от растяжения обёртки
})
UIRow(
  UIColumn(composer).style({ flexGrow: 1, flexShrink: 1, flexBase: 0, px: 14, py: 11, borderRadius: 22 }),
  sendButton,
).style({ alignItems: "flex-end", gap: 10 })   // кнопка отправки остаётся на якоре, пока поле растёт

Не давай самой textarea flexGrow/flexBase — flex-базис переопределяет внутреннюю высоту контента, и поле перестаёт расти.

Отправка и поток формы

onSubmit срабатывает, когда пользователь нажимает клавишу return на клавиатуре (только UIInput — в UITextArea Enter — это перевод строки, и точка). При отправке клавиатура прячется, кроме enterKey: "next" — тогда фокус должен перенести ты сам, и клавиатура остаётся:

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

focus() также покрывает экраны поиска с фокусом при открытии и валидацию в духе «сфокусируй невалидное поле».

Политика клавиатуры: keyboardShrink

TypeScript
input.style({ keyboardShrink: false })   // клавиатура накрывает UI — без релэйаута

Что делает раскладка, пока клавиатура поднята, решает сфокусированный инпут. true (по умолчанию): вьюпорт раскладки сжимается до области над клавиатурой (один релэйаут). false: клавиатура накрывает UI без пересчёта раскладки — для композеров чата, управляющих своим отступом самостоятельно, и полноэкранных канвасов, где рефлоу хуже перекрытия. В режиме оверлея читай app.keyboardHeight / событие "keyboard", чтобы освободить место самому:

TypeScript
// композер чата: режим оверлея + свой отступ — pb едет на клавиатуре
input.style({ keyboardShrink: false })
app.addEventListener("keyboard", (height) => {
  composerRow.style.pb = Math.max(24, height)    // 24 ≈ нижняя безопасная зона
})

Всё остальное про клавиатуру — работа хоста: он прокручивает сфокусированный инпут в видимую область внутри ближайшего скролл-контейнера, а скрытие следует одной универсальной доктрине:

  • Тап по контролу никогда не прячет. Тап по UIButton срабатывает с поднятой клавиатурой; тап по другому инпуту просто переносит фокус. (Кнопка отправки рядом с композером работает без мигания клавиатуры — вызови input.blur() из обработчика, когда прятать нужно.)
  • Прокрутка никогда не прячет — если скроллируемый сам не попросил. Так что выпадающие подсказки или список автокомплита под сфокусированным инпутом можно прокручивать и тапать его пункты с поднятой клавиатурой — без спецобработки. На iOS перетаскивание вниз, дошедшее до самой клавиатуры, прячет её интерактивно (жест iMessage); обычная прокрутка — нет. Скроллируемый может сменить собственную политику стилем keyboardDismissMode (containers.md): "scroll" заставляет любой драг в нём прятать сразу при старте — правильное ощущение для списка результатов поиска — а "none" выключает даже интерактивный драг.
  • Тап по неинтерактивной области прячет — фон, текст, картинки. Это единственная часть, от которой инпут может отказаться через keyboardDismiss: false (композеры чата — тогда тапы не прячут никогда, и остаются только blur(), клавиша return и жест перетаскивания вниз на iOS).
  • Клавиша return прячет согласно enterKey (кроме "next"), а iOS показывает панель Done над клавиатурами без клавиши return (number / decimal / phone). input.blur() остаётся программным путём.

Обычно приложение не содержит никакого кода про клавиатуру, кроме цепочки enterKey: "next"next.focus() — плюс keyboardDismiss: false на композере чата.

Подводные камни

TypeScript
// ✗ обработчик касания на обычном контейнере
UIColumn(...).onTouchStart(cb)              // нет такого метода — только UIButton/UIScreen/UIWidget
// ✓ оберни в UIButton
UIButton(...).onTouchStart(cb)

// ✗ кнопка рядом с инпутом 40px сжимается до высоты своего текста
UIRow(input.style({ height: 40 }), UIButton(icon))
// ✓ дай контролам одинаковый явный height
UIRow(input.style({ height: 40 }), UIButton(icon).style({ height: 40 }))

Смотрите также