Split Field and Input

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-01-15 15:27:36 +00:00
parent 4889b98450
commit c62c91d35a
10 changed files with 249 additions and 196 deletions
+3 -18
View File
@@ -6,8 +6,8 @@
/// <reference types="react" />
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<HTMLDivElement>
InputProps & React_2.RefAttributes<HTMLInputElement>
>;
// @public (undocumented)
export interface InputProps
extends React.ComponentPropsWithoutRef<typeof Field.Root> {
// (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<Input_2.Props, 'size'> {
// (undocumented)
size?: 'sm' | 'md';
}
@@ -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<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.Error>Error</Field.Error>
</Field.Root>
),
};
export const WithLabelAndDescription: Story = {
render: () => (
<Field.Root>
<Field.Label>Label</Field.Label>
<Input />
<Field.Description>Description</Field.Description>
</Field.Root>
),
};
export const WithError: Story = {
render: () => (
<Field.Root
validate={value =>
value !== 'Backstage' ? 'Please enter a different name' : null
}
validationMode="onChange"
>
<Field.Label>Name</Field.Label>
<Input />
<Field.Description>
An error will show if the value is not Backstage
</Field.Description>
<Field.Error match="customError">Error</Field.Error>
</Field.Root>
),
};
@@ -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);
}
@@ -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<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>
);
export const Field = {
Root: FieldRoot,
Label: FieldLabel,
Description: FieldDescription,
Error: FieldError,
Validity: FieldValidity,
};
@@ -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';
@@ -22,93 +22,20 @@ import { Inline } from '../Inline';
const meta = {
title: 'Components/Input',
component: Input,
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 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: () => <Input className="canon-Input" />,
};
export const Sizes: Story = {
args: {
label: 'Name',
description: 'Visible on your profile',
},
render: args => (
render: () => (
<Inline>
<Input size="sm" {...args} />
<Input size="md" {...args} />
<Input size="sm" />
<Input size="md" />
</Inline>
),
};
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',
},
};
@@ -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);
}
+18 -55
View File
@@ -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<HTMLDivElement, InputProps>((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<ElementRef<typeof InputPrimitive>, InputProps>(
(props, ref) => {
const { size = 'md', className, ...rest } = props;
return (
<Field.Root
ref={ref}
className={clsx('canon-input-root', className)}
style={style}
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',
})}
return (
<InputPrimitive
ref={ref}
className={clsx(
'canon-Input',
size === 'sm' ? 'canon-Input--size-sm' : 'canon-Input--size-md',
className,
)}
{...rest}
/>
);
},
);
<Field.Error
className="canon-input-error"
match={errorMatch}
forceShow={errorForceShow}
// children={errorForceShow ? 'An error occurred' : null}
/>
{description && (
<Field.Description className="canon-input-description">
{description}
</Field.Description>
)}
</Field.Root>
);
});
Input.displayName = 'Input';
Input.displayName = InputPrimitive.displayName;
export { Input };
+2 -10
View File
@@ -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<typeof Field.Root> {
label?: string;
description?: string;
export interface InputProps extends Omit<Input.Props, 'size'> {
size?: 'sm' | 'md';
placeholder?: string;
errorMatch?: Field.Error.Props['match'];
errorForceShow?: boolean;
errorMessage?: string;
required?: boolean;
}
+2 -1
View File
@@ -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';