/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/widgets/TextArea/TextArea.tsx
310 строк
9 KB
Maya Barnes
Bind widgets to query params - `st.text_input` & `st.text_area` (#13901)
12 фев 2026, 02:33
Не верифицирован
12 фев 2026, 02:33
f345cbe
Код
Авторство
О чём код?
/** * Copyright (c) Streamlit Inc. (2018-2022) Snowflake Inc. (2022-2026) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { FC, memo, useCallback, useRef, useState } from "react" import { Textarea as UITextArea } from "baseui/textarea" import { uniqueId } from "lodash-es" import { Element, TextArea as TextAreaProto } from "@streamlit/protobuf" import { getBorderColor } from "~lib/components/shared/Base/styled-components" import InputInstructions from "~lib/components/shared/InputInstructions/InputInstructions" import { WidgetLabel, WidgetLabelHelpIcon, } from "~lib/components/widgets/BaseWidget" import { useBasicWidgetState, ValueWithSource, } from "~lib/hooks/useBasicWidgetState" import { useCalculatedDimensions } from "~lib/hooks/useCalculatedDimensions" import { useEmotionTheme } from "~lib/hooks/useEmotionTheme" import useOnInputChange from "~lib/hooks/useOnInputChange" import useSubmitFormViaEnterKey from "~lib/hooks/useSubmitFormViaEnterKey" import { useTextInputAutoExpand } from "~lib/hooks/useTextInputAutoExpand" import useUpdateUiValue from "~lib/hooks/useUpdateUiValue" import { convertRemToPx } from "~lib/theme" import { isInForm, labelVisibilityProtoValueToEnum } from "~lib/util/utils" import { WidgetStateManager } from "~lib/WidgetStateManager" import { getTextAreaHeight } from "./heightUtils" import { StyledTextAreaContainer } from "./styled-components" export interface Props { disabled: boolean element: TextAreaProto widgetMgr: WidgetStateManager fragmentId?: string // needed for height outerElement: Element } type TextAreaValue = string | null const getStateFromWidgetMgr = ( widgetMgr: WidgetStateManager, element: TextAreaProto ): TextAreaValue | null => { return widgetMgr.getStringValue(element) ?? null } const getDefaultStateFromProto = (element: TextAreaProto): TextAreaValue => { return element.default ?? null } const getCurrStateFromProto = (element: TextAreaProto): TextAreaValue => { return element.value ?? null } const updateWidgetMgrState = ( element: TextAreaProto, widgetMgr: WidgetStateManager, valueWithSource: ValueWithSource<TextAreaValue>, fragmentId?: string ): void => { widgetMgr.setStringValue( element, valueWithSource.value, { fromUi: valueWithSource.fromUi }, fragmentId ) } const TextArea: FC<Props> = ({ disabled, element, widgetMgr, fragmentId, outerElement, }) => { // eslint-disable-next-line react-hooks/refs -- TODO: Do not access ref during render const id = useRef(uniqueId("text_area_")).current const { width, elementRef } = useCalculatedDimensions() /** * True if the user-specified state.value has not yet been synced to the WidgetStateManager. */ const [dirty, setDirty] = useState(false) /** * Whether the area is currently focused. */ const [focused, setFocused] = useState(false) // Determine if we should use auto-expansion. const isAutoHeight = outerElement.heightConfig?.useContent ?? false // Disable resize if stretch height is enabled. const isStretchHeight = outerElement.heightConfig?.useStretch ?? false // For text area, we need to set the height on the input element and let // that determine the height of the overall element so that resizing works. const inputHeight = getTextAreaHeight(outerElement, element) // Create ref for auto-expansion const textareaRef = useRef<HTMLTextAreaElement>(null) /** * The value specified by the user via the UI. If the user didn't touch this * widget's UI, the default value is used. */ const [uiValue, setUiValue] = useState<string | null>( () => getStateFromWidgetMgr(widgetMgr, element) ?? null ) const onFormCleared = useCallback(() => { setUiValue(element.default ?? null) setDirty(true) }, [element]) const queryParamBinding = element.queryParamKey ? { paramKey: element.queryParamKey, valueType: "string_value" as const, // Text area is clearable (empty string is a valid value) clearable: true, } : undefined const [value, setValueWithSource] = useBasicWidgetState< TextAreaValue, TextAreaProto >({ getStateFromWidgetMgr, getDefaultStateFromProto, getCurrStateFromProto, updateWidgetMgrState, element, widgetMgr, fragmentId, onFormCleared, queryParamBinding, }) useUpdateUiValue(value, uiValue, setUiValue, dirty) const theme = useEmotionTheme() const { height: autoExpandHeight, maxHeight: autoExpandMaxHeight, updateScrollHeight, } = useTextInputAutoExpand({ textareaRef, dependencies: [element.placeholder], }) const commitWidgetValue = useCallback((): void => { setDirty(false) setValueWithSource({ value: uiValue, fromUi: true }) }, [uiValue, setValueWithSource]) const onBlur = useCallback(() => { if (dirty) { commitWidgetValue() } setFocused(false) }, [dirty, commitWidgetValue]) const onFocus = useCallback(() => { setFocused(true) }, []) const additionalAction = useCallback(() => { if (isAutoHeight) { updateScrollHeight() } }, [isAutoHeight, updateScrollHeight]) const onChange = useOnInputChange({ formId: element.formId, maxChars: element.maxChars, setDirty, setUiValue, setValueWithSource, additionalAction, }) const onKeyDown = useSubmitFormViaEnterKey( element.formId, commitWidgetValue, dirty, widgetMgr, fragmentId, true ) const { placeholder, formId } = element // Show "Please enter" instructions if in a form & allowed, or not in form and state is dirty. const allowEnterToSubmit = isInForm({ formId }) ? widgetMgr.allowFormEnterToSubmit(formId) : dirty // Hide input instructions for small widget sizes. const shouldShowInstructions = focused && width > convertRemToPx(theme.breakpoints.hideWidgetDetails) return ( <StyledTextAreaContainer className="stTextArea" data-testid="stTextArea" ref={elementRef} > <WidgetLabel label={element.label} disabled={disabled} labelVisibility={labelVisibilityProtoValueToEnum( element.labelVisibility?.value )} htmlFor={id} > {element.help && ( <WidgetLabelHelpIcon content={element.help} label={element.label} /> )} </WidgetLabel> <UITextArea inputRef={isAutoHeight ? textareaRef : undefined} value={uiValue ?? ""} placeholder={placeholder} onBlur={onBlur} onFocus={onFocus} onChange={onChange} onKeyDown={onKeyDown} aria-label={element.label} disabled={disabled} id={id} overrides={{ Input: { style: { fontWeight: theme.fontWeights.normal, lineHeight: theme.lineHeights.inputWidget, // The default height of the text area is calculated to perfectly fit 3 lines of text. height: isAutoHeight ? autoExpandHeight : inputHeight, maxHeight: isAutoHeight ? autoExpandMaxHeight : "", minHeight: theme.sizes.largestElementHeight, resize: isStretchHeight ? "none" : "vertical", // Baseweb requires long-hand props, short-hand leads to weird bugs & warnings. paddingRight: theme.spacing.md, paddingLeft: theme.spacing.md, paddingBottom: theme.spacing.md, paddingTop: theme.spacing.md, "::placeholder": { color: theme.colors.fadedText60, }, }, }, Root: { props: { "data-testid": "stTextAreaRootElement", }, style: ({ $isFocused }: { $isFocused: boolean }) => { const borderColor = getBorderColor(theme.colors, $isFocused) return { // Baseweb requires long-hand props, short-hand leads to weird bugs & warnings. borderLeftWidth: theme.sizes.borderWidth, borderRightWidth: theme.sizes.borderWidth, borderTopWidth: theme.sizes.borderWidth, borderBottomWidth: theme.sizes.borderWidth, borderTopColor: borderColor, borderRightColor: borderColor, borderBottomColor: borderColor, borderLeftColor: borderColor, flexGrow: 1, } }, }, }} /> {shouldShowInstructions && ( <InputInstructions dirty={dirty} value={uiValue ?? ""} maxLength={element.maxChars} type={"multiline"} inForm={isInForm({ formId })} allowEnterToSubmit={allowEnterToSubmit} /> )} </StyledTextAreaContainer> ) } export default memo(TextArea)