Add doscs + FieldError
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
preview={<RadioGroupSnippet story="Default" />}
|
||||
code={radioGroupDefaultSnippet}
|
||||
/>
|
||||
|
||||
<ComponentInfos
|
||||
component="radio-group"
|
||||
classNames={['canon-RadioGroup', 'canon-RadioGroupContent']}
|
||||
usageCode={radioGroupUsageSnippet}
|
||||
/>
|
||||
|
||||
## API reference
|
||||
|
||||
<PropsTable data={radioGroupPropDefs} />
|
||||
|
||||
## Examples
|
||||
|
||||
### Horizontal
|
||||
|
||||
Here's a simple TextField with a description.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<RadioGroupSnippet story="Horizontal" />}
|
||||
code={radioGroupHorizontalSnippet}
|
||||
/>
|
||||
|
||||
### Disabled
|
||||
|
||||
You can disable the entire radio group by adding the `isDisabled` prop to the `RadioGroup` component.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<RadioGroupSnippet story="Disabled" />}
|
||||
code={radioGroupDisabledSnippet}
|
||||
/>
|
||||
|
||||
### Disabled Single radio
|
||||
|
||||
You can disable a single radio by adding the `isDisabled` prop to the `Radio` component.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<RadioGroupSnippet story="DisabledSingle" />}
|
||||
code={radioGroupDisabledSingleSnippet}
|
||||
/>
|
||||
|
||||
### Validation
|
||||
|
||||
Here's an example of a radio group with errors.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<RadioGroupSnippet story="Validation" />}
|
||||
code={radioGroupValidationSnippet}
|
||||
/>
|
||||
|
||||
### Read only
|
||||
|
||||
You can make the radio group read only by adding the `isReadOnly` prop to the `RadioGroup` component.
|
||||
|
||||
<Snippet
|
||||
align="center"
|
||||
py={4}
|
||||
open
|
||||
preview={<RadioGroupSnippet story="ReadOnly" />}
|
||||
code={radioGroupReadOnlySnippet}
|
||||
/>
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
classNamePropDefs,
|
||||
stylePropDefs,
|
||||
type PropDef,
|
||||
} from '@/utils/propDefs';
|
||||
|
||||
export const radioGroupPropDefs: Record<string, PropDef> = {
|
||||
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';
|
||||
|
||||
<RadioGroup />`;
|
||||
|
||||
export const radioGroupDefaultSnippet = `<RadioGroup label="Label" />`;
|
||||
|
||||
export const radioGroupDescriptionSnippet = `<TextField label="Label" description="Description" placeholder="Enter a URL" />`;
|
||||
|
||||
export const radioGroupHorizontalSnippet = `<RadioGroup label="Label" orientation="horizontal" />`;
|
||||
|
||||
export const radioGroupDisabledSnippet = `<RadioGroup label="Label" isDisabled>
|
||||
<Radio value="bulbasaur">Bulbasaur</Radio>
|
||||
<Radio value="charmander">Charmander</Radio>
|
||||
<Radio value="squirtle">Squirtle</Radio>
|
||||
</RadioGroup>`;
|
||||
|
||||
export const radioGroupDisabledSingleSnippet = `<RadioGroup label="Label">
|
||||
<Radio value="bulbasaur">Bulbasaur</Radio>
|
||||
<Radio value="charmander" isDisabled>Charmander</Radio>
|
||||
<Radio value="squirtle">Squirtle</Radio>
|
||||
</RadioGroup>`;
|
||||
|
||||
export const radioGroupValidationSnippet = `<RadioGroup validate: value => (value === \'charmander\' ? \'Nice try!\' : null)>
|
||||
<Radio value="bulbasaur">Bulbasaur</Radio>
|
||||
<Radio value="charmander">Charmander</Radio>
|
||||
<Radio value="squirtle">Squirtle</Radio>
|
||||
</RadioGroup>`;
|
||||
|
||||
export const radioGroupReadOnlySnippet = `<RadioGroup label="Label" isReadOnly>
|
||||
<Radio value="bulbasaur">Bulbasaur</Radio>
|
||||
<Radio value="charmander">Charmander</Radio>
|
||||
<Radio value="squirtle">Squirtle</Radio>
|
||||
</RadioGroup>`;
|
||||
@@ -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 ? <StoryComponent /> : null;
|
||||
};
|
||||
|
||||
export const RadioGroupSnippet = ({
|
||||
story,
|
||||
}: {
|
||||
story: keyof typeof RadioGroupStories;
|
||||
}) => {
|
||||
const stories = composeStories(RadioGroupStories);
|
||||
const StoryComponent = stories[story as keyof typeof stories];
|
||||
|
||||
return StoryComponent ? <StoryComponent /> : null;
|
||||
};
|
||||
|
||||
@@ -116,6 +116,11 @@ export const components: Page[] = [
|
||||
slug: 'menu',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'RadioGroup',
|
||||
slug: 'radio-group',
|
||||
status: 'alpha',
|
||||
},
|
||||
{
|
||||
title: 'Select',
|
||||
slug: 'select',
|
||||
|
||||
@@ -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<typeof FieldError>;
|
||||
|
||||
export default meta;
|
||||
type Story = StoryObj<typeof meta>;
|
||||
|
||||
// Show error with server validation using Form component
|
||||
export const WithServerValidation: Story = {
|
||||
render: () => (
|
||||
<Form validationErrors={{ demo: 'This is a server validation error.' }}>
|
||||
<TextField
|
||||
name="demo"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
<FieldError />
|
||||
</TextField>
|
||||
</Form>
|
||||
),
|
||||
};
|
||||
|
||||
// Show error using children
|
||||
export const WithCustomMessage: Story = {
|
||||
render: () => (
|
||||
<TextField
|
||||
isInvalid
|
||||
validationBehavior="aria"
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
<FieldError>This is a custom error message.</FieldError>
|
||||
</TextField>
|
||||
),
|
||||
};
|
||||
|
||||
// Show error with render prop function
|
||||
export const WithRenderProp: Story = {
|
||||
render: () => (
|
||||
<TextField
|
||||
isInvalid
|
||||
validationBehavior="aria"
|
||||
validate={() => 'This field is invalid'}
|
||||
style={{
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'flex-start',
|
||||
}}
|
||||
>
|
||||
<Input />
|
||||
<FieldError>
|
||||
{({ validationErrors }) =>
|
||||
validationErrors.length > 0 ? validationErrors[0] : 'Field is invalid'
|
||||
}
|
||||
</FieldError>
|
||||
</TextField>
|
||||
),
|
||||
};
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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<HTMLDivElement, FieldErrorProps>(
|
||||
(props: FieldErrorProps, ref) => {
|
||||
const { className, ...rest } = props;
|
||||
|
||||
return (
|
||||
<AriaFieldError
|
||||
className={clsx('canon-FieldError', className)}
|
||||
ref={ref}
|
||||
{...rest}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
FieldError.displayName = 'FieldError';
|
||||
@@ -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';
|
||||
@@ -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<HTMLDivElement, RadioGroupProps>(
|
||||
description={description}
|
||||
/>
|
||||
<div className="canon-RadioGroupContent">{children}</div>
|
||||
<FieldError className="canon-TextFieldError" />
|
||||
<FieldError />
|
||||
</AriaRadioGroup>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -97,16 +97,28 @@ export const Disabled: Story = {
|
||||
export const WithIcon: Story = {
|
||||
args: {
|
||||
...Default.args,
|
||||
defaultValue: 'Search...',
|
||||
icon: <Icon name="search" />,
|
||||
},
|
||||
render: args => (
|
||||
<TextField
|
||||
{...args}
|
||||
icon={<Icon name="search" />}
|
||||
defaultValue="Search..."
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const DisabledWithIcon: Story = {
|
||||
args: {
|
||||
...WithIcon.args,
|
||||
isDisabled: true,
|
||||
...Default.args,
|
||||
},
|
||||
render: args => (
|
||||
<TextField
|
||||
{...args}
|
||||
icon={<Icon name="search" />}
|
||||
defaultValue="Search..."
|
||||
isDisabled
|
||||
/>
|
||||
),
|
||||
};
|
||||
|
||||
export const ShowError: Story = {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<HTMLDivElement, TextFieldProps>(
|
||||
{...(icon && { 'data-icon': true })}
|
||||
/>
|
||||
</div>
|
||||
<FieldError className="canon-TextFieldError" />
|
||||
<FieldError />
|
||||
</AriaTextField>
|
||||
);
|
||||
},
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
Reference in New Issue
Block a user