Add new TextField component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-03-24 17:42:55 +00:00
parent 99d5845f81
commit b06f680a99
19 changed files with 499 additions and 501 deletions
+38 -40
View File
@@ -404,6 +404,36 @@
height: 1.5rem;
}
.canon-FieldRoot {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.canon-FieldLabel {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
margin-bottom: var(--canon-space-1_5);
}
.canon-FieldDescription {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-FieldError {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-Input {
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
@@ -435,52 +465,20 @@
border-color: var(--canon-fg-danger);
}
.canon-Input--size-sm {
.canon-Input[data-disabled] {
opacity: .5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-Input--size-small {
height: 2rem;
}
.canon-Input--size-md {
.canon-Input--size-medium {
height: 2.5rem;
}
.canon-FieldRoot {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.canon-FieldLabel {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
margin-bottom: var(--canon-space-1_5);
}
.canon-FieldDescription {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-FieldError {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-FieldValidity {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-MenuTrigger {
box-sizing: border-box;
border: 1px solid var(--color-gray-200);
-37
View File
@@ -1,37 +0,0 @@
.canon-FieldRoot {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.canon-FieldLabel {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
margin-bottom: var(--canon-space-1_5);
}
.canon-FieldDescription {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-FieldError {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-FieldValidity {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
padding-top: var(--canon-space-1_5);
margin: 0;
}
-38
View File
@@ -1,38 +0,0 @@
.canon-Input {
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
padding: 0 var(--canon-space-4);
background-color: var(--canon-bg-surface-1);
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
width: 100%;
transition: border-color .2s ease-in-out, outline-color .2s ease-in-out;
}
.canon-Input::placeholder {
color: var(--canon-fg-secondary);
}
.canon-Input:hover {
border-color: var(--canon-border-hover);
}
.canon-Input:focus-visible {
outline-color: var(--canon-border-pressed);
border-color: var(--canon-border-pressed);
outline-width: 0;
}
.canon-Input[data-invalid] {
border-color: var(--canon-fg-danger);
}
.canon-Input--size-sm {
height: 2rem;
}
.canon-Input--size-md {
height: 2.5rem;
}
+38 -40
View File
@@ -9605,6 +9605,36 @@
height: 1.5rem;
}
.canon-FieldRoot {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.canon-FieldLabel {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
margin-bottom: var(--canon-space-1_5);
}
.canon-FieldDescription {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-FieldError {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-Input {
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
@@ -9636,52 +9666,20 @@
border-color: var(--canon-fg-danger);
}
.canon-Input--size-sm {
.canon-Input[data-disabled] {
opacity: .5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-Input--size-small {
height: 2rem;
}
.canon-Input--size-md {
.canon-Input--size-medium {
height: 2.5rem;
}
.canon-FieldRoot {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.canon-FieldLabel {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
margin-bottom: var(--canon-space-1_5);
}
.canon-FieldDescription {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-FieldError {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-FieldValidity {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-MenuTrigger {
box-sizing: border-box;
border: 1px solid var(--color-gray-200);
+74
View File
@@ -0,0 +1,74 @@
.canon-FieldRoot {
font-family: var(--canon-font-regular);
flex-direction: column;
width: 100%;
display: flex;
}
.canon-FieldLabel {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
margin-bottom: var(--canon-space-1_5);
}
.canon-FieldDescription {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-FieldError {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
padding-top: var(--canon-space-1_5);
margin: 0;
}
.canon-Input {
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
padding: 0 var(--canon-space-4);
background-color: var(--canon-bg-surface-1);
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
width: 100%;
transition: border-color .2s ease-in-out, outline-color .2s ease-in-out;
}
.canon-Input::placeholder {
color: var(--canon-fg-secondary);
}
.canon-Input:hover {
border-color: var(--canon-border-hover);
}
.canon-Input:focus-visible {
outline-color: var(--canon-border-pressed);
border-color: var(--canon-border-pressed);
outline-width: 0;
}
.canon-Input[data-invalid] {
border-color: var(--canon-fg-danger);
}
.canon-Input[data-disabled] {
opacity: .5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-Input--size-small {
height: 2rem;
}
.canon-Input--size-medium {
height: 2.5rem;
}
@@ -1,87 +0,0 @@
/*
* 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 React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Field } from './Field';
import { Input } from '../Input/Input';
const meta = {
title: 'Components/Field',
component: Field.Root,
} satisfies Meta<typeof Field.Root>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
render: () => (
<Field.Root>
<Field.Label>Label</Field.Label>
<Input />
<Field.Error>Error</Field.Error>
</Field.Root>
),
};
export const WithLabelAndDescription: Story = {
render: () => (
<Field.Root>
<Field.Label>Label</Field.Label>
<Input />
<Field.Description>Description</Field.Description>
</Field.Root>
),
};
export const WithError: Story = {
render: () => (
<Field.Root
validate={value =>
value !== 'Backstage' ? 'Please enter a different name' : null
}
validationMode="onChange"
>
<Field.Label>Name</Field.Label>
<Input />
<Field.Description>
An error will show if the value is not Backstage
</Field.Description>
<Field.Error match="customError">Error</Field.Error>
</Field.Root>
),
};
export const WithValidity: Story = {
render: () => (
<Field.Root
validate={value =>
value !== 'Backstage' ? 'Please enter a different name' : null
}
>
<Field.Label>Name</Field.Label>
<Input />
<Field.Description>
An error will show if the value is not Backstage
</Field.Description>
<Field.Validity>
{validityState => (
<div>{validityState.value ? 'Not Backstage' : 'Backstage'}</div>
)}
</Field.Validity>
</Field.Root>
),
};
@@ -1,53 +0,0 @@
/*
* 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.
*/
.canon-FieldRoot {
display: flex;
flex-direction: column;
font-family: var(--canon-font-regular);
width: 100%;
}
.canon-FieldLabel {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
margin-bottom: var(--canon-space-1_5);
}
.canon-FieldDescription {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
margin: 0;
padding-top: var(--canon-space-1_5);
}
.canon-FieldError {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
margin: 0;
padding-top: var(--canon-space-1_5);
}
.canon-FieldValidity {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
margin: 0;
padding-top: var(--canon-space-1_5);
}
@@ -1,92 +0,0 @@
/*
* 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 React from 'react';
import { Field as FieldPrimitive } from '@base-ui-components/react/field';
import clsx from 'clsx';
const FieldRoot = React.forwardRef<
React.ElementRef<typeof FieldPrimitive.Root>,
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Root>
>(({ className, ...props }, ref) => (
<FieldPrimitive.Root
ref={ref}
className={clsx('canon-FieldRoot', className)}
{...props}
/>
));
FieldRoot.displayName = FieldPrimitive.Root.displayName;
const FieldLabel = React.forwardRef<
React.ElementRef<typeof FieldPrimitive.Label>,
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Label>
>(({ className, ...props }, ref) => (
<FieldPrimitive.Label
ref={ref}
className={clsx('canon-FieldLabel', className)}
{...props}
/>
));
FieldLabel.displayName = FieldPrimitive.Label.displayName;
const FieldDescription = React.forwardRef<
React.ElementRef<typeof FieldPrimitive.Description>,
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Description>
>(({ className, ...props }, ref) => (
<FieldPrimitive.Description
ref={ref}
className={clsx('canon-FieldDescription', className)}
{...props}
/>
));
FieldDescription.displayName = FieldPrimitive.Description.displayName;
const FieldError = React.forwardRef<
React.ElementRef<typeof FieldPrimitive.Error>,
React.ComponentPropsWithoutRef<typeof FieldPrimitive.Error>
>(({ className, ...props }, ref) => (
<FieldPrimitive.Error
ref={ref}
className={clsx('canon-FieldError', className)}
{...props}
/>
));
FieldError.displayName = FieldPrimitive.Error.displayName;
const FieldValidity = ({
children,
className,
...props
}: React.ComponentPropsWithoutRef<typeof FieldPrimitive.Validity> & {
className?: string;
}) => (
<FieldPrimitive.Validity {...props}>
{validityState => (
<div className={clsx('canon-FieldValidity', className)}>
{children(validityState)}
</div>
)}
</FieldPrimitive.Validity>
);
/** @public */
export const Field = {
Root: FieldRoot,
Label: FieldLabel,
Description: FieldDescription,
Error: FieldError,
Validity: FieldValidity,
};
@@ -1,41 +0,0 @@
/*
* 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 React from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { Input } from './Input';
import { Flex } from '../Flex';
const meta = {
title: 'Components/Input',
component: Input,
} satisfies Meta<typeof Input>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
render: () => <Input className="canon-Input" />,
};
export const Sizes: Story = {
render: () => (
<Flex>
<Input size="sm" />
<Input size="md" />
</Flex>
),
};
@@ -1,43 +0,0 @@
/*
* 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 React, { ElementRef, forwardRef } from 'react';
import { Input as InputPrimitive } from '@base-ui-components/react/input';
import clsx from 'clsx';
import type { InputProps } from './types';
/** @public */
const Input = forwardRef<ElementRef<typeof InputPrimitive>, InputProps>(
(props, ref) => {
const { size = 'md', className, ...rest } = props;
return (
<InputPrimitive
ref={ref}
className={clsx(
'canon-Input',
size === 'sm' ? 'canon-Input--size-sm' : 'canon-Input--size-md',
className,
)}
{...rest}
/>
);
},
);
Input.displayName = InputPrimitive.displayName;
export { Input };
@@ -1,18 +0,0 @@
/*
* 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 { Input } from './Input';
export type { InputProps } from './types';
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* 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.
@@ -14,9 +14,20 @@
* limitations under the License.
*/
import { Input } from '@base-ui-components/react/input';
import type { PropDef, GetPropDefTypes } from '../../props/prop-def';
/** @public */
export interface InputProps extends Omit<Input.Props, 'size'> {
size?: 'sm' | 'md';
}
export const textFieldPropDefs = {
size: {
type: 'enum',
values: ['small', 'medium'],
className: 'canon-Button--size',
default: 'medium',
responsive: true,
},
} satisfies {
size: PropDef<'small' | 'medium'>;
};
/** @public */
export type TextFieldOwnProps = GetPropDefTypes<typeof textFieldPropDefs>;
@@ -0,0 +1,177 @@
/*
* 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 React, { useState } from 'react';
import type { Meta, StoryObj } from '@storybook/react';
import { TextField } from './TextField';
import { Form } from '@base-ui-components/react/form';
import { Button } from '../Button';
import { userEvent, waitFor, within, expect } from '@storybook/test';
import { Flex } from '../Flex';
const meta = {
title: 'Components/TextField',
component: TextField,
} satisfies Meta<typeof TextField>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
name: 'url',
placeholder: 'Enter a URL',
},
};
export const Filled: Story = {
args: {
...Default.args,
defaultValue: 'https://example.com',
},
};
export const WithLabel: Story = {
args: {
...Default.args,
label: 'Label',
},
};
export const WithDescription: Story = {
args: {
...WithLabel.args,
description: 'Description',
},
};
export const Disabled: Story = {
args: {
...WithLabel.args,
disabled: true,
},
};
export const Sizes: Story = {
args: {
...Default.args,
label: 'Label',
description: 'Description',
},
render: args => (
<Flex direction="row" gap="4">
<TextField {...args} size="small" />
<TextField {...args} size="medium" />
</Flex>
),
};
export const Responsive: Story = {
args: {
...WithLabel.args,
size: {
initial: 'small',
sm: 'medium',
},
},
};
async function submitForm(value: string) {
// Mimic a server response
await new Promise(resolve => {
setTimeout(resolve, 200);
});
try {
const url = new URL(value);
if (url.hostname.endsWith('example.com')) {
return { error: 'The example domain is not allowed' };
}
} catch {
return { error: 'This is not a valid URL' };
}
return { success: true };
}
export const ShowErrorOnSubmit: Story = {
args: {
...WithLabel.args,
pattern: 'https?://.*',
type: 'url',
required: true,
label: 'Homepage',
},
decorators: [
Story => {
const [errors, setErrors] = useState({});
const [loading, setLoading] = useState(false);
return (
<Form
errors={errors}
onClearErrors={setErrors}
onSubmit={async event => {
event.preventDefault();
const formData = new FormData(event.currentTarget);
const value = formData.get('url') as string;
setLoading(true);
const response = await submitForm(value);
const serverErrors = {
url: response.error,
};
setErrors(serverErrors);
setLoading(false);
}}
>
<Story />
<Button
type="submit"
disabled={loading}
size="small"
style={{ marginTop: '0.75rem' }}
>
Submit
</Button>
</Form>
);
},
],
play: async ({ canvasElement }) => {
const canvas = within(canvasElement);
const input = canvas.getByLabelText('Homepage', {
selector: 'input',
});
await userEvent.type(input, 'https://example.com', {
delay: 20,
});
const submitButton = canvas.getByRole('button');
await userEvent.click(submitButton);
await waitFor(() => {
expect(
canvas.getByText('The example domain is not allowed'),
).toBeInTheDocument();
});
},
};
@@ -14,6 +14,36 @@
* limitations under the License.
*/
.canon-FieldRoot {
display: flex;
flex-direction: column;
font-family: var(--canon-font-regular);
width: 100%;
}
.canon-FieldLabel {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
margin-bottom: var(--canon-space-1_5);
}
.canon-FieldDescription {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-secondary);
margin: 0;
padding-top: var(--canon-space-1_5);
}
.canon-FieldError {
font-size: var(--canon-font-size-2);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-danger);
margin: 0;
padding-top: var(--canon-space-1_5);
}
.canon-Input {
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
@@ -45,10 +75,16 @@
border-color: var(--canon-fg-danger);
}
.canon-Input--size-sm {
.canon-Input[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
.canon-Input--size-small {
height: 2rem;
}
.canon-Input--size-md {
.canon-Input--size-medium {
height: 2.5rem;
}
@@ -0,0 +1,70 @@
/*
* 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 React, { forwardRef } from 'react';
import { Field } from '@base-ui-components/react/field';
import { Input } from '@base-ui-components/react/input';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import clsx from 'clsx';
import type { TextFieldProps } from './types';
/** @public */
export const TextField = forwardRef<HTMLInputElement, TextFieldProps>(
(props: TextFieldProps, ref) => {
const {
className,
size = 'medium',
label,
description,
name,
...rest
} = props;
// Get the responsive value for the variant
const responsiveSize = useResponsiveValue(size);
return (
<Field.Root className={clsx('canon-FieldRoot', className)} name={name}>
{label && (
<Field.Label className={clsx('canon-FieldLabel', className)}>
{label}
</Field.Label>
)}
<Input
ref={ref}
type={name}
className={clsx(
'canon-Input',
`canon-Input--size-${responsiveSize}`,
className,
)}
{...rest}
/>
{description && (
<Field.Description
className={clsx('canon-FieldDescription', className)}
>
{description}
</Field.Description>
)}
<Field.Error className={clsx('canon-FieldError', className)} />
</Field.Root>
);
},
);
TextField.displayName = 'TextField';
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export * from './Field';
export * from './TextField';
@@ -0,0 +1,45 @@
/*
* 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 { TextFieldOwnProps } from './TextField.props';
export interface TextFieldProps
extends Omit<React.ComponentPropsWithoutRef<'input'>, 'size'> {
/**
* The class name of the text field
*/
className?: string;
/**
* The size of the text field
* @defaultValue 'medium'
*/
size?: TextFieldOwnProps['size'];
/**
* The label of the text field
*/
label?: string;
/**
* The description of the text field
*/
description?: string;
/**
* The name of the text field
*/
name: string;
}
+1 -2
View File
@@ -25,8 +25,7 @@
@import '../components/Text/styles.css';
@import '../components/Heading/styles.css';
@import '../components/IconButton/styles.css';
@import '../components/Input/Input.styles.css';
@import '../components/Field/Field.styles.css';
@import '../components/TextField/TextField.styles.css';
@import '../components/Menu/Menu.styles.css';
@import '../components/Link/styles.css';
@import '../components/Tooltip/Tooltip.styles.css';
+1 -2
View File
@@ -37,8 +37,7 @@ export * from './components/Icon';
export * from './components/IconButton';
export * from './components/Checkbox';
export * from './components/Table';
export * from './components/Input';
export * from './components/Field';
export * from './components/TextField';
export * from './components/Tooltip';
export * from './components/Menu';
export * from './components/ScrollArea';