UIButton, UIInput и UITextArea
Три элемента, принимающие ввод пользователя. UIButton — единственный тапабельный контейнер —
обработчики касаний существуют только на UIButton, UIScreen и UIWidget (см.
События указателя); чтобы сделать что-либо кликабельным (карточку, строку
списка, иконку), оберни это в UIButton. UIInput — однострочное текстовое поле, UITextArea — его
многострочный собрат.
Обзор
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
UIButton(...children: (UINodeChild | UINodeChild[])[]): UIButtonКонтейнер с дефолтом flexDirection: "row" и детьми, центрированными по обеим осям
(justifyContent + alignItems "center") — кнопка «иконка+подпись» раскладывается правильно без
стилизации. Полная поверхность контейнера: .append / .insert / .remove / .setContent,
.style, .animateTo / .animateFrom, .onLayout. Используй flexDirection: "column" для кнопки
в форме карточки.
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({...}) — так что «зажми, удерживай, затем тащи» — это один
обработчик:
button.onLongPress(ev => {
device.vibrate("medium") // подтверждаем, что удержание засчитано
ev.track({
claim: true,
onMove: ({ deltaX, deltaY }) => moveElement(deltaX, deltaY),
onEnd: () => commit(),
})
})кнопки не рисуют собственного оформления — стилизуй их как любой контейнер (bgColor,
borderRadius, отступы). Дай кнопке явный height: сама по себе она высотой только со свой текст.
Отклик на нажатие — по желанию
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
UIInput(): UIInput // однострочное поле
UITextArea(): UITextArea // многострочное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 можно писать до открытия экрана — так строится экран настроек или форма
редактирования: заполни поля, пока собираешь дерево, затем открой его.
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, …):
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)
})значения type — подсказки клавиатуры, а не валидаторы — number показывает цифровую
клавиатуру, но не блокирует вставленные буквы; валидируй в onChange сам. Значение телефонной
клавиатуры — phone (в LeCodes нет HTML-подобного tel).
дай инпутам явный 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, доктрина скрытия клавиатуры.
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 отключает авторост (внутренняя прокрутка с самого начала).
Это рецепт композера чата:
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" — тогда фокус должен перенести ты сам, и клавиатура
остаётся:
const name = UIInput().style({ enterKey: "next" }).onSubmit(() => email.focus())
const email = UIInput().style({ type: "email", enterKey: "done" })
.onSubmit(() => submitForm())focus() также покрывает экраны поиска с фокусом при открытии и валидацию в духе «сфокусируй
невалидное поле».
Политика клавиатуры: keyboardShrink
input.style({ keyboardShrink: false }) // клавиатура накрывает UI — без релэйаутаЧто делает раскладка, пока клавиатура поднята, решает сфокусированный инпут. true
(по умолчанию): вьюпорт раскладки сжимается до области над клавиатурой (один релэйаут). false:
клавиатура накрывает UI без пересчёта раскладки — для композеров чата, управляющих своим отступом
самостоятельно, и полноэкранных канвасов, где рефлоу хуже перекрытия. В режиме оверлея читай
app.keyboardHeight / событие "keyboard", чтобы освободить место самому:
// композер чата: режим оверлея + свой отступ — 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 на композере чата.
Подводные камни
// ✗ обработчик касания на обычном контейнере
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 }))Смотрите также
- События указателя и жесты — объекты событий,
ev.track(),claim. - Стилизация —
.style(), состояния стиля,animateTo. - Контейнеры — правила раскладки
UIRow/UIColumn, которые наследует кнопка.