feat(ui): migrate Checkbox component to React Aria
Migrates the Checkbox component from Base UI Components to React Aria Components.
Breaking changes:
- Props renamed to React Aria conventions (checked → isSelected, disabled → isDisabled, etc.)
- Label prop removed - use children instead
- CSS class bui-CheckboxLabel removed
- Data attribute changed from data-checked to data-selected
- Use without label is no longer supported
Migration example:
Before: <Checkbox label="Accept terms" checked={agreed} onChange={setAgreed} />
After: <Checkbox isSelected={agreed} onChange={setAgreed}>Accept terms</Checkbox>
Changes include:
- Updated TypeScript types and component implementation
- Migrated CSS to use React Aria data attributes ([data-selected], [data-disabled], etc.)
- Updated Storybook stories and documentation
- Fixed CSS structure to properly separate label wrapper and checkbox indicator styles
- Updated component definitions and API reports
- Created changeset with migration guide
Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
@@ -0,0 +1,58 @@
|
||||
---
|
||||
'@backstage/ui': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Migrated Checkbox component from Base UI to React Aria Components.
|
||||
|
||||
API changes required:
|
||||
|
||||
- `checked` → `isSelected`
|
||||
- `defaultChecked` → `defaultSelected`
|
||||
- `disabled` → `isDisabled`
|
||||
- `required` → `isRequired`
|
||||
- `label` prop removed - use `children` instead
|
||||
- CSS: `bui-CheckboxLabel` class removed
|
||||
- Data attribute: `data-checked` → `data-selected`
|
||||
- Use without label is no longer supported
|
||||
|
||||
Migration examples:
|
||||
|
||||
Before:
|
||||
|
||||
```tsx
|
||||
<Checkbox label="Accept terms" checked={agreed} onChange={setAgreed} />
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```tsx
|
||||
<Checkbox isSelected={agreed} onChange={setAgreed}>
|
||||
Accept terms
|
||||
</Checkbox>
|
||||
```
|
||||
|
||||
Before:
|
||||
|
||||
```tsx
|
||||
<Checkbox label="Option" disabled />
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```tsx
|
||||
<Checkbox isDisabled>Option</Checkbox>
|
||||
```
|
||||
|
||||
Before:
|
||||
|
||||
```tsx
|
||||
<Checkbox />
|
||||
```
|
||||
|
||||
After:
|
||||
|
||||
```tsx
|
||||
<Checkbox>
|
||||
<VisuallyHidden>Accessible label</VisuallyHidden>
|
||||
</Checkbox>
|
||||
```
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-mui-to-bui': patch
|
||||
---
|
||||
|
||||
Updated BUI checkbox preview example to align with new component API.
|
||||
@@ -5,31 +5,32 @@ import {
|
||||
} from '@/utils/propDefs';
|
||||
|
||||
export const checkboxPropDefs: Record<string, PropDef> = {
|
||||
label: {
|
||||
type: 'string',
|
||||
responsive: false,
|
||||
},
|
||||
defaultChecked: {
|
||||
children: {
|
||||
type: 'enum',
|
||||
values: ['boolean', "'indeterminate'"],
|
||||
values: ['React.ReactNode'],
|
||||
responsive: false,
|
||||
},
|
||||
checked: {
|
||||
type: 'enum',
|
||||
values: ['boolean', "'indeterminate'"],
|
||||
responsive: false,
|
||||
},
|
||||
onChange: {
|
||||
type: 'enum',
|
||||
values: ["(checked: boolean | 'indeterminate') => void"],
|
||||
responsive: false,
|
||||
},
|
||||
disabled: {
|
||||
isSelected: {
|
||||
type: 'enum',
|
||||
values: ['boolean'],
|
||||
responsive: false,
|
||||
},
|
||||
required: {
|
||||
defaultSelected: {
|
||||
type: 'enum',
|
||||
values: ['boolean'],
|
||||
responsive: false,
|
||||
},
|
||||
onChange: {
|
||||
type: 'enum',
|
||||
values: ['(isSelected: boolean) => void'],
|
||||
responsive: false,
|
||||
},
|
||||
isDisabled: {
|
||||
type: 'enum',
|
||||
values: ['boolean'],
|
||||
responsive: false,
|
||||
},
|
||||
isRequired: {
|
||||
type: 'enum',
|
||||
values: ['boolean'],
|
||||
responsive: false,
|
||||
@@ -48,13 +49,13 @@ export const checkboxPropDefs: Record<string, PropDef> = {
|
||||
|
||||
export const checkboxUsageSnippet = `import { Checkbox } from '@backstage/ui';
|
||||
|
||||
<Checkbox />`;
|
||||
<Checkbox>Accept terms</Checkbox>`;
|
||||
|
||||
export const checkboxDefaultSnippet = `<Checkbox label="Accept terms and conditions" />`;
|
||||
export const checkboxDefaultSnippet = `<Checkbox>Accept terms and conditions</Checkbox>`;
|
||||
|
||||
export const checkboxVariantsSnippet = `<Inline alignY="center">
|
||||
<Checkbox />
|
||||
<Checkbox checked />
|
||||
<Checkbox label="Checkbox" />
|
||||
<Checkbox label="Checkbox" checked />
|
||||
</Inline>`;
|
||||
export const checkboxVariantsSnippet = `<Flex direction="column" gap="2">
|
||||
<Checkbox>Unchecked</Checkbox>
|
||||
<Checkbox isSelected>Checked</Checkbox>
|
||||
<Checkbox isDisabled>Disabled</Checkbox>
|
||||
<Checkbox isSelected isDisabled>Checked & Disabled</Checkbox>
|
||||
</Flex>`;
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
import { Avatar as Avatar_2 } from '@base-ui-components/react/avatar';
|
||||
import { ButtonProps as ButtonProps_2 } from 'react-aria-components';
|
||||
import { CellProps as CellProps_2 } from 'react-aria-components';
|
||||
import { CheckboxProps as CheckboxProps_2 } from 'react-aria-components';
|
||||
import { Collapsible as Collapsible_2 } from '@base-ui-components/react/collapsible';
|
||||
import { ColumnProps as ColumnProps_2 } from 'react-aria-components';
|
||||
import { ComponentProps } from 'react';
|
||||
@@ -270,31 +271,13 @@ export interface CellProps extends CellProps_2 {
|
||||
|
||||
// @public (undocumented)
|
||||
export const Checkbox: ForwardRefExoticComponent<
|
||||
CheckboxProps & RefAttributes<HTMLButtonElement>
|
||||
CheckboxProps & RefAttributes<HTMLLabelElement>
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface CheckboxProps {
|
||||
export interface CheckboxProps extends CheckboxProps_2 {
|
||||
// (undocumented)
|
||||
checked?: boolean;
|
||||
// (undocumented)
|
||||
className?: string;
|
||||
// (undocumented)
|
||||
defaultChecked?: boolean;
|
||||
// (undocumented)
|
||||
disabled?: boolean;
|
||||
// (undocumented)
|
||||
label?: string;
|
||||
// (undocumented)
|
||||
name?: string;
|
||||
// (undocumented)
|
||||
onChange?: (checked: boolean) => void;
|
||||
// (undocumented)
|
||||
required?: boolean;
|
||||
// (undocumented)
|
||||
style?: React.CSSProperties;
|
||||
// (undocumented)
|
||||
value?: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
// @public
|
||||
@@ -431,12 +414,11 @@ export const componentDefinitions: {
|
||||
};
|
||||
readonly Checkbox: {
|
||||
readonly classNames: {
|
||||
readonly root: 'bui-CheckboxRoot';
|
||||
readonly label: 'bui-CheckboxLabel';
|
||||
readonly root: 'bui-Checkbox';
|
||||
readonly indicator: 'bui-CheckboxIndicator';
|
||||
};
|
||||
readonly dataAttributes: {
|
||||
readonly checked: readonly [true, false];
|
||||
readonly selected: readonly [true, false];
|
||||
};
|
||||
};
|
||||
readonly Collapsible: {
|
||||
|
||||
@@ -17,35 +17,7 @@
|
||||
@layer tokens, base, components, utilities;
|
||||
|
||||
@layer components {
|
||||
.bui-CheckboxRoot {
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border);
|
||||
cursor: pointer;
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bui-CheckboxRoot:focus-visible {
|
||||
transition: none;
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.bui-CheckboxRoot[data-checked] {
|
||||
background-color: var(--bui-bg-solid);
|
||||
box-shadow: none;
|
||||
color: var(--bui-fg-solid);
|
||||
}
|
||||
|
||||
.bui-CheckboxLabel {
|
||||
.bui-Checkbox {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
@@ -55,18 +27,48 @@
|
||||
font-weight: var(--bui-font-weight-regular);
|
||||
color: var(--bui-fg-primary);
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
& .bui-CheckboxRoot:not([data-checked]) {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-hover);
|
||||
.bui-Checkbox[data-disabled] {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.bui-CheckboxIndicator {
|
||||
border: none;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border);
|
||||
border-radius: 2px;
|
||||
transition: background-color 0.2s ease-in-out;
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
padding: 0;
|
||||
flex-shrink: 0;
|
||||
color: var(--bui-fg-solid);
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
& {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bui-CheckboxIndicator {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bui-fg-solid);
|
||||
.bui-Checkbox[data-focus-visible] .bui-CheckboxIndicator {
|
||||
transition: none;
|
||||
outline: 2px solid var(--bui-ring);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.bui-Checkbox[data-selected] .bui-CheckboxIndicator {
|
||||
background-color: var(--bui-bg-solid);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.bui-Checkbox[data-hovered]:not([data-selected]) .bui-CheckboxIndicator {
|
||||
box-shadow: inset 0 0 0 1px var(--bui-border-hover);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import type { Meta, StoryObj } from '@storybook/react-vite';
|
||||
import { Checkbox } from './Checkbox';
|
||||
import { Flex } from '../Flex';
|
||||
import { Text } from '../Text';
|
||||
|
||||
const meta = {
|
||||
title: 'Backstage UI/Checkbox',
|
||||
@@ -29,31 +28,20 @@ type Story = StoryObj<typeof meta>;
|
||||
|
||||
export const Default: Story = {
|
||||
args: {
|
||||
label: 'Accept terms and conditions',
|
||||
children: 'Accept terms and conditions',
|
||||
},
|
||||
};
|
||||
|
||||
export const AllVariants: Story = {
|
||||
...Default,
|
||||
render: () => (
|
||||
<Flex align="center">
|
||||
<Checkbox />
|
||||
<Checkbox checked />
|
||||
<Checkbox label="Checkbox" />
|
||||
<Checkbox label="Checkbox" checked />
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
export const Playground: Story = {
|
||||
render: () => (
|
||||
<Flex>
|
||||
<Text>All variants</Text>
|
||||
<Flex align="center">
|
||||
<Checkbox />
|
||||
<Checkbox checked />
|
||||
<Checkbox label="Checkbox" />
|
||||
<Checkbox label="Checkbox" checked />
|
||||
</Flex>
|
||||
<Flex direction="column" gap="2">
|
||||
<Checkbox>Unchecked</Checkbox>
|
||||
<Checkbox isSelected>Checked</Checkbox>
|
||||
<Checkbox isDisabled>Disabled</Checkbox>
|
||||
<Checkbox isSelected isDisabled>
|
||||
Checked & Disabled
|
||||
</Checkbox>
|
||||
</Flex>
|
||||
),
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { forwardRef } from 'react';
|
||||
import { Checkbox as CheckboxPrimitive } from '@base-ui-components/react/checkbox';
|
||||
import { Checkbox as RACheckbox } from 'react-aria-components';
|
||||
import type { CheckboxProps } from './types';
|
||||
import { useStyles } from '../../hooks/useStyles';
|
||||
import clsx from 'clsx';
|
||||
@@ -23,33 +23,24 @@ import styles from './Checkbox.module.css';
|
||||
import { RiCheckLine } from '@remixicon/react';
|
||||
|
||||
/** @public */
|
||||
export const Checkbox = forwardRef<HTMLButtonElement, CheckboxProps>(
|
||||
export const Checkbox = forwardRef<HTMLLabelElement, CheckboxProps>(
|
||||
(props, ref) => {
|
||||
const { classNames, cleanedProps } = useStyles('Checkbox', props);
|
||||
const { label, onChange, className, ...rest } = cleanedProps;
|
||||
const { classNames } = useStyles('Checkbox');
|
||||
const { className, children, ...rest } = props;
|
||||
|
||||
const checkboxElement = (
|
||||
<CheckboxPrimitive.Root
|
||||
return (
|
||||
<RACheckbox
|
||||
ref={ref}
|
||||
className={clsx(classNames.root, styles[classNames.root], className)}
|
||||
onCheckedChange={onChange}
|
||||
{...rest}
|
||||
>
|
||||
<CheckboxPrimitive.Indicator
|
||||
<div
|
||||
className={clsx(classNames.indicator, styles[classNames.indicator])}
|
||||
>
|
||||
<RiCheckLine size={12} />
|
||||
</CheckboxPrimitive.Indicator>
|
||||
</CheckboxPrimitive.Root>
|
||||
);
|
||||
|
||||
return label ? (
|
||||
<label className={clsx(classNames.label, styles[classNames.label])}>
|
||||
{checkboxElement}
|
||||
{label}
|
||||
</label>
|
||||
) : (
|
||||
checkboxElement
|
||||
</div>
|
||||
{children}
|
||||
</RACheckbox>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
@@ -13,17 +13,9 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { CheckboxProps as RACheckboxProps } from 'react-aria-components';
|
||||
|
||||
/** @public */
|
||||
export interface CheckboxProps {
|
||||
label?: string;
|
||||
defaultChecked?: boolean;
|
||||
checked?: boolean;
|
||||
onChange?: (checked: boolean) => void;
|
||||
disabled?: boolean;
|
||||
required?: boolean;
|
||||
className?: string;
|
||||
name?: string;
|
||||
value?: string;
|
||||
style?: React.CSSProperties;
|
||||
export interface CheckboxProps extends RACheckboxProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -89,12 +89,11 @@ export const componentDefinitions = {
|
||||
},
|
||||
Checkbox: {
|
||||
classNames: {
|
||||
root: 'bui-CheckboxRoot',
|
||||
label: 'bui-CheckboxLabel',
|
||||
root: 'bui-Checkbox',
|
||||
indicator: 'bui-CheckboxIndicator',
|
||||
},
|
||||
dataAttributes: {
|
||||
checked: [true, false] as const,
|
||||
selected: [true, false] as const,
|
||||
},
|
||||
},
|
||||
Collapsible: {
|
||||
|
||||
@@ -92,7 +92,7 @@ export function BuiThemePreview({ mode, styleObject }: IsolatedPreviewProps) {
|
||||
{ value: 'option3', label: 'Option 3' },
|
||||
]}
|
||||
/>
|
||||
<Checkbox label="Checkbox Option" />
|
||||
<Checkbox>Checkbox Option</Checkbox>
|
||||
<RadioGroup label="Radio Group" orientation="horizontal">
|
||||
<Radio value="option-1">Option 1</Radio>
|
||||
<Radio value="option-2">Option 2</Radio>
|
||||
|
||||
Reference in New Issue
Block a user