Merge pull request #32123 from backstage/bui-fix-searchfield
BUI - Fixes startCollapsed prop in SearchField
This commit is contained in:
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@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).
|
||||
|
||||
Affected components: SearchField
|
||||
@@ -40,24 +40,25 @@
|
||||
}
|
||||
|
||||
&[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 +80,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&[data-collapsed='false'] {
|
||||
flex-basis: 200px;
|
||||
width: 200px;
|
||||
max-width: 200px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { useState } from 'react';
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { SearchField } from './SearchField';
|
||||
import { Form } from 'react-aria-components';
|
||||
@@ -165,13 +166,25 @@ 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>
|
||||
),
|
||||
};
|
||||
|
||||
export const StartCollapsedWithValue: Story = {
|
||||
args: {
|
||||
...StartCollapsed.args,
|
||||
defaultValue: 'https://example.com',
|
||||
},
|
||||
|
||||
render: args => <SearchField {...args} size="small" />,
|
||||
};
|
||||
|
||||
export const InHeader: Story = {
|
||||
decorators: [
|
||||
Story => (
|
||||
@@ -293,3 +306,41 @@ export const StartCollapsedWithOnChange: Story = {
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const StartCollapsedControlledEmpty: Story = {
|
||||
args: {
|
||||
...StartCollapsed.args,
|
||||
},
|
||||
render: function Render(args) {
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="row"
|
||||
gap="2"
|
||||
style={{ width: '100%', maxWidth: '600px' }}
|
||||
>
|
||||
<SearchField {...args} size="small" value={value} onChange={setValue} />
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export const StartCollapsedControlledWithValue: Story = {
|
||||
args: {
|
||||
...StartCollapsed.args,
|
||||
},
|
||||
render: function Render(args) {
|
||||
const [value, setValue] = useState('Component');
|
||||
|
||||
return (
|
||||
<Flex
|
||||
direction="row"
|
||||
gap="2"
|
||||
style={{ width: '100%', maxWidth: '600px' }}
|
||||
>
|
||||
<SearchField {...args} size="small" value={value} onChange={setValue} />
|
||||
</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,30 +68,29 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
|
||||
...rest
|
||||
} = cleanedProps;
|
||||
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
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);
|
||||
|
||||
const handleClick = (isFocused: boolean) => {
|
||||
const handleFocusChange = (isFocused: boolean) => {
|
||||
props.onFocusChange?.(isFocused);
|
||||
if (shouldCollapse) {
|
||||
if (isFocused) {
|
||||
setIsCollapsed(true);
|
||||
} else {
|
||||
setIsCollapsed(false);
|
||||
}
|
||||
}
|
||||
setIsFocused(isFocused);
|
||||
};
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
props.onChange?.(value);
|
||||
if (value.length > 0) {
|
||||
setShouldCollapse(false);
|
||||
} else {
|
||||
setShouldCollapse(true);
|
||||
}
|
||||
const handleContainerClick = () => {
|
||||
inputRef.current?.focus();
|
||||
};
|
||||
|
||||
const hasInputRef = !!inputRef.current;
|
||||
const hasValue = !!inputRef.current?.value;
|
||||
|
||||
const isCollapsed = hasInputRef
|
||||
? startCollapsed && !hasValue && !isFocused
|
||||
: startCollapsed && !rest.value && !rest.defaultValue && !isFocused;
|
||||
|
||||
return (
|
||||
<AriaSearchField
|
||||
className={clsx(classNames.root, styles[classNames.root], className)}
|
||||
@@ -102,10 +98,9 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
|
||||
aria-label={ariaLabel}
|
||||
aria-labelledby={ariaLabelledBy}
|
||||
data-collapsed={isCollapsed}
|
||||
onFocusChange={handleClick}
|
||||
onChange={handleChange}
|
||||
style={style}
|
||||
{...rest}
|
||||
onFocusChange={handleFocusChange}
|
||||
ref={ref}
|
||||
>
|
||||
<FieldLabel
|
||||
@@ -119,6 +114,7 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
|
||||
styles[classNames.inputWrapper],
|
||||
)}
|
||||
data-size={dataAttributes['data-size']}
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
{icon !== false && (
|
||||
<div
|
||||
@@ -133,6 +129,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}
|
||||
|
||||
Reference in New Issue
Block a user