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