/
vshmidt
/
streamlit
Обзор
Документация
Войти
/
vshmidt
/
streamlit
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
Аналитика
Безопасность
develop
frontend/lib/src/components/shared/Dropdown/Selectbox.tsx
335 строк
11 KB
Rishi Kumar
Enhancement/update dropdown design #12956 (#13004)
27 янв 2026, 23:49
Не верифицирован
27 янв 2026, 23:49
5d7b31e
Код
Авторство
О чём код?
/** * 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, useContext, useMemo, useRef, useState, } from "react" import { ChevronDown } from "baseui/icon" import { type OnChangeParams, Select as UISelect } from "baseui/select" import IsSidebarContext from "~lib/components/core/IsSidebarContext" import { getBorderColor } from "~lib/components/shared/Base/styled-components" import VirtualDropdown from "~lib/components/shared/Dropdown/VirtualDropdown" import { WidgetLabel, WidgetLabelHelpIcon, } from "~lib/components/widgets/BaseWidget" import { useEmotionTheme } from "~lib/hooks/useEmotionTheme" import { useExecuteWhenChanged } from "~lib/hooks/useExecuteWhenChanged" import { useSelectCommon } from "~lib/hooks/useSelectCommon" import { hasLightBackgroundColor } from "~lib/theme" import { LabelVisibilityOptions } from "~lib/util/utils" export interface Props { value: string | null onChange: (value: string | null) => void disabled: boolean // eslint-disable-next-line @typescript-eslint/no-explicit-any -- TODO: Replace 'any' with a more specific type. options: any[] label?: string | null labelVisibility?: LabelVisibilityOptions help?: string placeholder: string clearable?: boolean acceptNewOptions: boolean } const Selectbox: FC<Props> = ({ disabled, value: propValue, onChange, options: propOptions, label, labelVisibility, help, placeholder, clearable, acceptNewOptions, }) => { const theme = useEmotionTheme() const isInSidebar = useContext(IsSidebarContext) const [value, setValue] = useState<string | null>(propValue) // This ref is used to store the value before the user starts removing characters so that we can restore // the value in case the user dismisses the changes by clicking away. const valueBeforeRemovalRef = useRef<string | null>(value) useExecuteWhenChanged(() => { setValue(propValue) // Reset the ref when propValue changes externally (e.g., via session state) // to prevent handleBlur from restoring a stale value. valueBeforeRemovalRef.current = null }, [propValue]) const handleChange = useCallback( (params: OnChangeParams): void => { if (params.type === "remove") { valueBeforeRemovalRef.current = params.option?.value // We set the value so that BaseWeb updates the element's value while typing. // We don't want to commit the change yet, so we don't call onChange. setValue(null) return } valueBeforeRemovalRef.current = null if (params.type === "clear") { setValue(null) onChange(null) return } const [selected] = params.value setValue(selected.value) onChange(selected.value) }, [onChange] ) const handleBlur = useCallback(() => { if (valueBeforeRemovalRef.current !== null) { setValue(valueBeforeRemovalRef.current) } }, []) const opts = propOptions const { placeholder: selectboxPlaceholder, disabled: shouldDisable, selectOptions, inputReadOnly, valueToUiSingle, createFilterOptions, } = useSelectCommon({ options: opts as string[], isMulti: false, acceptNewOptions, placeholderInput: placeholder, }) const selectDisabled = disabled || shouldDisable const filterOptions = useMemo( () => createFilterOptions(), [createFilterOptions] ) const selectValue = valueToUiSingle(value) return ( <div className="stSelectbox" data-testid="stSelectbox"> <WidgetLabel label={label} labelVisibility={labelVisibility} disabled={selectDisabled} > {help && <WidgetLabelHelpIcon content={help} label={label} />} </WidgetLabel> <UISelect creatable={acceptNewOptions} disabled={selectDisabled} labelKey="label" aria-label={label || ""} onChange={handleChange} onBlur={handleBlur} options={selectOptions} filterOptions={filterOptions} clearable={clearable || false} escapeClearsValue={clearable || false} value={selectValue} valueKey="value" placeholder={selectboxPlaceholder} ignoreCase={false} overrides={{ Root: { style: () => ({ lineHeight: theme.lineHeights.inputWidget, fontWeight: theme.fontWeights.normal, }), }, Dropdown: { component: VirtualDropdown, style: { boxShadow: "none", overflow: "hidden" }, }, ClearIcon: { props: { overrides: { Svg: { style: { color: theme.colors.grayTextColor, // Setting this width and height makes the clear-icon align with dropdown arrows padding: theme.spacing.threeXS, height: theme.sizes.clearIconSize, width: theme.sizes.clearIconSize, ":hover": { fill: theme.colors.bodyText, }, }, }, }, }, }, ControlContainer: { style: ({ $isFocused }: { $isFocused: boolean }) => { const borderColor = getBorderColor(theme.colors, $isFocused) return { height: theme.sizes.minElementHeight, // 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, } }, }, IconsContainer: { style: () => ({ paddingRight: theme.spacing.sm, }), }, Placeholder: { style: () => ({ color: selectDisabled ? theme.colors.fadedText40 : theme.colors.fadedText60, }), }, ValueContainer: { style: () => ({ // Baseweb requires long-hand props, short-hand leads to weird bugs & warnings. paddingRight: theme.spacing.sm, paddingLeft: theme.spacing.md, paddingBottom: theme.spacing.sm, paddingTop: theme.spacing.sm, }), }, Input: { props: { readOnly: inputReadOnly, }, style: () => ({ lineHeight: theme.lineHeights.inputWidget, }), }, Popover: { props: { ignoreBoundary: isInSidebar, overrides: { Body: { style: () => { const lightBackground = hasLightBackgroundColor(theme) return { marginTop: theme.spacing.twoXS, marginRight: theme.spacing.none, marginBottom: theme.spacing.none, paddingTop: theme.spacing.sm, paddingBottom: theme.spacing.sm, maxHeight: "70vh", overflow: "auto", boxSizing: "border-box", borderTopLeftRadius: theme.radii.default, borderTopRightRadius: theme.radii.default, borderBottomRightRadius: theme.radii.default, borderBottomLeftRadius: theme.radii.default, borderLeftWidth: lightBackground ? "0" : theme.sizes.borderWidth, borderRightWidth: lightBackground ? "0" : theme.sizes.borderWidth, borderTopWidth: lightBackground ? "0" : theme.sizes.borderWidth, borderBottomWidth: lightBackground ? "0" : theme.sizes.borderWidth, borderLeftStyle: lightBackground ? "none" : "solid", borderRightStyle: lightBackground ? "none" : "solid", borderTopStyle: lightBackground ? "none" : "solid", borderBottomStyle: lightBackground ? "none" : "solid", borderLeftColor: lightBackground ? "transparent" : theme.colors.borderColor, borderRightColor: lightBackground ? "transparent" : theme.colors.borderColor, borderTopColor: lightBackground ? "transparent" : theme.colors.borderColor, borderBottomColor: lightBackground ? "transparent" : theme.colors.borderColor, boxShadow: lightBackground ? "0px 4px 16px rgba(0, 0, 0, 0.16)" : "0px 4px 16px rgba(0, 0, 0, 0.7)", } }, }, }, }, }, SingleValue: { style: () => ({ // remove margin from select value so that there is no jumpb, e.g. when pressing backspace on a selected option and removing a character. marginLeft: theme.spacing.none, }), }, SelectArrow: { component: ChevronDown, props: { style: { ...(selectDisabled && { cursor: "not-allowed", }), }, overrides: { Svg: { style: () => ({ width: theme.iconSizes.xl, height: theme.iconSizes.xl, }), }, }, }, }, }} /> </div> ) } export default memo(Selectbox)