Migrate Select component from useStyles to useDefinition

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-02-26 11:22:05 +01:00
parent 2e1eddf6c6
commit 8288e5ac85
7 changed files with 175 additions and 117 deletions
+30 -17
View File
@@ -1962,36 +1962,49 @@ export const Select: ForwardRefExoticComponent<
// @public
export const SelectDefinition: {
readonly styles: {
readonly [key: string]: string;
};
readonly classNames: {
readonly root: 'bui-Select';
readonly popover: 'bui-SelectPopover';
readonly trigger: 'bui-SelectTrigger';
readonly chevron: 'bui-SelectTriggerChevron';
readonly value: 'bui-SelectValue';
readonly list: 'bui-SelectList';
readonly item: 'bui-SelectItem';
readonly itemIndicator: 'bui-SelectItemIndicator';
readonly itemLabel: 'bui-SelectItemLabel';
readonly searchWrapper: 'bui-SelectSearchWrapper';
readonly search: 'bui-SelectSearch';
readonly searchClear: 'bui-SelectSearchClear';
readonly noResults: 'bui-SelectNoResults';
};
readonly dataAttributes: {
readonly size: readonly ['small', 'medium'];
readonly propDefs: {
readonly icon: {};
readonly size: {
readonly dataAttribute: true;
readonly default: 'small';
};
readonly options: {};
readonly searchable: {};
readonly searchPlaceholder: {};
readonly label: {};
readonly secondaryLabel: {};
readonly description: {};
readonly isRequired: {};
readonly className: {};
};
};
// @public (undocumented)
export interface SelectProps<T extends 'single' | 'multiple'>
extends SelectProps_2<Option_2, T>,
Omit<FieldLabelProps, 'htmlFor' | 'id' | 'className'> {
export type SelectOwnProps = {
icon?: ReactNode;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
options?: Array<Option_2>;
searchable?: boolean;
searchPlaceholder?: string;
label?: FieldLabelProps['label'];
secondaryLabel?: FieldLabelProps['secondaryLabel'];
description?: FieldLabelProps['description'];
isRequired?: boolean;
className?: string;
};
// @public (undocumented)
export interface SelectProps<T extends 'single' | 'multiple'>
extends SelectOwnProps,
Omit<SelectProps_2<Option_2, T>, keyof SelectOwnProps> {
selectionMode?: T;
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
}
// @public (undocumented)
+12 -19
View File
@@ -18,13 +18,11 @@ import { forwardRef, useEffect } from 'react';
import { Select as AriaSelect, Popover } from 'react-aria-components';
import clsx from 'clsx';
import { SelectProps } from './types';
import { useStyles } from '../../hooks/useStyles';
import { useDefinition } from '../../hooks/useDefinition';
import { SelectDefinition } from './definition';
import { PopoverDefinition } from '../Popover/definition';
import { FieldLabel } from '../FieldLabel';
import { FieldError } from '../FieldError';
import styles from './Select.module.css';
import stylesPopover from '../Popover/Popover.module.css';
import { SelectTrigger } from './SelectTrigger';
import { SelectContent } from './SelectContent';
@@ -33,30 +31,28 @@ export const Select = forwardRef<
HTMLDivElement,
SelectProps<'single' | 'multiple'>
>((props, ref) => {
const { classNames: popoverClassNames } = useStyles(PopoverDefinition);
const { classNames, dataAttributes, cleanedProps } = useStyles(
const { ownProps, restProps, dataAttributes } = useDefinition(
SelectDefinition,
{
size: 'small',
placeholder: 'Select an option',
...props,
},
);
const {
className,
classes,
label,
description,
options,
icon,
searchable,
searchPlaceholder,
'aria-label': ariaLabel,
'aria-labelledby': ariaLabelledBy,
isRequired,
secondaryLabel,
...rest
} = cleanedProps;
} = ownProps;
const ariaLabel = restProps['aria-label'];
const ariaLabelledBy = restProps['aria-labelledby'];
useEffect(() => {
if (!label && !ariaLabel && !ariaLabelledBy) {
@@ -70,12 +66,10 @@ export const Select = forwardRef<
return (
<AriaSelect
className={clsx(classNames.root, styles[classNames.root], className)}
className={classes.root}
{...dataAttributes}
ref={ref}
aria-label={ariaLabel}
aria-labelledby={ariaLabelledBy}
{...rest}
{...restProps}
>
<FieldLabel
label={label}
@@ -86,10 +80,9 @@ export const Select = forwardRef<
<FieldError />
<Popover
className={clsx(
popoverClassNames.root,
stylesPopover[popoverClassNames.root],
classNames.popover,
styles[classNames.popover],
PopoverDefinition.classNames.root,
PopoverDefinition.styles[PopoverDefinition.classNames.root],
classes.popover,
)}
{...dataAttributes}
>
@@ -22,11 +22,9 @@ import {
} from 'react-aria-components';
import { useFilter } from 'react-aria';
import { RiCloseCircleLine } from '@remixicon/react';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import { SelectDefinition } from './definition';
import { useDefinition } from '../../hooks/useDefinition';
import { SelectContentDefinition } from './definition';
import { SelectListBox } from './SelectListBox';
import styles from './Select.module.css';
import type { Option } from './types';
interface SelectContentProps {
@@ -35,13 +33,10 @@ interface SelectContentProps {
options?: Array<Option>;
}
export function SelectContent({
searchable,
searchPlaceholder = 'Search...',
options,
}: SelectContentProps) {
export function SelectContent(props: SelectContentProps) {
const { contains } = useFilter({ sensitivity: 'base' });
const { classNames } = useStyles(SelectDefinition);
const { ownProps } = useDefinition(SelectContentDefinition, props);
const { classes, searchable, searchPlaceholder, options } = ownProps;
if (!searchable) {
return <SelectListBox options={options} />;
@@ -51,22 +46,11 @@ export function SelectContent({
<Autocomplete filter={contains}>
<SearchField
autoFocus
className={clsx(
classNames.searchWrapper,
styles[classNames.searchWrapper],
)}
className={classes.root}
aria-label={searchPlaceholder}
>
<Input
placeholder={searchPlaceholder}
className={clsx(classNames.search, styles[classNames.search])}
/>
<Button
className={clsx(
classNames.searchClear,
styles[classNames.searchClear],
)}
>
<Input placeholder={searchPlaceholder} className={classes.search} />
<Button className={classes.searchClear}>
<RiCloseCircleLine />
</Button>
</SearchField>
@@ -16,10 +16,8 @@
import { ListBox, ListBoxItem, Text } from 'react-aria-components';
import { RiCheckLine } from '@remixicon/react';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import { SelectDefinition } from './definition';
import styles from './Select.module.css';
import { useDefinition } from '../../hooks/useDefinition';
import { SelectListBoxDefinition } from './definition';
import type { Option } from './types';
interface SelectListBoxProps {
@@ -27,42 +25,30 @@ interface SelectListBoxProps {
}
const NoResults = () => {
const { classNames } = useStyles(SelectDefinition);
const { ownProps } = useDefinition(SelectListBoxDefinition, {});
const { classes } = ownProps;
return (
<div className={clsx(classNames.noResults, styles[classNames.noResults])}>
No results found.
</div>
);
return <div className={classes.noResults}>No results found.</div>;
};
export function SelectListBox({ options, ...props }: SelectListBoxProps) {
const { classNames } = useStyles(SelectDefinition, props);
export function SelectListBox(props: SelectListBoxProps) {
const { ownProps } = useDefinition(SelectListBoxDefinition, props);
const { classes, options } = ownProps;
return (
<ListBox
className={clsx(classNames.list, styles[classNames.list])}
renderEmptyState={() => <NoResults />}
>
<ListBox className={classes.root} renderEmptyState={() => <NoResults />}>
{options?.map(option => (
<ListBoxItem
key={option.value}
id={option.value}
textValue={option.label}
className={clsx(classNames.item, styles[classNames.item])}
className={classes.item}
isDisabled={option.disabled}
>
<div
className={clsx(
classNames.itemIndicator,
styles[classNames.itemIndicator],
)}
>
<div className={classes.itemIndicator}>
<RiCheckLine />
</div>
<Text
slot="label"
className={clsx(classNames.itemLabel, styles[classNames.itemLabel])}
>
<Text slot="label" className={classes.itemLabel}>
{option.label}
</Text>
</ListBoxItem>
@@ -17,25 +17,22 @@
import { ReactNode } from 'react';
import { Button, SelectValue } from 'react-aria-components';
import { RiArrowDownSLine } from '@remixicon/react';
import clsx from 'clsx';
import { useStyles } from '../../hooks/useStyles';
import { SelectDefinition } from './definition';
import styles from './Select.module.css';
import { useDefinition } from '../../hooks/useDefinition';
import { SelectTriggerDefinition } from './definition';
interface SelectTriggerProps {
icon?: ReactNode;
}
export function SelectTrigger({ icon }: SelectTriggerProps) {
const { classNames } = useStyles(SelectDefinition);
export function SelectTrigger(props: SelectTriggerProps) {
const { ownProps } = useDefinition(SelectTriggerDefinition, props);
const { classes, icon } = ownProps;
return (
<Button className={clsx(classNames.trigger, styles[classNames.trigger])}>
<Button className={classes.root}>
{icon}
<SelectValue
className={clsx(classNames.value, styles[classNames.value])}
/>
<div className={clsx(classNames.chevron, styles[classNames.chevron])}>
<SelectValue className={classes.value} />
<div className={classes.chevron}>
<RiArrowDownSLine aria-hidden="true" />
</div>
</Button>
+92 -16
View File
@@ -14,29 +14,105 @@
* limitations under the License.
*/
import type { ComponentDefinition } from '../../types';
import { defineComponent } from '../../hooks/useDefinition';
import type { SelectOwnProps } from './types';
import styles from './Select.module.css';
/**
* Component definition for Select
* @public
*/
export const SelectDefinition = {
export const SelectDefinition = defineComponent<SelectOwnProps>()({
styles,
classNames: {
root: 'bui-Select',
popover: 'bui-SelectPopover',
trigger: 'bui-SelectTrigger',
chevron: 'bui-SelectTriggerChevron',
value: 'bui-SelectValue',
list: 'bui-SelectList',
item: 'bui-SelectItem',
itemIndicator: 'bui-SelectItemIndicator',
itemLabel: 'bui-SelectItemLabel',
searchWrapper: 'bui-SelectSearchWrapper',
search: 'bui-SelectSearch',
searchClear: 'bui-SelectSearchClear',
noResults: 'bui-SelectNoResults',
},
dataAttributes: {
size: ['small', 'medium'] as const,
propDefs: {
icon: {},
size: { dataAttribute: true, default: 'small' },
options: {},
searchable: {},
searchPlaceholder: {},
label: {},
secondaryLabel: {},
description: {},
isRequired: {},
className: {},
},
} as const satisfies ComponentDefinition;
});
/** @internal */
interface SelectTriggerOwnProps {
icon?: SelectOwnProps['icon'];
}
/**
* Component definition for SelectTrigger
* @internal
*/
export const SelectTriggerDefinition = defineComponent<SelectTriggerOwnProps>()(
{
styles,
classNames: {
root: 'bui-SelectTrigger',
chevron: 'bui-SelectTriggerChevron',
value: 'bui-SelectValue',
},
propDefs: {
icon: {},
},
},
);
/** @internal */
interface SelectContentOwnProps {
searchable?: boolean;
searchPlaceholder?: string;
options?: SelectOwnProps['options'];
}
/**
* Component definition for SelectContent
* @internal
*/
export const SelectContentDefinition = defineComponent<SelectContentOwnProps>()(
{
styles,
classNames: {
root: 'bui-SelectSearchWrapper',
search: 'bui-SelectSearch',
searchClear: 'bui-SelectSearchClear',
},
propDefs: {
searchable: {},
searchPlaceholder: { default: 'Search...' },
options: {},
},
},
);
/** @internal */
interface SelectListBoxOwnProps {
options?: SelectOwnProps['options'];
}
/**
* Component definition for SelectListBox
* @internal
*/
export const SelectListBoxDefinition = defineComponent<SelectListBoxOwnProps>()(
{
styles,
classNames: {
root: 'bui-SelectList',
item: 'bui-SelectItem',
itemIndicator: 'bui-SelectItemIndicator',
itemLabel: 'bui-SelectItemLabel',
noResults: 'bui-SelectNoResults',
},
propDefs: {
options: {},
},
},
);
+12 -3
View File
@@ -23,9 +23,7 @@ import type { FieldLabelProps } from '../FieldLabel/types';
export type Option = { value: string; label: string; disabled?: boolean };
/** @public */
export interface SelectProps<T extends 'single' | 'multiple'>
extends AriaSelectProps<Option, T>,
Omit<FieldLabelProps, 'htmlFor' | 'id' | 'className'> {
export type SelectOwnProps = {
/**
* An icon to render before the input
*/
@@ -55,6 +53,17 @@ export interface SelectProps<T extends 'single' | 'multiple'>
*/
searchPlaceholder?: string;
label?: FieldLabelProps['label'];
secondaryLabel?: FieldLabelProps['secondaryLabel'];
description?: FieldLabelProps['description'];
isRequired?: boolean;
className?: string;
};
/** @public */
export interface SelectProps<T extends 'single' | 'multiple'>
extends SelectOwnProps,
Omit<AriaSelectProps<Option, T>, keyof SelectOwnProps> {
/**
* Selection mode, single or multiple
* @defaultvalue 'single'