diff --git a/.changeset/whole-papers-retire.md b/.changeset/whole-papers-retire.md new file mode 100644 index 0000000000..fed9ff7aa3 --- /dev/null +++ b/.changeset/whole-papers-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/canon': minor +--- + +**BREAKING**: Added a new TextField component to replace the Field and Input component. After feedback, it became clear that we needed to build a more opinionated version to avoid any problem in the future. diff --git a/packages/canon/css/components.css b/packages/canon/css/components.css index 8492bb6d83..bc473d2006 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-MenuPositioner { outline: 0; } 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 a998cdcb8e..b5093e217c 100644 --- a/packages/canon/css/styles.css +++ b/packages/canon/css/styles.css @@ -9610,6 +9610,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); @@ -9641,52 +9671,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-MenuPositioner { outline: 0; } 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/package.json b/packages/canon/package.json index e3a2918ef5..b5d2d715f6 100644 --- a/packages/canon/package.json +++ b/packages/canon/package.json @@ -56,7 +56,6 @@ "@storybook/react": "^8.6.8", "@storybook/react-webpack5": "^8.6.8", "@storybook/test": "^8.6.8", - "@testing-library/jest-dom": "^6.0.0", "@types/react": "^18.0.0", "@types/react-dom": "^18.0.0", "chalk": "^5.4.1", diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index b0b400aaea..766b40febd 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -6,9 +6,7 @@ import { Breakpoint as Breakpoint_2 } from '@backstage/canon'; import { Context } from 'react'; import type { CSSProperties } from 'react'; -import { Field as Field_2 } from '@base-ui-components/react/field'; import { ForwardRefExoticComponent } from 'react'; -import { Input as Input_2 } from '@base-ui-components/react/input'; import { Menu as Menu_2 } from '@base-ui-components/react/menu'; import { default as React_2 } from 'react'; import * as React_3 from 'react'; @@ -247,36 +245,6 @@ export type EnumPropDef = { required?: boolean; }; -// @public (undocumented) -export const Field: { - Root: React_2.ForwardRefExoticComponent< - Omit, 'ref'> & - React_2.RefAttributes - >; - Label: React_2.ForwardRefExoticComponent< - Omit, 'ref'> & - React_2.RefAttributes - >; - Description: React_2.ForwardRefExoticComponent< - Omit< - Field_2.Description.Props & React_2.RefAttributes, - 'ref' - > & - React_2.RefAttributes - >; - Error: React_2.ForwardRefExoticComponent< - Omit, 'ref'> & - React_2.RefAttributes - >; - Validity: ({ - children, - className, - ...props - }: React_2.ComponentPropsWithoutRef & { - className?: string; - }) => React_2.JSX.Element; -}; - // @public (undocumented) export const Flex: ForwardRefExoticComponent< FlexProps & RefAttributes @@ -719,17 +687,6 @@ export interface IconProviderProps { // @public (undocumented) export const icons: IconMap; -// @public (undocumented) -export const Input: React_2.ForwardRefExoticComponent< - InputProps & React_2.RefAttributes ->; - -// @public (undocumented) -export interface InputProps extends Omit { - // (undocumented) - size?: 'sm' | 'md'; -} - // @public (undocumented) export type JustifyContent = | 'stretch' @@ -1096,6 +1053,21 @@ const Text_2: React_2.ForwardRefExoticComponent< >; export { Text_2 as Text }; +// @public (undocumented) +export const TextField: React_2.ForwardRefExoticComponent< + TextFieldProps & React_2.RefAttributes +>; + +// @public (undocumented) +export interface TextFieldProps + extends Omit, 'size'> { + className?: string; + description?: string; + label?: string; + name: string; + size?: 'small' | 'medium' | Partial>; +} + // @public (undocumented) export interface TextProps { // (undocumented) 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/Field/index.ts b/packages/canon/src/components/Field/index.ts deleted file mode 100644 index 297ed9b27b..0000000000 --- a/packages/canon/src/components/Field/index.ts +++ /dev/null @@ -1,17 +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 * from './Field'; 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/types.ts b/packages/canon/src/components/Input/types.ts deleted file mode 100644 index ac013d1bbd..0000000000 --- a/packages/canon/src/components/Input/types.ts +++ /dev/null @@ -1,22 +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 { Input } from '@base-ui-components/react/input'; - -/** @public */ -export interface InputProps extends Omit { - size?: 'sm' | 'md'; -} diff --git a/packages/canon/src/components/Select/Select.tsx b/packages/canon/src/components/Select/Select.tsx index 1fc7072489..bcacefd02e 100644 --- a/packages/canon/src/components/Select/Select.tsx +++ b/packages/canon/src/components/Select/Select.tsx @@ -16,7 +16,8 @@ import React from 'react'; import { Select as SelectPrimitive } from '@base-ui-components/react/select'; -import { Field, Icon } from '@backstage/canon'; +import { Field } from '@base-ui-components/react/field'; +import { Icon } from '../Icon'; import clsx from 'clsx'; import './Select.styles.css'; import { SelectProps } from './types'; 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..dd5558437b --- /dev/null +++ b/packages/canon/src/components/TextField/TextField.stories.tsx @@ -0,0 +1,163 @@ +/* + * 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 { 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', + }, + }, +}; + +export const ShowErrorOnSubmit: Story = { + args: { + ...WithLabel.args, + pattern: 'https?://.*', + type: 'url', + required: true, + label: 'Homepage', + name: 'url', + value: 'https://backstage-fake-site.com', + }, + decorators: [ + Story => { + const [errors, setErrors] = useState | undefined>( + undefined, + ); + const [loading, setLoading] = useState(false); + + const handleSubmit = async (event: React.FormEvent) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + + setLoading(true); + + await new Promise(resolve => { + setTimeout(resolve, 200); + }); + + try { + const url = new URL(formData.get('url') as string); + + const allowedHosts = [ + 'backstage.io', + 'beta.backstage.io', + 'www.backstage.io', + ]; + + if (!allowedHosts.includes(url.hostname)) { + setErrors({ url: 'The example domain is not allowed' }); + setLoading(false); + + return; + } + + setErrors(undefined); + setLoading(false); + + return; + } catch { + setErrors({ url: 'This is not a valid URL' }); + setLoading(false); + } + }; + + return ( +
setErrors(undefined)} + onSubmit={handleSubmit} + > + + + + ); + }, + ], +}; 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..ccea9e8c65 --- /dev/null +++ b/packages/canon/src/components/TextField/TextField.tsx @@ -0,0 +1,62 @@ +/* + * 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/Input/index.ts b/packages/canon/src/components/TextField/index.ts similarity index 88% rename from packages/canon/src/components/Input/index.ts rename to packages/canon/src/components/TextField/index.ts index ce4e08ccc2..29df00ccaf 100644 --- a/packages/canon/src/components/Input/index.ts +++ b/packages/canon/src/components/TextField/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { Input } from './Input'; -export type { InputProps } from './types'; +export * from './TextField'; +export type { TextFieldProps } from './types'; diff --git a/packages/canon/src/components/TextField/types.ts b/packages/canon/src/components/TextField/types.ts new file mode 100644 index 0000000000..100d5fcb17 --- /dev/null +++ b/packages/canon/src/components/TextField/types.ts @@ -0,0 +1,47 @@ +/* + * 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 { Breakpoint } from '../../types'; + +/** @public */ +export interface TextFieldProps + extends Omit, 'size'> { + /** + * The class name of the text field + */ + className?: string; + + /** + * The size of the text field + * @defaultValue 'medium' + */ + size?: 'small' | 'medium' | Partial>; + + /** + * 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 cd6517c6ed..2480baab14 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 1f7841cfd5..7435b6dd60 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'; diff --git a/yarn.lock b/yarn.lock index e34a9f6ffe..baf0dd3925 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3789,7 +3789,6 @@ __metadata: "@storybook/react": ^8.6.8 "@storybook/react-webpack5": ^8.6.8 "@storybook/test": ^8.6.8 - "@testing-library/jest-dom": ^6.0.0 "@types/react": ^18.0.0 "@types/react-dom": ^18.0.0 chalk: ^5.4.1