From 42536b45128a59445526e44e4d736569206b71f8 Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Wed, 17 Dec 2025 10:41:01 +0100 Subject: [PATCH] fix(ui): Simplify SearchField collapsed tracking logic Removes the internal state tracking almost completely, and instead computes collapsed status based on focus and input value. This fixes a bug where a controlled search field would contract when the user writes something and then deletes the value via keyboard. Signed-off-by: Johan Persson --- .../SearchField/SearchField.stories.tsx | 21 +++++++- .../components/SearchField/SearchField.tsx | 48 ++++--------------- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/packages/ui/src/components/SearchField/SearchField.stories.tsx b/packages/ui/src/components/SearchField/SearchField.stories.tsx index 203e11c73c..96f0310e73 100644 --- a/packages/ui/src/components/SearchField/SearchField.stories.tsx +++ b/packages/ui/src/components/SearchField/SearchField.stories.tsx @@ -307,7 +307,7 @@ export const StartCollapsedWithOnChange: Story = { }, }; -export const StartCollapsedControlled: Story = { +export const StartCollapsedControlledEmpty: Story = { args: { ...StartCollapsed.args, }, @@ -325,3 +325,22 @@ export const StartCollapsedControlled: Story = { ); }, }; + +export const StartCollapsedControlledWithValue: Story = { + args: { + ...StartCollapsed.args, + }, + render: function Render(args) { + const [value, setValue] = useState('Component'); + + return ( + + + + ); + }, +}; diff --git a/packages/ui/src/components/SearchField/SearchField.tsx b/packages/ui/src/components/SearchField/SearchField.tsx index aa5e22d900..2dffd12a93 100644 --- a/packages/ui/src/components/SearchField/SearchField.tsx +++ b/packages/ui/src/components/SearchField/SearchField.tsx @@ -68,25 +68,8 @@ export const SearchField = forwardRef( ...rest } = cleanedProps; - const hasProvidedValue = props.value || props.defaultValue; - const initialCollapsed = !!startCollapsed && !hasProvidedValue; + const [isFocused, setIsFocused] = useState(false); const inputRef = useRef(null); - const [isCollapsed, setIsCollapsed] = useState(initialCollapsed); - const [shouldCollapse, setShouldCollapse] = useState(!hasProvidedValue); - - // If a value becomes available (controlled or defaultValue), force expand so the user can see it. - // If it becomes empty again and startCollapsed is enabled, allow collapse behavior again. - useEffect(() => { - if (hasProvidedValue) { - setShouldCollapse(false); - setIsCollapsed(false); - } else { - setShouldCollapse(true); - if (startCollapsed) { - setIsCollapsed(true); - } - } - }, [hasProvidedValue, startCollapsed]); // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. const secondaryLabelText = @@ -94,35 +77,24 @@ export const SearchField = forwardRef( const handleFocusChange = (isFocused: boolean) => { props.onFocusChange?.(isFocused); - if (shouldCollapse) { - if (isFocused) { - // When focusing, expand the field (unless startCollapsed + a value/defaultValue is provided) - setIsCollapsed(false); - } else { - // When blurring, collapse the field - setIsCollapsed(true); - } - } + setIsFocused(isFocused); }; const handleContainerClick = () => { - if (startCollapsed && isCollapsed) { - // Focus the input after state update - setTimeout(() => { - inputRef.current?.focus(); - }, 0); - } + inputRef.current?.focus(); }; const handleChange = (value: string) => { props.onChange?.(value); - if (value.length > 0) { - setShouldCollapse(false); - } else { - setShouldCollapse(true); - } }; + const hasInputRef = !!inputRef.current; + const hasValue = !!inputRef.current?.value; + + const isCollapsed = hasInputRef + ? startCollapsed && !hasValue && !isFocused + : startCollapsed && !rest.value && !rest.defaultValue && !isFocused; + return (