diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index cb376e64ed..b95ea2425a 100644 --- a/packages/canon/css/components.css +++ b/packages/canon/css/components.css @@ -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); diff --git a/packages/canon/css/field.css b/packages/canon/css/field.css deleted file mode 100644 index dcd3a71af5..0000000000 --- a/packages/canon/css/field.css +++ /dev/null @@ -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; -} diff --git a/packages/canon/css/input.css b/packages/canon/css/input.css deleted file mode 100644 index a0ca6b18e4..0000000000 --- a/packages/canon/css/input.css +++ /dev/null @@ -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; -} diff --git a/packages/canon/css/styles.css b/packages/canon/css/styles.css index 3b312c220e..5c747ce3d1 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -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); diff --git a/packages/canon/css/textfield.css b/packages/canon/css/textfield.css new file mode 100644 index 0000000000..e40251fd5a --- /dev/null +++ b/packages/canon/css/textfield.css @@ -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; +} diff --git a/packages/canon/src/components/Field/Field.stories.tsx b/packages/canon/src/components/Field/Field.stories.tsx deleted file mode 100644 index b949010f5f..0000000000 --- a/packages/canon/src/components/Field/Field.stories.tsx +++ /dev/null @@ -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; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: () => ( - - Label - - Error - - ), -}; - -export const WithLabelAndDescription: Story = { - render: () => ( - - Label - - Description - - ), -}; - -export const WithError: Story = { - render: () => ( - - value !== 'Backstage' ? 'Please enter a different name' : null - } - validationMode="onChange" - > - Name - - - An error will show if the value is not Backstage - - Error - - ), -}; - -export const WithValidity: Story = { - render: () => ( - - value !== 'Backstage' ? 'Please enter a different name' : null - } - > - Name - - - An error will show if the value is not Backstage - - - {validityState => ( -
{validityState.value ? 'Not Backstage' : 'Backstage'}
- )} -
-
- ), -}; diff --git a/packages/canon/src/components/Field/Field.styles.css b/packages/canon/src/components/Field/Field.styles.css deleted file mode 100644 index 6a8e3c8e22..0000000000 --- a/packages/canon/src/components/Field/Field.styles.css +++ /dev/null @@ -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); -} diff --git a/packages/canon/src/components/Field/Field.tsx b/packages/canon/src/components/Field/Field.tsx deleted file mode 100644 index b0c86b2c85..0000000000 --- a/packages/canon/src/components/Field/Field.tsx +++ /dev/null @@ -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, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -FieldRoot.displayName = FieldPrimitive.Root.displayName; - -const FieldLabel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -FieldLabel.displayName = FieldPrimitive.Label.displayName; - -const FieldDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -FieldDescription.displayName = FieldPrimitive.Description.displayName; - -const FieldError = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -FieldError.displayName = FieldPrimitive.Error.displayName; - -const FieldValidity = ({ - children, - className, - ...props -}: React.ComponentPropsWithoutRef & { - className?: string; -}) => ( - - {validityState => ( -
- {children(validityState)} -
- )} -
-); - -/** @public */ -export const Field = { - Root: FieldRoot, - Label: FieldLabel, - Description: FieldDescription, - Error: FieldError, - Validity: FieldValidity, -}; diff --git a/packages/canon/src/components/Input/Input.stories.tsx b/packages/canon/src/components/Input/Input.stories.tsx deleted file mode 100644 index c1c76ddc09..0000000000 --- a/packages/canon/src/components/Input/Input.stories.tsx +++ /dev/null @@ -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; - -export default meta; -type Story = StoryObj; - -export const Primary: Story = { - render: () => , -}; - -export const Sizes: Story = { - render: () => ( - - - - - ), -}; diff --git a/packages/canon/src/components/Input/Input.tsx b/packages/canon/src/components/Input/Input.tsx deleted file mode 100644 index 5eb68e7bf4..0000000000 --- a/packages/canon/src/components/Input/Input.tsx +++ /dev/null @@ -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, InputProps>( - (props, ref) => { - const { size = 'md', className, ...rest } = props; - - return ( - - ); - }, -); - -Input.displayName = InputPrimitive.displayName; - -export { Input }; diff --git a/packages/canon/src/components/Input/index.ts b/packages/canon/src/components/Input/index.ts deleted file mode 100644 index ce4e08ccc2..0000000000 --- a/packages/canon/src/components/Input/index.ts +++ /dev/null @@ -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'; diff --git a/packages/canon/src/components/Input/types.ts b/packages/canon/src/components/TextField/TextField.props.ts similarity index 57% rename from packages/canon/src/components/Input/types.ts rename to packages/canon/src/components/TextField/TextField.props.ts index ac013d1bbd..db0d09567e 100644 --- a/packages/canon/src/components/Input/types.ts +++ b/packages/canon/src/components/TextField/TextField.props.ts @@ -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 { - 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; diff --git a/packages/canon/src/components/TextField/TextField.stories.tsx b/packages/canon/src/components/TextField/TextField.stories.tsx new file mode 100644 index 0000000000..a9af58dcf9 --- /dev/null +++ b/packages/canon/src/components/TextField/TextField.stories.tsx @@ -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; + +export default meta; +type Story = StoryObj; + +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 => ( + + + + + ), +}; + +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 ( +
{ + 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); + }} + > + + + + ); + }, + ], + 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(); + }); + }, +}; diff --git a/packages/canon/src/components/Input/Input.styles.css b/packages/canon/src/components/TextField/TextField.styles.css similarity index 61% rename from packages/canon/src/components/Input/Input.styles.css rename to packages/canon/src/components/TextField/TextField.styles.css index 0ff108008d..bd36661b23 100644 --- a/packages/canon/src/components/Input/Input.styles.css +++ b/packages/canon/src/components/TextField/TextField.styles.css @@ -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; } diff --git a/packages/canon/src/components/TextField/TextField.tsx b/packages/canon/src/components/TextField/TextField.tsx new file mode 100644 index 0000000000..aca4f070d4 --- /dev/null +++ b/packages/canon/src/components/TextField/TextField.tsx @@ -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( + (props: TextFieldProps, ref) => { + const { + className, + size = 'medium', + label, + description, + name, + ...rest + } = props; + + // Get the responsive value for the variant + const responsiveSize = useResponsiveValue(size); + + return ( + + {label && ( + + {label} + + )} + + {description && ( + + {description} + + )} + + + ); + }, +); + +TextField.displayName = 'TextField'; diff --git a/packages/canon/src/components/Field/index.ts b/packages/canon/src/components/TextField/index.ts similarity index 95% rename from packages/canon/src/components/Field/index.ts rename to packages/canon/src/components/TextField/index.ts index 297ed9b27b..d491fe5e42 100644 --- a/packages/canon/src/components/Field/index.ts +++ b/packages/canon/src/components/TextField/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './Field'; +export * from './TextField'; diff --git a/packages/canon/src/components/TextField/types.ts b/packages/canon/src/components/TextField/types.ts new file mode 100644 index 0000000000..9b1b06509b --- /dev/null +++ b/packages/canon/src/components/TextField/types.ts @@ -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, '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; +} diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index 8c77a0f58c..9d86eeaeff 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -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'; diff --git a/packages/canon/src/index.ts b/packages/canon/src/index.ts index 3c9a28b44d..b2dee95cd9 100644 --- a/packages/canon/src/index.ts +++ b/packages/canon/src/index.ts @@ -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';