/* * 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 {hoistCmp, HoistProps, LayoutProps, StyleProps} from '@xh/hoist/core'; import '@xh/hoist/desktop/register'; import {textArea as bpTextarea} from '@xh/hoist/kit/blueprint'; import {TEST_ID, withDefault} from '@xh/hoist/utils/js'; import {composeRefs, getLayoutProps} from '@xh/hoist/utils/react'; import {Ref} from 'react'; import './TextArea.scss'; export interface TextAreaProps extends HoistProps, HoistInputProps, LayoutProps, StyleProps { value?: string; /** True to focus the control on render. */ autoFocus?: boolean; /** True to commit on every change/keystroke, default false. */ commitOnChange?: boolean; /** Ref handler that receives HTML element backing this component. */ inputRef?: Ref; /** Callback for normalized keydown event. */ onKeyDown?: (e: KeyboardEvent) => void; /** True to select contents when control receives focus. */ selectOnFocus?: boolean; /** True to allow browser spell check, default false. */ spellCheck?: boolean; /** Text to display when control is empty. */ placeholder?: string; } /** * A multi-line text input. */ export const [TextArea, textArea] = hoistCmp.withFactory({ displayName: 'TextArea', className: 'xh-text-area', render(props, ref) { return useHoistInputModel(cmp, props, ref, TextAreaInputModel); } }); (TextArea as any).hasLayoutSupport = true; //----------------------- // Implementation //----------------------- class TextAreaInputModel extends HoistInputModel { override xhImpl = true; override get commitOnChange() { return withDefault(this.componentProps.commitOnChange, false); } onChange = ev => { this.noteValueChange(ev.target.value); }; onKeyDown = ev => { if (ev.key === 'Enter' && !ev.shiftKey) this.doCommit(); this.componentProps.onKeyDown?.(ev); }; override onFocus = ev => { if (this.componentProps.selectOnFocus) { ev.target.select(); } this.noteFocused(); }; } // Note: we don't use the `ref` here, but the presence of a second argument is required. const cmp = hoistCmp.factory(({model, className, ...props}, ref) => { const {width, height, flex, ...layoutProps} = getLayoutProps(props); return bpTextarea({ value: model.renderValue || '', autoFocus: props.autoFocus, disabled: props.disabled, inputRef: composeRefs(model.inputRef as Ref, props.inputRef), placeholder: props.placeholder, spellCheck: withDefault(props.spellCheck, false), tabIndex: props.tabIndex, [TEST_ID]: props.testId, id: props.id, className, style: { ...props.style, ...layoutProps, width: withDefault(width, 300), height: withDefault(height, 100), flex: withDefault(flex, null) }, onBlur: model.onBlur, onChange: model.onChange, onFocus: model.onFocus, onKeyDown: model.onKeyDown }); });