Add collapsible prop on SearchField
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
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'] {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user