Migrate TextField component from useStyles to useDefinition

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-02-25 13:28:06 +01:00
parent c62ab0cf7b
commit 9c5d262398
3 changed files with 64 additions and 66 deletions
@@ -16,48 +16,36 @@
import { forwardRef, useEffect } from 'react';
import { Input, TextField as AriaTextField } from 'react-aria-components';
import clsx from 'clsx';
import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import type { TextFieldProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import { useDefinition } from '../../hooks/useDefinition';
import { TextFieldDefinition } from './definition';
import styles from './TextField.module.css';
/** @public */
export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
(props, ref) => {
const {
label,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
} = props;
useEffect(() => {
if (!label && !ariaLabel && !ariaLabelledBy) {
console.warn(
'TextField 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(
TextFieldDefinition,
{
size: 'small',
...props,
},
props,
);
const {
className,
description,
classes,
label,
icon,
isRequired,
secondaryLabel,
placeholder,
...rest
} = cleanedProps;
description,
} = ownProps;
useEffect(() => {
if (!label && !restProps['aria-label'] && !restProps['aria-labelledby']) {
console.warn(
'TextField requires either a visible label, aria-label, or aria-labelledby for accessibility',
);
}
}, [label, restProps['aria-label'], restProps['aria-labelledby']]);
// If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required.
const secondaryLabelText =
@@ -65,12 +53,9 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
return (
<AriaTextField
className={clsx(classNames.root, styles[classNames.root], className)}
className={classes.root}
{...dataAttributes}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
style={style}
{...rest}
{...restProps}
ref={ref}
>
<FieldLabel
@@ -79,18 +64,12 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
description={description}
/>
<div
className={clsx(
classNames.inputWrapper,
styles[classNames.inputWrapper],
)}
className={classes.inputWrapper}
data-size={dataAttributes['data-size']}
>
{icon && (
<div
className={clsx(
classNames.inputIcon,
styles[classNames.inputIcon],
)}
className={classes.inputIcon}
data-size={dataAttributes['data-size']}
aria-hidden="true"
>
@@ -98,7 +77,7 @@ export const TextField = forwardRef<HTMLDivElement, TextFieldProps>(
</div>
)}
<Input
className={clsx(classNames.input, styles[classNames.input])}
className={classes.input}
{...(icon && { 'data-icon': true })}
placeholder={placeholder}
/>
@@ -14,13 +14,16 @@
* limitations under the License.
*/
import type { ComponentDefinition } from '../../types';
import { defineComponent } from '../../hooks/useDefinition';
import type { TextFieldOwnProps } from './types';
import styles from './TextField.module.css';
/**
* Component definition for TextField
* @public
*/
export const TextFieldDefinition = {
export const TextFieldDefinition = defineComponent<TextFieldOwnProps>()({
styles,
classNames: {
root: 'bui-TextField',
inputWrapper: 'bui-InputWrapper',
@@ -28,9 +31,14 @@ export const TextFieldDefinition = {
inputIcon: 'bui-InputIcon',
inputAction: 'bui-InputAction',
},
dataAttributes: {
invalid: [true, false] as const,
disabled: [true, false] as const,
size: ['small', 'medium'] as const,
propDefs: {
size: { dataAttribute: true, default: 'small' },
className: {},
icon: {},
placeholder: {},
label: {},
description: {},
secondaryLabel: {},
isRequired: {},
},
} as const satisfies ComponentDefinition;
});
+29 -18
View File
@@ -15,14 +15,40 @@
*/
import type { TextFieldProps as AriaTextFieldProps } 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 type TextFieldOwnProps = {
/**
* The size of the text field
* @defaultValue 'medium'
*/
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
className?: string;
/**
* An icon to render before the input
*/
icon?: ReactNode;
/**
* Text to display in the input when it has no value
*/
placeholder?: string;
label?: FieldLabelProps['label'];
description?: FieldLabelProps['description'];
secondaryLabel?: FieldLabelProps['secondaryLabel'];
isRequired?: boolean;
};
/** @public */
export interface TextFieldProps
extends AriaTextFieldProps,
Omit<FieldLabelProps, 'htmlFor' | 'id' | 'className'> {
extends Omit<AriaTextFieldProps, 'className' | 'isRequired' | 'description'>,
TextFieldOwnProps {
/**
* The HTML input type for the text field
*
@@ -31,19 +57,4 @@ export interface TextFieldProps
* search inputs and `PasswordField` for password inputs.
*/
type?: 'text' | 'email' | 'tel' | 'url';
/**
* An icon to render before the input
*/
icon?: ReactNode;
/**
* The size of the text field
* @defaultValue 'medium'
*/
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
/**
* Text to display in the input when it has no value
*/
placeholder?: string;
}