diff --git a/packages/canon/report.api.md b/packages/canon/report.api.md index bba5a6c46e..058250c599 100644 --- a/packages/canon/report.api.md +++ b/packages/canon/report.api.md @@ -6,8 +6,8 @@ /// import type { CSSProperties } from 'react'; -import { Field } from '@base-ui-components/react/field'; import { ForwardRefExoticComponent } from 'react'; +import { Input as Input_2 } from '@base-ui-components/react/input'; import { default as React_2 } from 'react'; import * as React_3 from 'react'; import { ReactNode } from 'react'; @@ -326,26 +326,11 @@ export interface InlineProps extends SpaceProps { // @public (undocumented) export const Input: React_2.ForwardRefExoticComponent< - InputProps & React_2.RefAttributes + InputProps & React_2.RefAttributes >; // @public (undocumented) -export interface InputProps - extends React.ComponentPropsWithoutRef { - // (undocumented) - description?: string; - // (undocumented) - errorForceShow?: boolean; - // (undocumented) - errorMatch?: Field.Error.Props['match']; - // (undocumented) - errorMessage?: string; - // (undocumented) - label?: string; - // (undocumented) - placeholder?: string; - // (undocumented) - required?: boolean; +export interface InputProps extends Omit { // (undocumented) size?: 'sm' | 'md'; } diff --git a/packages/canon/src/components/Field/Field.stories.tsx b/packages/canon/src/components/Field/Field.stories.tsx new file mode 100644 index 0000000000..911bb07245 --- /dev/null +++ b/packages/canon/src/components/Field/Field.stories.tsx @@ -0,0 +1,66 @@ +/* + * 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 + + ), +}; diff --git a/packages/canon/src/components/Field/Field.styles.css b/packages/canon/src/components/Field/Field.styles.css new file mode 100644 index 0000000000..485cf32c15 --- /dev/null +++ b/packages/canon/src/components/Field/Field.styles.css @@ -0,0 +1,45 @@ +/* + * 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-caption); + font-weight: var(--canon-font-weight-regular); + color: var(--canon-text-primary); + margin-bottom: var(--canon-spacing-3xs); +} + +.canon-FieldDescription { + font-size: var(--canon-font-size-caption); + font-weight: var(--canon-font-weight-regular); + color: var(--canon-text-secondary); + margin: 0; + padding-top: var(--canon-spacing-3xs); +} + +.canon-FieldError { + font-size: var(--canon-font-size-caption); + font-weight: var(--canon-font-weight-regular); + color: var(--canon-error); + margin: 0; + padding-top: var(--canon-spacing-3xs); +} diff --git a/packages/canon/src/components/Field/Field.tsx b/packages/canon/src/components/Field/Field.tsx new file mode 100644 index 0000000000..2e512e7944 --- /dev/null +++ b/packages/canon/src/components/Field/Field.tsx @@ -0,0 +1,84 @@ +/* + * 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, + ...props +}: React.ComponentPropsWithoutRef) => ( + + {validityState => children(validityState)} + +); + +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 new file mode 100644 index 0000000000..297ed9b27b --- /dev/null +++ b/packages/canon/src/components/Field/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './Field'; diff --git a/packages/canon/src/components/Input/Input.stories.tsx b/packages/canon/src/components/Input/Input.stories.tsx index a3f2f59581..a215162688 100644 --- a/packages/canon/src/components/Input/Input.stories.tsx +++ b/packages/canon/src/components/Input/Input.stories.tsx @@ -22,93 +22,20 @@ import { Inline } from '../Inline'; const meta = { title: 'Components/Input', component: Input, - decorators: [ - Story => ( -
- -
- ), - ], - argTypes: { - label: { - control: 'text', - }, - description: { - control: 'text', - }, - placeholder: { - control: 'text', - }, - size: { - control: 'select', - options: ['sm', 'md'], - }, - }, } satisfies Meta; export default meta; type Story = StoryObj; -export const Default: Story = {}; - -export const WithPlaceholder: Story = { - args: { - placeholder: 'Enter your name', - }, -}; - -export const WithLabel: Story = { - args: { - label: 'Name', - }, -}; - -export const WithDescription: Story = { - args: { - description: 'Visible on your profile', - }, -}; - -export const WithLabelAndDescription: Story = { - args: { - label: 'Name', - description: 'Visible on your profile', - }, +export const Primary: Story = { + render: () => , }; export const Sizes: Story = { - args: { - label: 'Name', - description: 'Visible on your profile', - }, - render: args => ( + render: () => ( - - + + ), }; - -export const WithErrorValidation: Story = { - args: { - validate: value => - value !== 'Charles' ? 'Please enter a different name' : null, - required: true, - label: 'Name', - description: 'Visible on your profile', - validationMode: 'onChange', - errorMatch: 'customError', - }, -}; - -export const WithInvalidProp: Story = { - args: { - invalid: true, - required: true, - label: 'Name', - description: 'Visible on your profile', - validationMode: 'onChange', - errorForceShow: true, - validate: () => 'Stuff', - }, -}; diff --git a/packages/canon/src/components/Input/styles.css b/packages/canon/src/components/Input/Input.styles.css similarity index 59% rename from packages/canon/src/components/Input/styles.css rename to packages/canon/src/components/Input/Input.styles.css index ef4cfa2a07..f1ac7a5580 100644 --- a/packages/canon/src/components/Input/styles.css +++ b/packages/canon/src/components/Input/Input.styles.css @@ -14,21 +14,7 @@ * limitations under the License. */ -.canon-input-root { - display: flex; - flex-direction: column; - font-family: var(--canon-font-regular); - width: 100%; -} - -.canon-input-label { - font-size: var(--canon-font-size-caption); - font-weight: var(--canon-font-weight-regular); - color: var(--canon-text-primary); - margin-bottom: var(--canon-spacing-3xs); -} - -.canon-input-control { +.canon-Input { border-radius: var(--canon-border-radius-sm); border: 1px solid var(--canon-border-base); padding: var(--canon-spacing-sm); @@ -39,41 +25,28 @@ transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; } -.canon-input-control::placeholder { +.canon-Input::placeholder { color: var(--canon-text-secondary); } -.canon-input-control:hover { +.canon-Input:hover { border-color: var(--canon-border-hover); } -.canon-input-control:focus-visible { +.canon-Input:focus-visible { outline-color: var(--canon-border-selected); outline-width: 0px; border-color: var(--canon-border-selected); } -.canon-input-control[data-invalid] { +.canon-Input[data-invalid] { border-color: var(--canon-error); } -.canon-input-control-sm { +.canon-Input--size-sm { height: 2rem; } -.canon-input-control-md { +.canon-Input--size-md { height: 2.5rem; } - -.canon-input-description { - font-size: var(--canon-font-size-caption); - font-weight: var(--canon-font-weight-regular); - color: var(--canon-text-secondary); - margin-top: var(--canon-spacing-3xs); -} - -.canon-input-error { - font-size: var(--canon-font-size-caption); - color: var(--canon-error); - margin-top: var(--canon-spacing-3xs); -} diff --git a/packages/canon/src/components/Input/Input.tsx b/packages/canon/src/components/Input/Input.tsx index 688ecc5c2d..5eb68e7bf4 100644 --- a/packages/canon/src/components/Input/Input.tsx +++ b/packages/canon/src/components/Input/Input.tsx @@ -14,67 +14,30 @@ * limitations under the License. */ -'use client'; - -import React, { forwardRef } from 'react'; +import React, { ElementRef, forwardRef } from 'react'; +import { Input as InputPrimitive } from '@base-ui-components/react/input'; import clsx from 'clsx'; -import { Field } from '@base-ui-components/react/field'; import type { InputProps } from './types'; /** @public */ -const Input = forwardRef((props, ref) => { - const { - className, - label, - description, - errorMatch = 'customError', - errorForceShow = false, - errorMessage = 'An error occurred', - validationMode = 'onChange', - size = 'md', - placeholder, - required = false, - style, - ...rest - } = props; +const Input = forwardRef, InputProps>( + (props, ref) => { + const { size = 'md', className, ...rest } = props; - return ( - - {label && ( - {label} - )} - - + ); + }, +); - - - {description && ( - - {description} - - )} - - ); -}); - -Input.displayName = 'Input'; +Input.displayName = InputPrimitive.displayName; export { Input }; diff --git a/packages/canon/src/components/Input/types.ts b/packages/canon/src/components/Input/types.ts index f268273632..ac013d1bbd 100644 --- a/packages/canon/src/components/Input/types.ts +++ b/packages/canon/src/components/Input/types.ts @@ -14,17 +14,9 @@ * limitations under the License. */ -import { Field } from '@base-ui-components/react/field'; +import { Input } from '@base-ui-components/react/input'; /** @public */ -export interface InputProps - extends React.ComponentPropsWithoutRef { - label?: string; - description?: string; +export interface InputProps extends Omit { size?: 'sm' | 'md'; - placeholder?: string; - errorMatch?: Field.Error.Props['match']; - errorForceShow?: boolean; - errorMessage?: string; - required?: boolean; } diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index b7c4ac838a..bc6a7f3fe5 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -25,4 +25,5 @@ @import '../components/Table/styles.css'; @import '../components/Text/styles.css'; @import '../components/Heading/styles.css'; -@import '../components/Input/styles.css'; +@import '../components/Input/Input.styles.css'; +@import '../components/Field/Field.styles.css';