diff --git a/.changeset/hot-colts-float.md b/.changeset/hot-colts-float.md
index 8bfa32256c..093dcfb12a 100644
--- a/.changeset/hot-colts-float.md
+++ b/.changeset/hot-colts-float.md
@@ -3,3 +3,5 @@
---
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).
+
+Affected components: SearchField
diff --git a/packages/ui/src/components/SearchField/SearchField.stories.tsx b/packages/ui/src/components/SearchField/SearchField.stories.tsx
index 5b6cf2df07..21f5040a6f 100644
--- a/packages/ui/src/components/SearchField/SearchField.stories.tsx
+++ b/packages/ui/src/components/SearchField/SearchField.stories.tsx
@@ -175,6 +175,15 @@ export const StartCollapsed: Story = {
),
};
+export const StartCollapsedWithValue: Story = {
+ args: {
+ ...StartCollapsed.args,
+ defaultValue: 'https://example.com',
+ },
+
+ render: args => ,
+};
+
export const InHeader: Story = {
decorators: [
Story => (
diff --git a/packages/ui/src/components/SearchField/SearchField.tsx b/packages/ui/src/components/SearchField/SearchField.tsx
index f9068ec8d6..aa5e22d900 100644
--- a/packages/ui/src/components/SearchField/SearchField.tsx
+++ b/packages/ui/src/components/SearchField/SearchField.tsx
@@ -68,19 +68,35 @@ export const SearchField = forwardRef(
...rest
} = cleanedProps;
- const [isCollapsed, setIsCollapsed] = useState(startCollapsed);
- const [shouldCollapse, setShouldCollapse] = useState(true);
+ const hasProvidedValue = props.value || props.defaultValue;
+ const initialCollapsed = !!startCollapsed && !hasProvidedValue;
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 =
secondaryLabel || (isRequired ? 'Required' : null);
- const handleClick = (isFocused: boolean) => {
+ const handleFocusChange = (isFocused: boolean) => {
props.onFocusChange?.(isFocused);
if (shouldCollapse) {
if (isFocused) {
- // When focusing, expand the field
+ // When focusing, expand the field (unless startCollapsed + a value/defaultValue is provided)
setIsCollapsed(false);
} else {
// When blurring, collapse the field
@@ -90,9 +106,7 @@ export const SearchField = forwardRef(
};
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();
@@ -116,7 +130,7 @@ export const SearchField = forwardRef(
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
data-collapsed={isCollapsed}
- onFocusChange={handleClick}
+ onFocusChange={handleFocusChange}
onChange={handleChange}
style={style}
{...rest}