diff --git a/.changeset/empty-poets-give.md b/.changeset/empty-poets-give.md new file mode 100644 index 0000000000..2eb63c2ed5 --- /dev/null +++ b/.changeset/empty-poets-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': patch +--- + +Add new `RadioGroup` + `Radio` component to Canon diff --git a/canon-docs/src/content/components/radio-group.mdx b/canon-docs/src/content/components/radio-group.mdx new file mode 100644 index 0000000000..703ac6b8dd --- /dev/null +++ b/canon-docs/src/content/components/radio-group.mdx @@ -0,0 +1,98 @@ +import { PropsTable } from '@/components/PropsTable'; +import { Snippet } from '@/components/Snippet'; +import { RadioGroupSnippet } from '@/snippets/stories-snippets'; +import { + radioGroupPropDefs, + radioGroupUsageSnippet, + radioGroupDefaultSnippet, + radioGroupDescriptionSnippet, + radioGroupHorizontalSnippet, + radioGroupDisabledSnippet, + radioGroupDisabledSingleSnippet, + radioGroupValidationSnippet, + radioGroupReadOnlySnippet, +} from './radio-group.props'; +import { ComponentInfos } from '@/components/ComponentInfos'; + +# RadioGroup + +A radio group allows a user to select a single item from a list of mutually exclusive options. + +} + code={radioGroupDefaultSnippet} +/> + + + +## API reference + + + +## Examples + +### Horizontal + +Here's a simple TextField with a description. + +} + code={radioGroupHorizontalSnippet} +/> + +### Disabled + +You can disable the entire radio group by adding the `isDisabled` prop to the `RadioGroup` component. + +} + code={radioGroupDisabledSnippet} +/> + +### Disabled Single radio + +You can disable a single radio by adding the `isDisabled` prop to the `Radio` component. + +} + code={radioGroupDisabledSingleSnippet} +/> + +### Validation + +Here's an example of a radio group with errors. + +} + code={radioGroupValidationSnippet} +/> + +### Read only + +You can make the radio group read only by adding the `isReadOnly` prop to the `RadioGroup` component. + +} + code={radioGroupReadOnlySnippet} +/> diff --git a/canon-docs/src/content/components/radio-group.props.ts b/canon-docs/src/content/components/radio-group.props.ts new file mode 100644 index 0000000000..c016be5ac6 --- /dev/null +++ b/canon-docs/src/content/components/radio-group.props.ts @@ -0,0 +1,64 @@ +import { + classNamePropDefs, + stylePropDefs, + type PropDef, +} from '@/utils/propDefs'; + +export const radioGroupPropDefs: Record = { + size: { + type: 'enum', + values: ['small', 'medium'], + default: 'small', + responsive: true, + }, + label: { + type: 'string', + }, + icon: { + type: 'enum', + values: ['ReactNode'], + }, + description: { + type: 'string', + }, + name: { + type: 'string', + required: true, + }, + ...classNamePropDefs, + ...stylePropDefs, +}; + +export const radioGroupUsageSnippet = `import { RadioGroup } from '@backstage/canon'; + +`; + +export const radioGroupDefaultSnippet = ``; + +export const radioGroupDescriptionSnippet = ``; + +export const radioGroupHorizontalSnippet = ``; + +export const radioGroupDisabledSnippet = ` + Bulbasaur + Charmander + Squirtle +`; + +export const radioGroupDisabledSingleSnippet = ` + Bulbasaur + Charmander + Squirtle +`; + +export const radioGroupValidationSnippet = ` (value === \'charmander\' ? \'Nice try!\' : null)> + Bulbasaur + Charmander + Squirtle +`; + +export const radioGroupReadOnlySnippet = ` + Bulbasaur + Charmander + Squirtle +`; diff --git a/canon-docs/src/snippets/stories-snippets.tsx b/canon-docs/src/snippets/stories-snippets.tsx index e5a7464b43..ef25a37900 100644 --- a/canon-docs/src/snippets/stories-snippets.tsx +++ b/canon-docs/src/snippets/stories-snippets.tsx @@ -18,6 +18,7 @@ import * as MenuStories from '../../../packages/canon/src/components/Menu/Menu.s import * as LinkStories from '../../../packages/canon/src/components/Link/Link.stories'; import * as AvatarStories from '../../../packages/canon/src/components/Avatar/Avatar.stories'; import * as CollapsibleStories from '../../../packages/canon/src/components/Collapsible/Collapsible.stories'; +import * as RadioGroupStories from '../../../packages/canon/src/components/RadioGroup/RadioGroup.stories'; import * as TabsStories from '../../../packages/canon/src/components/Tabs/Tabs.stories'; import * as SwitchStories from '../../../packages/canon/src/components/Switch/Switch.stories'; @@ -197,3 +198,14 @@ export const SwitchSnippet = ({ return StoryComponent ? : null; }; + +export const RadioGroupSnippet = ({ + story, +}: { + story: keyof typeof RadioGroupStories; +}) => { + const stories = composeStories(RadioGroupStories); + const StoryComponent = stories[story as keyof typeof stories]; + + return StoryComponent ? : null; +}; diff --git a/canon-docs/src/utils/data.ts b/canon-docs/src/utils/data.ts index a741ad5160..92f0fdbf87 100644 --- a/canon-docs/src/utils/data.ts +++ b/canon-docs/src/utils/data.ts @@ -116,6 +116,11 @@ export const components: Page[] = [ slug: 'menu', status: 'alpha', }, + { + title: 'RadioGroup', + slug: 'radio-group', + status: 'alpha', + }, { title: 'Select', slug: 'select', diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 9f32da5d66..806ee771d2 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9496,6 +9496,13 @@ min-width: 10.5rem; } +.canon-FieldError { + color: var(--canon-fg-danger); + font-size: var(--canon-font-size-2); + font-weight: var(--canon-font-weight-regular); + margin-top: var(--canon-space-2); +} + .canon-FieldLabel { margin-bottom: var(--canon-space-3); gap: var(--canon-space-1); @@ -9815,6 +9822,96 @@ overflow: hidden; } +.canon-RadioGroup { + color: var(--canon-fg-primary); + flex-direction: column; + display: flex; +} + +.canon-RadioGroup[data-orientation="horizontal"] .canon-RadioGroupContent { + gap: var(--canon-space-4); + flex-direction: row; +} + +.canon-RadioGroupContent { + gap: var(--canon-space-2); + flex-direction: column; + display: flex; +} + +.canon-Radio { + align-items: center; + gap: var(--canon-space-2); + font-size: var(--canon-font-size-2); + color: var(--canon-fg-primary); + forced-color-adjust: none; + display: flex; + position: relative; + + &:before { + content: ""; + box-sizing: border-box; + border: .125rem solid var(--canon-border); + background: var(--canon-gray-1); + border-radius: var(--canon-radius-full); + width: 1rem; + height: 1rem; + transition: all .2s; + display: block; + } + + &[data-pressed]:before { + border-color: var(--canon-border); + } + + &[data-selected] { + &:before { + border-color: var(--canon-bg-solid); + border-width: .25rem; + } + + &[data-pressed]:before { + border-color: var(--canon-bg-solid); + } + } + + &[data-focus-visible]:before { + outline: 2px solid var(--canon-ring); + outline-offset: 2px; + } + + &[data-disabled] { + cursor: not-allowed; + color: var(--canon-fg-disabled); + + &:before { + border-color: var(--canon-border-disabled); + background: var(--canon-bg-disabled); + } + + &[data-selected]:before { + border-color: var(--canon-border-disabled); + } + } + + &[data-invalid]:before, &[data-invalid][data-selected]:before { + border-color: var(--canon-border-danger); + } + + &[data-disabled][data-invalid] { + color: var(--canon-fg-disabled); + + &:before { + border-color: var(--canon-border-disabled); + background: var(--canon-bg-disabled); + } + + &[data-selected]:before { + border-color: var(--canon-border-disabled); + } + } +} + .canon-TableRoot { caption-side: bottom; border-collapse: collapse; @@ -10119,13 +10216,6 @@ border: 1px solid var(--canon-border-disabled); } -.canon-TextFieldError { - color: var(--canon-fg-danger); - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - margin-top: var(--canon-space-2); -} - .canon-TooltipPopup { box-sizing: border-box; transform-origin: var(--transform-origin); diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index bdab503f61..1b1e465a61 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -20,6 +20,8 @@ import { HTMLAttributes } from 'react'; import { JSX as JSX_2 } from 'react/jsx-runtime'; import { LinkProps as LinkProps_2 } from 'react-aria-components'; import { Menu as Menu_2 } from '@base-ui-components/react/menu'; +import type { RadioGroupProps as RadioGroupProps_2 } from 'react-aria-components'; +import type { RadioProps as RadioProps_2 } from 'react-aria-components'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RefAttributes } from 'react'; @@ -1032,6 +1034,27 @@ export type PositionProps = GetPropDefTypes; // @public (undocumented) export type PropDef = RegularPropDef | ResponsivePropDef; +// @public (undocumented) +export const Radio: ForwardRefExoticComponent< + RadioProps & RefAttributes +>; + +// @public (undocumented) +export const RadioGroup: ForwardRefExoticComponent< + RadioGroupProps & RefAttributes +>; + +// @public (undocumented) +export interface RadioGroupProps + extends Omit, + Omit { + // (undocumented) + children?: ReactNode; +} + +// @public (undocumented) +export interface RadioProps extends RadioProps_2 {} + // @public (undocumented) export type ReactNodePropDef = { type: 'ReactNode'; diff --git a/packages/canon/src/components/FieldError/FieldError.stories.tsx b/packages/canon/src/components/FieldError/FieldError.stories.tsx new file mode 100644 index 0000000000..39714d16d2 --- /dev/null +++ b/packages/canon/src/components/FieldError/FieldError.stories.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import { TextField, Input, Form } from 'react-aria-components'; +import { FieldError } from './FieldError'; + +const meta = { + title: 'Forms/FieldError', + component: FieldError, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +// Show error with server validation using Form component +export const WithServerValidation: Story = { + render: () => ( +
+ + + + +
+ ), +}; + +// Show error using children +export const WithCustomMessage: Story = { + render: () => ( + + + This is a custom error message. + + ), +}; + +// Show error with render prop function +export const WithRenderProp: Story = { + render: () => ( + 'This field is invalid'} + style={{ + display: 'flex', + flexDirection: 'column', + alignItems: 'flex-start', + }} + > + + + {({ validationErrors }) => + validationErrors.length > 0 ? validationErrors[0] : 'Field is invalid' + } + + + ), +}; diff --git a/packages/canon/src/components/FieldError/FieldError.styles.css b/packages/canon/src/components/FieldError/FieldError.styles.css new file mode 100644 index 0000000000..816eb3c35e --- /dev/null +++ b/packages/canon/src/components/FieldError/FieldError.styles.css @@ -0,0 +1,6 @@ +.canon-FieldError { + color: var(--canon-fg-danger); + font-size: var(--canon-font-size-2); + font-weight: var(--canon-font-weight-regular); + margin-top: var(--canon-space-2); +} diff --git a/packages/canon/src/components/FieldError/FieldError.tsx b/packages/canon/src/components/FieldError/FieldError.tsx new file mode 100644 index 0000000000..b90d7b3bd3 --- /dev/null +++ b/packages/canon/src/components/FieldError/FieldError.tsx @@ -0,0 +1,39 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef } from 'react'; +import { + FieldError as AriaFieldError, + type FieldErrorProps, +} from 'react-aria-components'; +import clsx from 'clsx'; + +/** @public */ +export const FieldError = forwardRef( + (props: FieldErrorProps, ref) => { + const { className, ...rest } = props; + + return ( + + ); + }, +); + +FieldError.displayName = 'FieldError'; diff --git a/packages/canon/src/components/FieldError/index.ts b/packages/canon/src/components/FieldError/index.ts new file mode 100644 index 0000000000..5b61f83ace --- /dev/null +++ b/packages/canon/src/components/FieldError/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './FieldError'; diff --git a/packages/canon/src/components/RadioGroup/RadioGroup.stories.tsx b/packages/canon/src/components/RadioGroup/RadioGroup.stories.tsx new file mode 100644 index 0000000000..4f28e04bfc --- /dev/null +++ b/packages/canon/src/components/RadioGroup/RadioGroup.stories.tsx @@ -0,0 +1,147 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Meta, StoryObj } from '@storybook/react'; +import { RadioGroup, Radio } from './RadioGroup'; + +const meta = { + title: 'Forms/RadioGroup', + component: RadioGroup, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +export const Default: Story = { + args: { + label: 'What is your favorite pokemon?', + }, + render: args => ( + + Bulbasaur + Charmander + Squirtle + + ), +}; + +export const Horizontal: Story = { + args: { + ...Default.args, + orientation: 'horizontal', + }, + render: args => ( + + Bulbasaur + Charmander + Squirtle + + ), +}; + +export const Disabled: Story = { + args: { + ...Default.args, + isDisabled: true, + }, + render: args => ( + + Bulbasaur + Charmander + Squirtle + + ), +}; + +export const DisabledSingle: Story = { + args: { + ...Default.args, + }, + render: args => ( + + Bulbasaur + + Charmander + + Squirtle + + ), +}; + +export const DisabledAndSelected: Story = { + args: { + ...Default.args, + value: 'charmander', + }, + render: args => ( + + Bulbasaur + + Charmander + + Squirtle + + ), +}; + +export const Invalid: Story = { + args: { + ...Default.args, + name: 'pokemon', + isInvalid: true, + }, + render: args => ( + + Bulbasaur + + Charmander + + Squirtle + + ), +}; + +export const Validation: Story = { + args: { + ...Default.args, + name: 'pokemon', + defaultValue: 'charmander', + validationBehavior: 'aria', + validate: value => (value === 'charmander' ? 'Nice try!' : null), + }, + render: args => ( + + Bulbasaur + Charmander + Squirtle + + ), +}; + +export const ReadOnly: Story = { + args: { + ...Default.args, + isReadOnly: true, + defaultValue: 'charmander', + }, + render: args => ( + + Bulbasaur + Charmander + Squirtle + + ), +}; diff --git a/packages/canon/src/components/RadioGroup/RadioGroup.styles.css b/packages/canon/src/components/RadioGroup/RadioGroup.styles.css new file mode 100644 index 0000000000..a712f9031f --- /dev/null +++ b/packages/canon/src/components/RadioGroup/RadioGroup.styles.css @@ -0,0 +1,95 @@ +.canon-RadioGroup { + display: flex; + flex-direction: column; + color: var(--canon-fg-primary); +} + +.canon-RadioGroup[data-orientation='horizontal'] .canon-RadioGroupContent { + flex-direction: row; + gap: var(--canon-space-4); +} + +.canon-RadioGroupContent { + display: flex; + flex-direction: column; + gap: var(--canon-space-2); +} + +.canon-Radio { + display: flex; + /* This is needed so the HiddenInput is positioned correctly */ + position: relative; + align-items: center; + gap: var(--canon-space-2); + font-size: var(--canon-font-size-2); + color: var(--canon-fg-primary); + forced-color-adjust: none; + + &:before { + content: ''; + display: block; + width: 1rem; + height: 1rem; + box-sizing: border-box; + border: 0.125rem solid var(--canon-border); + background: var(--canon-gray-1); + border-radius: var(--canon-radius-full); + transition: all 200ms; + } + + &[data-pressed]:before { + border-color: var(--canon-border); + } + + &[data-selected] { + &:before { + border-color: var(--canon-bg-solid); + border-width: 0.25rem; + } + + &[data-pressed]:before { + border-color: var(--canon-bg-solid); + } + } + + &[data-focus-visible]:before { + outline: 2px solid var(--canon-ring); + outline-offset: 2px; + } + + &[data-disabled] { + cursor: not-allowed; + color: var(--canon-fg-disabled); + + &:before { + border-color: var(--canon-border-disabled); + background: var(--canon-bg-disabled); + } + + &[data-selected]:before { + border-color: var(--canon-border-disabled); + } + } + + &[data-invalid]:before { + border-color: var(--canon-border-danger); + } + + &[data-invalid][data-selected]:before { + border-color: var(--canon-border-danger); + } + + /* Ensure disabled state prevails over invalid state */ + &[data-disabled][data-invalid] { + color: var(--canon-fg-disabled); + + &:before { + border-color: var(--canon-border-disabled); + background: var(--canon-bg-disabled); + } + + &[data-selected]:before { + border-color: var(--canon-border-disabled); + } + } +} diff --git a/packages/canon/src/components/RadioGroup/RadioGroup.tsx b/packages/canon/src/components/RadioGroup/RadioGroup.tsx new file mode 100644 index 0000000000..d1d7451a92 --- /dev/null +++ b/packages/canon/src/components/RadioGroup/RadioGroup.tsx @@ -0,0 +1,86 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { forwardRef, useEffect } from 'react'; +import { + RadioGroup as AriaRadioGroup, + Radio as AriaRadio, +} from 'react-aria-components'; +import clsx from 'clsx'; +import { FieldLabel } from '../FieldLabel'; +import { FieldError } from '../FieldError'; + +import type { RadioGroupProps, RadioProps } from './types'; + +/** @public */ +export const RadioGroup = forwardRef( + (props, ref) => { + const { + className, + label, + secondaryLabel, + description, + isRequired, + 'aria-label': ariaLabel, + 'aria-labelledby': ariaLabelledBy, + children, + ...rest + } = props; + + useEffect(() => { + if (!label && !ariaLabel && !ariaLabelledBy) { + console.warn( + 'RadioGroup requires either a visible label, aria-label, or aria-labelledby for accessibility', + ); + } + }, [label, ariaLabel, ariaLabelledBy]); + + // If a secondary label is provided, use it. Otherwise, use 'Required' if the field is required. + const secondaryLabelText = + secondaryLabel || (isRequired ? 'Required' : null); + + return ( + + +
{children}
+ +
+ ); + }, +); + +RadioGroup.displayName = 'RadioGroup'; + +/** @public */ +export const Radio = forwardRef((props, ref) => { + const { className, ...rest } = props; + + return ( + + ); +}); + +RadioGroup.displayName = 'RadioGroup'; diff --git a/packages/canon/src/components/RadioGroup/index.ts b/packages/canon/src/components/RadioGroup/index.ts new file mode 100644 index 0000000000..fd9572f667 --- /dev/null +++ b/packages/canon/src/components/RadioGroup/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './RadioGroup'; +export * from './types'; diff --git a/packages/canon/src/components/RadioGroup/types.ts b/packages/canon/src/components/RadioGroup/types.ts new file mode 100644 index 0000000000..029ef4baf9 --- /dev/null +++ b/packages/canon/src/components/RadioGroup/types.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { + RadioGroupProps as AriaRadioGroupProps, + RadioProps as AriaRadioProps, +} from 'react-aria-components'; +import type { FieldLabelProps } from '../FieldLabel/types'; +import { ReactNode } from 'react'; + +/** @public */ +export interface RadioGroupProps + extends Omit, + Omit { + children?: ReactNode; +} + +/** @public */ +export interface RadioProps extends AriaRadioProps {} diff --git a/packages/canon/src/components/TextField/TextField.styles.css b/packages/canon/src/components/TextField/TextField.styles.css index 9f2039b3af..c0432d88aa 100644 --- a/packages/canon/src/components/TextField/TextField.styles.css +++ b/packages/canon/src/components/TextField/TextField.styles.css @@ -104,10 +104,3 @@ cursor: not-allowed; border: 1px solid var(--canon-border-disabled); } - -.canon-TextFieldError { - color: var(--canon-fg-danger); - font-size: var(--canon-font-size-2); - font-weight: var(--canon-font-weight-regular); - margin-top: var(--canon-space-2); -} diff --git a/packages/canon/src/components/TextField/TextField.tsx b/packages/canon/src/components/TextField/TextField.tsx index edb3b1dc29..8c8471b0ef 100644 --- a/packages/canon/src/components/TextField/TextField.tsx +++ b/packages/canon/src/components/TextField/TextField.tsx @@ -15,14 +15,11 @@ */ import { forwardRef, useEffect } from 'react'; -import { - Input, - TextField as AriaTextField, - FieldError, -} from 'react-aria-components'; +import { Input, TextField as AriaTextField } from 'react-aria-components'; import { useResponsiveValue } from '../../hooks/useResponsiveValue'; import clsx from 'clsx'; import { FieldLabel } from '../FieldLabel'; +import { FieldError } from '../FieldError'; import type { TextFieldProps } from './types'; @@ -88,7 +85,7 @@ export const TextField = forwardRef( placeholder={placeholder} /> - + ); }, diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index aa453f7314..407723fcc9 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -23,6 +23,7 @@ @import '../components/Container/styles.css'; @import '../components/DataTable/Root/DataTableRoot.styles.css'; @import '../components/DataTable/Pagination/DataTablePagination.styles.css'; +@import '../components/FieldError/FieldError.styles.css'; @import '../components/FieldLabel/FieldLabel.styles.css'; @import '../components/Flex/styles.css'; @import '../components/Grid/styles.css'; @@ -30,6 +31,7 @@ @import '../components/Icon/styles.css'; @import '../components/Link/styles.css'; @import '../components/Menu/Menu.styles.css'; +@import '../components/RadioGroup/RadioGroup.styles.css'; @import '../components/Table/styles.css'; @import '../components/Table/TableCell/TableCell.styles.css'; @import '../components/Table/TableCellText/TableCellText.styles.css'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index d35883bbe6..cc540d1bb7 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -41,6 +41,7 @@ export * from './components/Icon'; export * from './components/ButtonIcon'; export * from './components/ButtonLink'; export * from './components/Checkbox'; +export * from './components/RadioGroup'; export * from './components/Table'; export * from './components/Tabs'; export * from './components/TextField';