Improve Input component
Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
@@ -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',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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
-1
@@ -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;
|
||||
}
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user