From f3c58385c26f833213a049beb4de6102d1d4db26 Mon Sep 17 00:00:00 2001 From: Charles de Dreuille Date: Fri, 10 Jan 2025 23:37:41 +0000 Subject: [PATCH] Improve Input component Signed-off-by: Charles de Dreuille --- .../src/components/Field/Field.stories.tsx | 39 --------- packages/canon/src/components/Field/Field.tsx | 85 ------------------- .../canon/src/components/Field/styles.css | 29 ------- .../src/components/Input/Input.stories.tsx | 75 +++++++++++++++- packages/canon/src/components/Input/Input.tsx | 64 +++++++++++--- packages/canon/src/components/Input/index.ts | 1 + .../canon/src/components/Input/styles.css | 78 +++++++++++++++++ .../{Field/index.ts => Input/types.ts} | 14 ++- packages/canon/src/css/components.css | 2 +- packages/canon/src/css/core.css | 5 +- 10 files changed, 218 insertions(+), 174 deletions(-) delete mode 100644 packages/canon/src/components/Field/Field.stories.tsx delete mode 100644 packages/canon/src/components/Field/Field.tsx delete mode 100644 packages/canon/src/components/Field/styles.css create mode 100644 packages/canon/src/components/Input/styles.css rename packages/canon/src/components/{Field/index.ts => Input/types.ts} (65%) 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 d6f061ac05..0000000000 --- a/packages/canon/src/components/Field/Field.stories.tsx +++ /dev/null @@ -1,39 +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, - parameters: { - layout: 'centered', - }, -} satisfies Meta; - -export default meta; -type Story = StoryObj; - -export const Default: Story = { - render: () => ( - - Label - - - ), -}; diff --git a/packages/canon/src/components/Field/Field.tsx b/packages/canon/src/components/Field/Field.tsx deleted file mode 100644 index a121baab9f..0000000000 --- a/packages/canon/src/components/Field/Field.tsx +++ /dev/null @@ -1,85 +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, - ...props -}: React.ComponentPropsWithoutRef) => ( - - {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/styles.css b/packages/canon/src/components/Field/styles.css deleted file mode 100644 index 728ee87f31..0000000000 --- a/packages/canon/src/components/Field/styles.css +++ /dev/null @@ -1,29 +0,0 @@ -.canon-fieldRoot { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.canon-fieldLabel { - font-size: 0.875rem; - font-weight: 500; - line-height: 1.25rem; -} - -.canon-fieldControl { - display: flex; - flex-direction: column; - gap: 0.5rem; -} - -.canon-fieldDescription { - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem; -} - -.canon-fieldError { - font-size: 0.875rem; - font-weight: 400; - line-height: 1.25rem; -} diff --git a/packages/canon/src/components/Input/Input.stories.tsx b/packages/canon/src/components/Input/Input.stories.tsx index 54f8a86768..140696e0f5 100644 --- a/packages/canon/src/components/Input/Input.stories.tsx +++ b/packages/canon/src/components/Input/Input.stories.tsx @@ -17,18 +17,85 @@ import React from 'react'; import type { Meta, StoryObj } from '@storybook/react'; import { Input } from './Input'; +import { Inline } from '../Inline'; const meta = { title: 'Components/Input', component: Input, - parameters: { - layout: 'centered', + 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 Primary: Story = { - render: () => , +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 Sizes: Story = { + args: { + label: 'Name', + description: 'Visible on your profile', + }, + render: args => ( + + + + + ), +}; + +export const WithError: Story = { + args: { + invalid: true, + validate: value => (value ? null : 'Please enter your name'), + required: true, + label: 'Name', + description: 'Visible on your profile', + error: 'Please enter your name', + }, }; diff --git a/packages/canon/src/components/Input/Input.tsx b/packages/canon/src/components/Input/Input.tsx index ca90e94f8f..e62301c5cf 100644 --- a/packages/canon/src/components/Input/Input.tsx +++ b/packages/canon/src/components/Input/Input.tsx @@ -13,22 +13,60 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { Input as InputPrimitive } from '@base-ui-components/react/input'; +import React, { forwardRef } from 'react'; import clsx from 'clsx'; +import { Field } from '@base-ui-components/react/field'; +import type { InputProps } from './types'; /** @public */ -const Input = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)); -Input.displayName = InputPrimitive.displayName; +const Input = forwardRef((props, ref) => { + const { + className, + label, + description, + match = 'valueMissing', + validationMode = 'onChange', + size = 'md', + placeholder, + error = 'Please enter your a value', + required = false, + ...rest + } = props; + + return ( + + {label && ( + {label} + )} + + + + + {error} + + + {description && ( + + {description} + + )} + + ); +}); + +Input.displayName = 'Input'; export { Input }; diff --git a/packages/canon/src/components/Input/index.ts b/packages/canon/src/components/Input/index.ts index a757e30478..049076e1c6 100644 --- a/packages/canon/src/components/Input/index.ts +++ b/packages/canon/src/components/Input/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { Input } from './Input'; diff --git a/packages/canon/src/components/Input/styles.css b/packages/canon/src/components/Input/styles.css new file mode 100644 index 0000000000..f8baae4ff4 --- /dev/null +++ b/packages/canon/src/components/Input/styles.css @@ -0,0 +1,78 @@ +/* + * 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-input-root { + display: flex; + flex-direction: column; + font-family: var(--canon-font-regular); +} + +.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 { + border-radius: var(--canon-border-radius-sm); + border: 1px solid var(--canon-border-base); + padding: var(--canon-spacing-sm); + background-color: var(--canon-surface-1); + font-size: var(--canon-font-size-body); + font-weight: var(--canon-font-weight-regular); + color: var(--canon-text-primary); + transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out; +} + +.canon-input-control::placeholder { + color: var(--canon-text-secondary); +} + +.canon-input-control:hover { + border-color: var(--canon-border-hover); +} + +.canon-input-control:focus-visible { + outline-color: var(--canon-border-selected); + outline-width: 0px; + border-color: var(--canon-border-selected); +} + +.canon-input-control[data-invalid] { + border-color: var(--canon-error); +} + +.canon-input-control-sm { + height: 2rem; +} + +.canon-input-control-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/Field/index.ts b/packages/canon/src/components/Input/types.ts similarity index 65% rename from packages/canon/src/components/Field/index.ts rename to packages/canon/src/components/Input/types.ts index b96bd91ebe..c4ebff83b5 100644 --- a/packages/canon/src/components/Field/index.ts +++ b/packages/canon/src/components/Input/types.ts @@ -13,4 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { Field } from './Field'; + +import { Field } from '@base-ui-components/react/field'; + +export interface InputProps + extends React.ComponentPropsWithoutRef { + label?: string; + description?: string; + size?: 'sm' | 'md'; + placeholder?: string; + match?: Field.Error.Props['match']; + error?: string; + required?: boolean; +} diff --git a/packages/canon/src/css/components.css b/packages/canon/src/css/components.css index 2dc13a7677..b7c4ac838a 100644 --- a/packages/canon/src/css/components.css +++ b/packages/canon/src/css/components.css @@ -25,4 +25,4 @@ @import '../components/Table/styles.css'; @import '../components/Text/styles.css'; @import '../components/Heading/styles.css'; -@import '../components/Field/styles.css'; +@import '../components/Input/styles.css'; diff --git a/packages/canon/src/css/core.css b/packages/canon/src/css/core.css index 4f15d571c7..b5008fb803 100644 --- a/packages/canon/src/css/core.css +++ b/packages/canon/src/css/core.css @@ -82,7 +82,7 @@ --canon-border-hover: rgba(0, 0, 0, 0.2); --canon-border-warning: #e36d05; --canon-border-error: #e22b2b; - --canon-border-selected: #1db954; + --canon-border-selected: rgba(0, 0, 0, 0.4); /* States - Add more states */ --canon-error: #f50000; @@ -152,9 +152,10 @@ /* Borders */ --canon-border-base: rgba(255, 255, 255, 0.2); + --canon-border-hover: rgba(255, 255, 255, 0.3); --canon-border-warning: #f50000; --canon-border-error: #f87503; - --canon-border-selected: #25d262; + --canon-border-selected: rgba(255, 255, 255, 0.4); /* States - Add more states */ --canon-error: #f50000;