From 4f1b4ad6c018de7db200d865dcdd0fd4f3df6d1e Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Thu, 19 Jun 2025 21:32:43 +0100 Subject: [PATCH] Add doscs + FieldError Signed-off-by: Charles de Dreuille --- .../src/content/components/radio-group.mdx | 98 +++++++++++++++++++ .../content/components/radio-group.props.ts | 64 ++++++++++++ canon-docs/src/snippets/stories-snippets.tsx | 12 +++ canon-docs/src/utils/data.ts | 5 + .../FieldError/FieldError.stories.tsx | 87 ++++++++++++++++ .../FieldError/FieldError.styles.css | 6 ++ .../src/components/FieldError/FieldError.tsx | 39 ++++++++ .../canon/src/components/FieldError/index.ts | 17 ++++ .../src/components/RadioGroup/RadioGroup.tsx | 4 +- .../TextField/TextField.stories.tsx | 20 +++- .../components/TextField/TextField.styles.css | 7 -- .../src/components/TextField/TextField.tsx | 9 +- packages/canon/src/css/components.css | 1 + packages/canon/src/index.ts | 1 + 14 files changed, 351 insertions(+), 19 deletions(-) create mode 100644 canon-docs/src/content/components/radio-group.mdx create mode 100644 canon-docs/src/content/components/radio-group.props.ts create mode 100644 packages/canon/src/components/FieldError/FieldError.stories.tsx create mode 100644 packages/canon/src/components/FieldError/FieldError.styles.css create mode 100644 packages/canon/src/components/FieldError/FieldError.tsx create mode 100644 packages/canon/src/components/FieldError/index.ts 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/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.tsx b/packages/canon/src/components/RadioGroup/RadioGroup.tsx index 5bdfcb8a20..d1d7451a92 100644 --- a/packages/canon/src/components/RadioGroup/RadioGroup.tsx +++ b/packages/canon/src/components/RadioGroup/RadioGroup.tsx @@ -18,10 +18,10 @@ import { forwardRef, useEffect } from 'react'; import { RadioGroup as AriaRadioGroup, Radio as AriaRadio, - FieldError, } from 'react-aria-components'; import clsx from 'clsx'; import { FieldLabel } from '../FieldLabel'; +import { FieldError } from '../FieldError'; import type { RadioGroupProps, RadioProps } from './types'; @@ -66,7 +66,7 @@ export const RadioGroup = forwardRef( description={description} />
{children}
- + ); }, diff --git a/packages/canon/src/components/TextField/TextField.stories.tsx b/packages/canon/src/components/TextField/TextField.stories.tsx index e07aa6ea05..c8fc5b5710 100644 --- a/packages/canon/src/components/TextField/TextField.stories.tsx +++ b/packages/canon/src/components/TextField/TextField.stories.tsx @@ -97,16 +97,28 @@ export const Disabled: Story = { export const WithIcon: Story = { args: { ...Default.args, - defaultValue: 'Search...', - icon: , }, + render: args => ( + } + defaultValue="Search..." + /> + ), }; export const DisabledWithIcon: Story = { args: { - ...WithIcon.args, - isDisabled: true, + ...Default.args, }, + render: args => ( + } + defaultValue="Search..." + isDisabled + /> + ), }; export const ShowError: Story = { 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 444e76e983..d20e68ef88 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'; @@ -86,7 +83,7 @@ export const TextField = forwardRef( {...(icon && { 'data-icon': true })} /> - + ); }, diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index 4199bc04eb..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'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 79b295484c..dc0b3c1cfc 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -38,6 +38,7 @@ export * from './components/Checkbox'; export * from './components/Collapsible'; export * from './components/DataTable'; export * from './components/FieldLabel'; +export * from './components/FieldError'; export * from './components/Heading'; export * from './components/Icon'; export * from './components/Link';