Migrate SearchField component from useStyles to useDefinition

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-02-25 12:30:43 +01:00
parent dbf64fc345
commit c62ab0cf7b
4 changed files with 94 additions and 65 deletions
+34 -8
View File
@@ -1793,6 +1793,9 @@ export const SearchField: ForwardRefExoticComponent<
// @public
export const SearchFieldDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-SearchField';
readonly clear: 'bui-SearchFieldClear';
@@ -1800,21 +1803,44 @@ export const SearchFieldDefinition: {
readonly input: 'bui-SearchFieldInput';
readonly inputIcon: 'bui-SearchFieldInputIcon';
};
readonly dataAttributes: {
readonly startCollapsed: readonly [true, false];
readonly size: readonly ['small', 'medium'];
readonly propDefs: {
readonly startCollapsed: {
readonly dataAttribute: true;
readonly default: false;
};
readonly size: {
readonly dataAttribute: true;
readonly default: 'small';
};
readonly className: {};
readonly icon: {};
readonly placeholder: {
readonly default: 'Search';
};
readonly label: {};
readonly description: {};
readonly secondaryLabel: {};
readonly isRequired: {};
};
};
// @public (undocumented)
export interface SearchFieldProps
extends SearchFieldProps_2,
Omit<FieldLabelProps, 'htmlFor' | 'id' | 'className'> {
export type SearchFieldOwnProps = {
icon?: ReactNode | false;
placeholder?: string;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
placeholder?: string;
startCollapsed?: boolean;
}
className?: string;
label?: FieldLabelProps['label'];
description?: FieldLabelProps['description'];
secondaryLabel?: FieldLabelProps['secondaryLabel'];
isRequired?: boolean;
};
// @public (undocumented)
export interface SearchFieldProps
extends Omit<SearchFieldProps_2, 'className' | 'isRequired' | 'description'>,
SearchFieldOwnProps {}
// @public (undocumented)
export interface SearchState {
@@ -20,53 +20,39 @@ import {
SearchField as AriaSearchField,
Button,
} from 'react-aria-components';
import clsx from 'clsx';
import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import { RiSearch2Line, RiCloseCircleLine } from '@remixicon/react';
import { useStyles } from '../../hooks/useStyles';
import { useDefinition } from '../../hooks/useDefinition';
import { SearchFieldDefinition } from './definition';
import styles from './SearchField.module.css';
import type { SearchFieldProps } from './types';
/** @public */
export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
(props, ref) => {
const {
label,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
} = props;
useEffect(() => {
if (!label && !ariaLabel && !ariaLabelledBy) {
console.warn(
'SearchField requires either a visible label, aria-label, or aria-labelledby for accessibility',
);
}
}, [label, ariaLabel, ariaLabelledBy]);
const { classNames, dataAttributes, style, cleanedProps } = useStyles(
const { ownProps, restProps, dataAttributes } = useDefinition(
SearchFieldDefinition,
{
size: 'small',
placeholder: 'Search',
startCollapsed: false,
...props,
},
props,
);
const {
className,
description,
classes,
label,
icon,
isRequired,
secondaryLabel,
placeholder,
startCollapsed,
...rest
} = cleanedProps;
description,
} = ownProps;
useEffect(() => {
if (!label && !restProps['aria-label'] && !restProps['aria-labelledby']) {
console.warn(
'SearchField requires either a visible label, aria-label, or aria-labelledby for accessibility',
);
}
}, [label, restProps['aria-label'], restProps['aria-labelledby']]);
const [isFocused, setIsFocused] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
@@ -76,7 +62,7 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
secondaryLabel || (isRequired ? 'Required' : null);
const handleFocusChange = (isFocused: boolean) => {
props.onFocusChange?.(isFocused);
restProps.onFocusChange?.(isFocused);
setIsFocused(isFocused);
};
@@ -89,17 +75,17 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
const isCollapsed = hasInputRef
? startCollapsed && !hasValue && !isFocused
: startCollapsed && !rest.value && !rest.defaultValue && !isFocused;
: startCollapsed &&
!restProps.value &&
!restProps.defaultValue &&
!isFocused;
return (
<AriaSearchField
className={clsx(classNames.root, styles[classNames.root], className)}
className={classes.root}
{...dataAttributes}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
data-collapsed={isCollapsed}
style={style}
{...rest}
{...restProps}
onFocusChange={handleFocusChange}
ref={ref}
>
@@ -109,19 +95,13 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
description={description}
/>
<div
className={clsx(
classNames.inputWrapper,
styles[classNames.inputWrapper],
)}
className={classes.inputWrapper}
data-size={dataAttributes['data-size']}
onClick={handleContainerClick}
>
{icon !== false && (
<div
className={clsx(
classNames.inputIcon,
styles[classNames.inputIcon],
)}
className={classes.inputIcon}
data-size={dataAttributes['data-size']}
aria-hidden="true"
>
@@ -130,12 +110,12 @@ export const SearchField = forwardRef<HTMLDivElement, SearchFieldProps>(
)}
<Input
ref={inputRef}
className={clsx(classNames.input, styles[classNames.input])}
className={classes.input}
{...(icon !== false && { 'data-icon': true })}
placeholder={placeholder}
/>
<Button
className={clsx(classNames.clear, styles[classNames.clear])}
className={classes.clear}
data-size={dataAttributes['data-size']}
>
<RiCloseCircleLine />
@@ -14,13 +14,16 @@
* limitations under the License.
*/
import type { ComponentDefinition } from '../../types';
import { defineComponent } from '../../hooks/useDefinition';
import type { SearchFieldOwnProps } from './types';
import styles from './SearchField.module.css';
/**
* Component definition for SearchField
* @public
*/
export const SearchFieldDefinition = {
export const SearchFieldDefinition = defineComponent<SearchFieldOwnProps>()({
styles,
classNames: {
root: 'bui-SearchField',
clear: 'bui-SearchFieldClear',
@@ -28,8 +31,15 @@ export const SearchFieldDefinition = {
input: 'bui-SearchFieldInput',
inputIcon: 'bui-SearchFieldInputIcon',
},
dataAttributes: {
startCollapsed: [true, false] as const,
size: ['small', 'medium'] as const,
propDefs: {
startCollapsed: { dataAttribute: true, default: false },
size: { dataAttribute: true, default: 'small' },
className: {},
icon: {},
placeholder: { default: 'Search' },
label: {},
description: {},
secondaryLabel: {},
isRequired: {},
},
} as const satisfies ComponentDefinition;
});
@@ -15,14 +15,12 @@
*/
import type { SearchFieldProps as AriaSearchFieldProps } from 'react-aria-components';
import { ReactNode } from 'react';
import type { ReactNode } from 'react';
import type { Breakpoint } from '../../types';
import type { FieldLabelProps } from '../FieldLabel/types';
/** @public */
export interface SearchFieldProps
extends AriaSearchFieldProps,
Omit<FieldLabelProps, 'htmlFor' | 'id' | 'className'> {
export type SearchFieldOwnProps = {
/**
* An icon to render before the input
*/
@@ -43,4 +41,19 @@ export interface SearchFieldProps
* Controls whether the SearchField starts in a collapsed state.
*/
startCollapsed?: boolean;
}
className?: string;
label?: FieldLabelProps['label'];
description?: FieldLabelProps['description'];
secondaryLabel?: FieldLabelProps['secondaryLabel'];
isRequired?: boolean;
};
/** @public */
export interface SearchFieldProps
extends Omit<
AriaSearchFieldProps,
'className' | 'isRequired' | 'description'
>,
SearchFieldOwnProps {}