Add collapsible prop on SearchField

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-08-02 13:23:46 +01:00
parent 65cd0fbc6b
commit 300d450670
13 changed files with 211 additions and 12 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -7,6 +7,7 @@ import {
searchFieldDefaultSnippet,
searchFieldSizesSnippet,
searchFieldDescriptionSnippet,
searchFieldCollapsibleSnippet,
} from './search-field.props';
import { PageTitle } from '@/components/PageTitle';
import { Theming } from '@/components/Theming';
@@ -59,6 +60,18 @@ Here's a simple SearchField with a description.
code={searchFieldDescriptionSnippet}
/>
### Collapsible
You can make the SearchField collapsible by setting the `collapsible` prop to `true`.
<Snippet
align="center"
py={4}
open
preview={<SearchFieldSnippet story="Collapsible" />}
code={searchFieldCollapsibleSnippet}
/>
<Theming component="SearchField" />
<ChangelogComponent component="search-field" />
@@ -25,6 +25,10 @@ export const searchFieldPropDefs: Record<string, PropDef> = {
type: 'string',
required: true,
},
collapsible: {
type: 'boolean',
default: 'false',
},
...classNamePropDefs,
...stylePropDefs,
};
@@ -41,3 +45,5 @@ export const searchFieldSizesSnippet = `<Flex direction="row" gap="4">
</Flex>`;
export const searchFieldDescriptionSnippet = `<SearchField label="Label" description="Description" placeholder="Enter a URL" />`;
export const searchFieldCollapsibleSnippet = `<SearchField collapsible />`;
@@ -205,6 +205,10 @@
padding-inline: var(--bui-space-1);
border: none;
}
.bui-Input {
border-radius: var(--bui-radius-3);
}
}
[data-theme='light'][data-theme-name='spotify'] {
+53 -3
View File
@@ -10277,6 +10277,7 @@
.bui-TextField {
font-family: var(--bui-font-regular);
flex-direction: column;
flex-shrink: 0;
width: 100%;
display: flex;
}
@@ -10305,7 +10306,9 @@
left: var(--bui-space-3);
margin-right: var(--bui-space-1);
color: var(--bui-fg-primary);
pointer-events: none;
flex-shrink: 0;
transition: left .2s ease-in-out;
position: absolute;
top: 50%;
transform: translateY(-50%);
@@ -10323,7 +10326,7 @@
.bui-Input {
padding: 0 var(--bui-space-3);
border-radius: var(--bui-radius-3);
border-radius: var(--bui-radius-2);
border: 1px solid var(--bui-border);
background-color: var(--bui-bg-surface-1);
font-size: var(--bui-font-size-3);
@@ -10370,8 +10373,55 @@
}
}
.bui-SearchField[data-empty] .bui-InputClear {
display: none;
.bui-SearchField {
&[data-empty] {
& .bui-InputClear {
display: none;
}
}
&[data-collapsible="true"] {
padding: 0;
transition: width .3s ease-in-out;
&[data-collapsed="false"] {
cursor: pointer;
&[data-size="medium"] {
width: 2.5rem;
height: 2.5rem;
}
&[data-size="small"] {
width: 2rem;
height: 2rem;
}
&[data-size="medium"] .bui-Input {
padding-left: 0;
&::placeholder {
opacity: 0;
}
}
&[data-size="small"] .bui-Input {
padding-left: 0;
&::placeholder {
opacity: 0;
}
}
&[data-size="small"] .bui-InputIcon {
left: var(--bui-space-2);
}
&[data-size="medium"] .bui-InputIcon {
left: 10px;
}
}
}
}
.bui-InputClear {
+4
View File
@@ -482,6 +482,9 @@ export const componentDefinitions: {
readonly root: 'bui-SearchField';
readonly clear: 'bui-InputClear';
};
readonly dataAttributes: {
readonly collapsible: readonly [true, false];
};
};
readonly Select: {
readonly classNames: {
@@ -1450,6 +1453,7 @@ export const SearchField: ForwardRefExoticComponent<
export interface SearchFieldProps
extends SearchFieldProps_2,
Omit<FieldLabelProps, 'htmlFor' | 'id'> {
collapsible?: boolean;
icon?: ReactNode | false;
placeholder?: string;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
@@ -20,6 +20,9 @@ import { Form } from 'react-aria-components';
import { Icon } from '../Icon';
import { Flex } from '../Flex';
import { FieldLabel } from '../FieldLabel';
import { ButtonIcon } from '../ButtonIcon';
import { RiCactusLine } from '@remixicon/react';
import { Button } from '../Button';
const meta = {
title: 'Forms/SearchField',
@@ -152,3 +155,37 @@ export const CustomField: Story = {
</>
),
};
export const Collapsible: Story = {
args: {
...Default.args,
collapsible: true,
},
render: args => (
<Flex direction="row" gap="4">
<SearchField {...args} size="small" />
<SearchField {...args} size="medium" />
</Flex>
),
};
export const CollapsibleNextToButtons: Story = {
args: {
...Collapsible.args,
},
render: args => (
<Flex direction="row" gap="2" style={{ width: '100%', maxWidth: '600px' }}>
<SearchField {...args} size="small" />
<ButtonIcon icon={<RiCactusLine />} size="small" variant="secondary" />
<Button size="small" variant="secondary">
Hello world
</Button>
<SearchField {...args} size="medium" />
<ButtonIcon icon={<RiCactusLine />} size="medium" variant="secondary" />
<Button size="medium" variant="secondary">
Hello world
</Button>
</Flex>
),
};
@@ -14,8 +14,55 @@
* limitations under the License.
*/
.bui-SearchField[data-empty] .bui-InputClear {
display: none;
.bui-SearchField {
&[data-empty] {
.bui-InputClear {
display: none;
}
}
&[data-collapsible='true'] {
transition: width 0.3s ease-in-out;
padding: 0;
&[data-collapsed='false'] {
cursor: pointer;
&[data-size='medium'] {
width: 2.5rem;
height: 2.5rem;
}
&[data-size='small'] {
width: 2rem;
height: 2rem;
}
&[data-size='medium'] .bui-Input {
padding-left: 0;
&::placeholder {
opacity: 0;
}
}
&[data-size='small'] .bui-Input {
padding-left: 0;
&::placeholder {
opacity: 0;
}
}
&[data-size='small'] .bui-InputIcon {
left: var(--bui-space-2);
}
&[data-size='medium'] .bui-InputIcon {
left: 10px;
}
}
}
}
.bui-InputClear {
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { forwardRef, useEffect } from 'react';
import { forwardRef, useEffect, useState } from 'react';
import {
Input,
SearchField as AriaSearchField,
@@ -40,11 +40,15 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
description,
isRequired,
placeholder = 'Search',
collapsible = false,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
...rest
} = props;
const [isCollapsed, setIsCollapsed] = useState(false);
const [shouldCollapse, setShouldCollapse] = useState(true);
useEffect(() => {
if (!label && !ariaLabel && !ariaLabelledBy) {
console.warn(
@@ -60,14 +64,32 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
},
);
const { classNames: searchFieldClassNames } = useStyles('SearchField', {
size,
});
const { classNames: searchFieldClassNames } = useStyles('SearchField', {});
// 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) => {
props.onFocusChange?.(isFocused);
if (shouldCollapse) {
if (isFocused) {
setIsCollapsed(true);
} else {
setIsCollapsed(false);
}
}
};
const handleChange = (value: string) => {
props.onChange?.(value);
if (value.length > 0) {
setShouldCollapse(false);
} else {
setShouldCollapse(true);
}
};
return (
<AriaSearchField
className={clsx(
@@ -78,6 +100,10 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
{...dataAttributes}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
data-collapsible={collapsible}
data-collapsed={isCollapsed}
onFocusChange={handleClick}
onChange={handleChange}
{...rest}
ref={ref}
>
@@ -38,4 +38,9 @@ export interface SearchFieldProps
* The placeholder text for the input
*/
placeholder?: string;
/**
* The icon to render in the input
*/
collapsible?: boolean;
}
@@ -19,6 +19,7 @@
flex-direction: column;
font-family: var(--bui-font-regular);
width: 100%;
flex-shrink: 0;
}
.bui-InputWrapper {
@@ -49,6 +50,9 @@
margin-right: var(--bui-space-1);
color: var(--bui-fg-primary);
flex-shrink: 0;
pointer-events: none;
/* To animate the icon when the input is collapsed */
transition: left 0.2s ease-in-out;
&[data-size='small'],
&[data-size='small'] svg {
@@ -67,7 +71,7 @@
display: flex;
align-items: center;
padding: 0 var(--bui-space-3);
border-radius: var(--bui-radius-3);
border-radius: var(--bui-radius-2);
border: 1px solid var(--bui-border);
background-color: var(--bui-bg-surface-1);
font-size: var(--bui-font-size-3);
@@ -202,6 +202,9 @@ export const componentDefinitions = {
root: 'bui-SearchField',
clear: 'bui-InputClear',
},
dataAttributes: {
collapsible: [true, false] as const,
},
},
Select: {
classNames: {