Fixes SearchField in Backstage UI

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-12-14 09:28:02 +00:00
parent b1f44f9d0d
commit b4a4911c47
4 changed files with 46 additions and 13 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/ui': patch
---
Fixed SearchField `startCollapsed` prop not working correctly in Backstage UI. The field now properly starts in a collapsed state, expands when clicked and focused, and collapses back when unfocused with no input. Also fixed CSS logic to work correctly in all layout contexts (flex row, flex column, and regular containers).
@@ -40,24 +40,27 @@
}
&[data-startCollapsed='true'] {
transition: flex-basis 0.3s ease-in-out;
transition:
flex-basis 0.3s ease-in-out,
width 0.3s ease-in-out,
max-width 0.3s ease-in-out;
padding: 0;
flex: 0 1 auto;
&[data-collapsed='true'] {
flex-basis: 200px;
}
&[data-collapsed='false'] {
cursor: pointer;
&[data-size='medium'] {
flex-basis: 2.5rem;
width: 2.5rem;
max-width: 2.5rem;
height: 2.5rem;
}
&[data-size='small'] {
flex-basis: 2rem;
width: 2rem;
max-width: 2rem;
height: 2rem;
}
@@ -79,6 +82,12 @@
}
}
}
&[data-collapsed='false'] {
flex-basis: 200px;
width: 200px;
max-width: 200px;
}
}
}
@@ -165,9 +165,12 @@ export const StartCollapsed: Story = {
},
render: args => (
<Flex direction="row" gap="4">
<Flex direction="column" gap="4">
<Flex direction="row" gap="4">
<SearchField {...args} size="small" />
<SearchField {...args} size="medium" />
</Flex>
<SearchField {...args} size="small" />
<SearchField {...args} size="medium" />
</Flex>
),
};
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { forwardRef, useEffect, useState } from 'react';
import { forwardRef, useEffect, useState, useRef } from 'react';
import {
Input,
SearchField as AriaSearchField,
@@ -39,9 +39,6 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
'aria-labelledby': ariaLabelledBy,
} = props;
const [isCollapsed, setIsCollapsed] = useState(false);
const [shouldCollapse, setShouldCollapse] = useState(true);
useEffect(() => {
if (!label && !ariaLabel && !ariaLabelledBy) {
console.warn(
@@ -71,6 +68,10 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
...rest
} = cleanedProps;
const [isCollapsed, setIsCollapsed] = useState(startCollapsed);
const [shouldCollapse, setShouldCollapse] = useState(true);
const inputRef = useRef<HTMLInputElement>(null);
// If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required.
const secondaryLabelText =
secondaryLabel || (isRequired ? 'Required' : null);
@@ -79,13 +80,26 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
props.onFocusChange?.(isFocused);
if (shouldCollapse) {
if (isFocused) {
setIsCollapsed(true);
} else {
// When focusing, expand the field
setIsCollapsed(false);
} else {
// When blurring, collapse the field
setIsCollapsed(true);
}
}
};
const handleContainerClick = () => {
// If the field is collapsed (small), expand it and focus the input
if (startCollapsed && isCollapsed) {
setIsCollapsed(false);
// Focus the input after state update
setTimeout(() => {
inputRef.current?.focus();
}, 0);
}
};
const handleChange = (value: string) => {
props.onChange?.(value);
if (value.length > 0) {
@@ -119,6 +133,7 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
styles[classNames.inputWrapper],
)}
data-size={dataAttributes['data-size']}
onClick={handleContainerClick}
>
{icon !== false && (
<div
@@ -133,6 +148,7 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
</div>
)}
<Input
ref={inputRef}
className={clsx(classNames.input, styles[classNames.input])}
{...(icon !== false && { 'data-icon': true })}
placeholder={placeholder}