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 <johanopersson@gmail.com>
This commit is contained in:
@@ -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 (
|
||||
<Flex
|
||||
direction="row"
|
||||
gap="2"
|
||||
style={{ width: '100%', maxWidth: '600px' }}
|
||||
>
|
||||
<SearchField {...args} size="small" value={value} onChange={setValue} />
|
||||
</Flex>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
@@ -68,25 +68,8 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
|
||||
...rest
|
||||
} = cleanedProps;
|
||||
|
||||
const hasProvidedValue = props.value || props.defaultValue;
|
||||
const initialCollapsed = !!startCollapsed && !hasProvidedValue;
|
||||
const [isFocused, setIsFocused] = useState(false);
|
||||
const inputRef = useRef<HTMLInputElement>(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<HTMLDivElement, SearchFieldProps>(
|
||||
|
||||
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 (
|
||||
<AriaSearchField
|
||||
className={clsx(classNames.root, styles[classNames.root], className)}
|
||||
|
||||
Reference in New Issue
Block a user