/* * This file belongs to Hoist, an application development toolkit * developed by Extremely Heavy Industries (www.xh.io | info@xh.io) * * Copyright © 2026 Extremely Heavy Industries Inc. */ import {HoistInputModel, HoistInputProps, useHoistInputModel} from '@xh/hoist/cmp/input'; import {box, hbox} from '@xh/hoist/cmp/layout'; import {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core'; import {Icon} from '@xh/hoist/icon'; import {input} from '@xh/hoist/kit/onsen'; import {button} from '@xh/hoist/mobile/cmp/button'; import '@xh/hoist/mobile/register'; import {getTestId, TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {getLayoutProps} from '@xh/hoist/utils/react'; import type {Property} from 'csstype'; import {isEmpty} from 'lodash'; import {ReactElement} from 'react'; import './TextInput.scss'; export interface TextInputProps extends HoistProps, HoistInputProps, StyleProps, LayoutProps { value?: string; /** * HTML `autocapitalize` attribute to set on underlying element. * * Hoist does not specify a default value, deferring to the browser, but pass 'none' to disable * for inputs where auto-capitalization is not desired or does not generally make sense. */ autoCapitalize?: string; /** * HTML `autocomplete` attribute to set on underlying element. * * Defaults to 'off' for fields of type text and 'new-password' for fields of type 'password' * to defeat browser auto-completion, which is typically not desired in Hoist applications. * Set to 'on' or a more specific autocomplete token to enable. * * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#autofilling-form-controls%3A-the-autocomplete-attribute * See https://developer.mozilla.org/en-US/docs/Web/Security/Securing_your_site/Turning_off_form_autocompletion */ autoComplete?: string; /** True to commit on every change/keystroke, default false. */ commitOnChange?: boolean; /** True to show a "clear" button aligned to the right of the control. Defaults to false. */ enableClear?: boolean; /** Icon to display inline on the left side of the input. */ leftIcon?: ReactElement; /** Onsen modifier string */ modifier?: string; /** Text to display when control is empty */ placeholder?: string; /** Whether text in field is selected when field receives focus */ selectOnFocus?: boolean; /** Whether to allow browser spell check, defaults to false */ spellCheck?: boolean; /** Alignment of entry text within control, default 'left'. */ textAlign?: Property.TextAlign; /** Underlying HTML element type. */ type?: 'text' | 'password'; } /** * A Text Input */ export const [TextInput, textInput] = hoistCmp.withFactory({ displayName: 'TextInput', className: 'xh-text-input', render(props, ref) { return useHoistInputModel(cmp, props, ref, TextInputModel); } }); (TextInput as any).hasLayoutSupport = true; //----------------------- // Implementation //----------------------- class TextInputModel extends HoistInputModel { override xhImpl = true; override get commitOnChange() { return withDefault(this.componentProps.commitOnChange, false); } get showClearButton() { const {enableClear, disabled} = this.componentProps; return enableClear && !disabled && !isEmpty(this.internalValue); } onChange = ev => { let {value} = ev.target; if (value === '') value = null; this.noteValueChange(value); }; onKeyDown = ev => { if (ev.key === 'Enter') this.doCommit(); this.componentProps.onKeyDown?.(ev); }; override onFocus = ev => { if (this.componentProps.selectOnFocus && ev.target && ev.target.select) { ev.target.select(); } this.noteFocused(); }; } const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { const {width, ...layoutProps} = getLayoutProps(props); return hbox({ ref, className, style: { ...props.style, ...layoutProps, width: withDefault(width, null) }, items: [ leftIcon(), input({ value: model.renderValue || '', autoCapitalize: props.autoCapitalize, autoComplete: withDefault( props.autoComplete, props.type === 'password' ? 'new-password' : 'off' ), disabled: props.disabled, modifier: props.modifier, placeholder: props.placeholder, spellCheck: withDefault(props.spellCheck, false), tabIndex: props.tabIndex, type: props.type, className: 'xh-text-input__input', style: {textAlign: withDefault(props.textAlign, 'left')}, [TEST_ID]: props.testId, onInput: model.onChange, onKeyDown: model.onKeyDown, onBlur: model.onBlur, onFocus: model.onFocus }), clearButton() ] }); }); const leftIcon = hoistCmp.factory(({model}) => { const {leftIcon} = model.componentProps; return leftIcon ? box({className: 'xh-text-input__left-icon', item: leftIcon}) : null; }); const clearButton = hoistCmp.factory(({model}) => button({ className: 'xh-text-input__clear-button', icon: Icon.cross(), tabIndex: -1, minimal: true, omit: !model.showClearButton, testId: getTestId(model.componentProps, 'clear-btn'), onClick: () => { model.noteValueChange(null); model.doCommit(); model.focus(); } }) );