Improve Input component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-01-10 23:37:41 +00:00
parent 3782558d0a
commit f3c58385c2
10 changed files with 218 additions and 174 deletions
@@ -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<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.Root>
),
};
@@ -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<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,
...props
}: React.ComponentPropsWithoutRef<typeof FieldPrimitive.Validity>) => (
<FieldPrimitive.Validity {...props}>
{validityState => children(validityState)}
</FieldPrimitive.Validity>
);
/** @public */
export const Field = {
Root: FieldRoot,
Label: FieldLabel,
Description: FieldDescription,
Error: FieldError,
Validity: FieldValidity,
};
@@ -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;
}
@@ -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 => (
<div style={{ maxWidth: '400px' }}>
<Story />
</div>
),
],
argTypes: {
label: {
control: 'text',
},
description: {
control: 'text',
},
placeholder: {
control: 'text',
},
size: {
control: 'select',
options: ['sm', 'md'],
},
},
} satisfies Meta<typeof Input>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Primary: Story = {
render: () => <Input />,
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 => (
<Inline>
<Input size="sm" {...args} />
<Input size="md" {...args} />
</Inline>
),
};
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',
},
};
+51 -13
View File
@@ -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<typeof InputPrimitive>,
React.ComponentPropsWithoutRef<typeof InputPrimitive>
>(({ className, ...props }, ref) => (
<InputPrimitive
ref={ref}
className={clsx('canon-input', className)}
{...props}
/>
));
Input.displayName = InputPrimitive.displayName;
const Input = forwardRef<HTMLDivElement, InputProps>((props, ref) => {
const {
className,
label,
description,
match = 'valueMissing',
validationMode = 'onChange',
size = 'md',
placeholder,
error = 'Please enter your a value',
required = false,
...rest
} = props;
return (
<Field.Root
ref={ref}
className={clsx('canon-input-root', className)}
validationMode={validationMode}
{...rest}
>
{label && (
<Field.Label className="canon-input-label">{label}</Field.Label>
)}
<Field.Control
placeholder={placeholder}
required={required}
className={clsx('canon-input-control', {
'canon-input-control-sm': size === 'sm',
'canon-input-control-md': size === 'md',
})}
/>
<Field.Error className="canon-input-error" match={match}>
{error}
</Field.Error>
{description && (
<Field.Description className="canon-input-description">
{description}
</Field.Description>
)}
</Field.Root>
);
});
Input.displayName = 'Input';
export { Input };
@@ -13,4 +13,5 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export { Input } from './Input';
@@ -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);
}
@@ -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<typeof Field.Root> {
label?: string;
description?: string;
size?: 'sm' | 'md';
placeholder?: string;
match?: Field.Error.Props['match'];
error?: string;
required?: boolean;
}
+1 -1
View File
@@ -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';
+3 -2
View File
@@ -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;