Add new FormInput component

Signed-off-by: Charles de Dreuille <charles.dedreuille@gmail.com>
This commit is contained in:
Charles de Dreuille
2025-06-16 22:41:44 +01:00
parent 08f5e42495
commit 76a58b9009
6 changed files with 278 additions and 0 deletions
@@ -0,0 +1,87 @@
/*
* 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 type { Meta, StoryObj } from '@storybook/react';
import { FormInput } from './FormInput';
import { Icon } from '../Icon';
import { Flex } from '../Flex';
const meta = {
title: 'Forms/FormInput',
component: FormInput,
argTypes: {
required: {
control: 'boolean',
},
icon: {
control: 'object',
},
},
} satisfies Meta<typeof FormInput>;
export default meta;
type Story = StoryObj<typeof meta>;
export const Default: Story = {
args: {
name: 'url',
placeholder: 'Enter a URL',
style: {
maxWidth: '300px',
},
},
};
export const Sizes: Story = {
args: {
...Default.args,
},
render: args => (
<Flex direction="row" gap="4" style={{ width: '100%', maxWidth: '600px' }}>
<FormInput {...args} size="small" icon={<Icon name="sparkling" />} />
<FormInput {...args} size="medium" icon={<Icon name="sparkling" />} />
</Flex>
),
};
export const Filled: Story = {
args: {
...Default.args,
defaultValue: 'https://example.com',
},
};
export const Disabled: Story = {
args: {
...Default.args,
disabled: true,
},
};
export const WithIcon: Story = {
args: {
...Default.args,
placeholder: 'Search...',
icon: <Icon name="search" />,
},
};
export const DisabledWithIcon: Story = {
args: {
...WithIcon.args,
disabled: true,
},
};
@@ -0,0 +1,91 @@
/*
* 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-FormInput {
display: flex;
align-items: center;
padding: 0 var(--canon-space-3);
border-radius: var(--canon-radius-3);
border: 1px solid var(--canon-border);
background-color: var(--canon-bg-surface-1);
}
.canon-FormInputIcon {
margin-right: var(--canon-space-1);
color: var(--canon-fg-primary);
flex-shrink: 0;
}
.canon-FormInputIcon[data-size='small'],
.canon-FormInputIcon[data-size='small'] svg {
width: 1rem;
height: 1rem;
}
.canon-FormInputIcon[data-size='medium'],
.canon-FormInputIcon[data-size='medium'] svg {
width: 1.25rem;
height: 1.25rem;
}
.canon-Input {
border: none;
background: none;
font-size: var(--canon-font-size-3);
font-family: var(--canon-font-regular);
font-weight: var(--canon-font-weight-regular);
color: var(--canon-fg-primary);
transition: border-color 0.2s ease-in-out, outline-color 0.2s ease-in-out;
width: 100%;
height: 100%;
cursor: inherit;
padding: 0;
}
.canon-Input::placeholder {
color: var(--canon-fg-secondary);
}
.canon-FormInput[data-size='small'] {
height: 2rem;
}
.canon-FormInput[data-size='medium'] {
height: 2.5rem;
}
.canon-Input[data-focused] {
outline-color: var(--canon-border-pressed);
outline-width: 0px;
}
.canon-FormInput:has(> .canon-Input:hover) {
border-color: var(--canon-border-hover);
}
.canon-FormInput[data-focused] {
border-color: var(--canon-border-pressed);
}
.canon-FormInput[data-invalid] {
border-color: var(--canon-fg-danger);
}
.canon-FormInput[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
border: 1px solid var(--canon-border-disabled);
}
@@ -0,0 +1,48 @@
/*
* 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 { forwardRef } from 'react';
import { Input } from 'react-aria-components';
import { useResponsiveValue } from '../../hooks/useResponsiveValue';
import clsx from 'clsx';
import type { FormInputProps } from './types';
/** @public */
export const FormInput = forwardRef<HTMLInputElement, FormInputProps>(
(props: FormInputProps, ref) => {
const { className, icon, size = 'small', ...rest } = props;
// Get the responsive value for the variant
const responsiveSize = useResponsiveValue(size);
return (
<div
className={clsx('canon-FormInput', className)}
data-size={responsiveSize}
>
{icon && (
<div className="canon-FormInputIcon" aria-hidden="true">
{icon}
</div>
)}
<Input className="canon-Input" {...rest} ref={ref} />
</div>
);
},
);
FormInput.displayName = 'FormInput';
@@ -0,0 +1,18 @@
/*
* 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 './TextField';
export type { TextFieldProps } from './types';
@@ -0,0 +1,33 @@
/*
* Copyright 2025 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 { InputProps } from 'react-aria-components';
import { ReactNode } from 'react';
import type { Breakpoint } from '../../types';
/** @public */
export interface FormInputProps extends Omit<InputProps, 'size'> {
/**
* An icon to render before the input
*/
icon?: ReactNode;
/**
* The size of the text field
* @defaultValue 'medium'
*/
size?: 'small' | 'medium' | Partial<Record<Breakpoint, 'small' | 'medium'>>;
}
+1
View File
@@ -21,6 +21,7 @@
@import '../components/DataTable/Root/DataTableRoot.styles.css';
@import '../components/DataTable/Pagination/DataTablePagination.styles.css';
@import '../components/Flex/styles.css';
@import '../components/FormInput/FormInput.styles.css';
@import '../components/Grid/styles.css';
@import '../components/Container/styles.css';
@import '../components/Icon/styles.css';