feat(app): added Input components [EE-2007] (#6028)

pull/5065/head
Marcelo Rydel 2021-11-17 11:32:57 -07:00 committed by GitHub
parent b280eb6997
commit 0ee403c1b2
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 2840 additions and 3156 deletions

View File

@ -0,0 +1,54 @@
import { Meta } from '@storybook/react';
import { useState } from 'react';
import { TextInput, Select } from '../Input';
import { FormControl } from './FormControl';
export default {
title: 'Components/Form/Control',
} as Meta;
interface TextFieldProps {
label: string;
tooltip?: string;
}
export function TextField({ label, tooltip = '' }: TextFieldProps) {
const [value, setValue] = useState('');
const inputId = 'input';
return (
<FormControl inputId={inputId} label={label} tooltip={tooltip}>
<TextInput id={inputId} type="text" value={value} onChange={setValue} />
</FormControl>
);
}
TextField.args = {
label: 'label',
tooltip: '',
};
export function SelectField({ label, tooltip = '' }: TextFieldProps) {
const options = [
{ value: 1, label: 'one' },
{ value: 2, label: 'two' },
];
const [value, setValue] = useState(0);
const inputId = 'input';
return (
<FormControl inputId={inputId} label={label} tooltip={tooltip}>
<Select
className="form-control"
value={value}
onChange={(value) => setValue(value)}
options={options}
/>
</FormControl>
);
}
SelectField.args = {
label: 'select',
tooltip: '',
};

View File

@ -0,0 +1,30 @@
import { render } from '@testing-library/react';
import { FormControl, Props } from './FormControl';
function renderDefault({
inputId = 'id',
label,
tooltip = '',
errors,
}: Partial<Props>) {
return render(
<FormControl
inputId={inputId}
label={label}
tooltip={tooltip}
errors={errors}
>
<input />
</FormControl>
);
}
test('should display a Input component', async () => {
const label = 'test label';
const { findByText } = renderDefault({ label });
const inputElem = await findByText(label);
expect(inputElem).toBeTruthy();
});

View File

@ -0,0 +1,41 @@
import { PropsWithChildren, ReactNode } from 'react';
import { Tooltip } from '@/portainer/components/Tooltip';
export interface Props {
inputId: string;
label: string | ReactNode;
tooltip?: string;
children: ReactNode;
errors?: string | ReactNode;
}
export function FormControl({
inputId,
label,
tooltip = '',
children,
errors,
}: PropsWithChildren<Props>) {
return (
<div>
<div className="form-group">
<label
htmlFor={inputId}
className="col-sm-3 col-lg-2 control-label text-left"
>
{label}
{tooltip && <Tooltip message={tooltip} />}
</label>
<div className="col-sm-9 col-lg-10">{children}</div>
</div>
{errors && (
<div className="form-group col-md-12">
<div className="small text-warning">{errors}</div>
</div>
)}
</div>
);
}

View File

@ -0,0 +1 @@
export { FormControl } from './FormControl';

View File

@ -0,0 +1,41 @@
import clsx from 'clsx';
import { HTMLInputTypeAttribute } from 'react';
import { InputProps } from './types';
interface Props extends InputProps {
type?: HTMLInputTypeAttribute;
onChange(value: string): void;
value: number | string;
component?: 'input' | 'textarea';
rows?: number;
readonly?: boolean;
}
export function BaseInput({
component = 'input',
value,
disabled,
id,
readonly,
required,
type,
className,
rows,
onChange,
}: Props) {
const Component = component;
return (
<Component
value={value}
disabled={disabled}
id={id}
readOnly={readonly}
required={required}
type={type}
className={clsx(className, 'form-control')}
onChange={(e) => onChange(e.target.value)}
rows={rows}
/>
);
}

View File

@ -0,0 +1,25 @@
import { Meta, Story } from '@storybook/react';
import { useState } from 'react';
import { NumberInput } from './NumberInput';
export default {
title: 'Components/Form/NumberInput',
args: {
disabled: false,
},
} as Meta;
interface Args {
disabled?: boolean;
}
export function Example({ disabled }: Args) {
const [value, setValue] = useState(0);
return <NumberInput value={value} onChange={setValue} disabled={disabled} />;
}
export const DisabledNumberInput: Story<Args> = Example.bind({});
DisabledNumberInput.args = {
disabled: true,
};

View File

@ -0,0 +1,33 @@
import clsx from 'clsx';
import { BaseInput } from './BaseInput';
import { InputProps } from './types';
interface Props extends InputProps {
value: number;
readonly?: boolean;
onChange(value: number): void;
}
export function NumberInput({
disabled,
required,
id,
value,
className,
readonly,
onChange,
}: Props) {
return (
<BaseInput
id={id}
type="number"
className={clsx(className, 'form-control')}
value={value}
disabled={disabled}
readonly={readonly}
required={required}
onChange={(value) => onChange(parseFloat(value))}
/>
);
}

View File

@ -0,0 +1,36 @@
import { Meta, Story } from '@storybook/react';
import { useState } from 'react';
import { Select } from './Select';
export default {
title: 'Components/Form/Select',
args: {
disabled: false,
},
} as Meta;
interface Args {
disabled?: boolean;
}
export function Example({ disabled }: Args) {
const [value, setValue] = useState(0);
const options = [
{ value: 1, label: 'one' },
{ value: 2, label: 'two' },
];
return (
<Select<number>
value={value}
onChange={setValue}
disabled={disabled}
options={options}
/>
);
}
export const DisabledSelect: Story<Args> = Example.bind({});
DisabledSelect.args = {
disabled: true,
};

View File

@ -0,0 +1,44 @@
import clsx from 'clsx';
import { FormEvent } from 'react';
import { ChangeProps, InputProps } from './types';
interface Option<T extends string | number> {
value: T;
label: string;
}
interface Props<T extends string | number> extends InputProps, ChangeProps<T> {
options: Option<T>[];
}
export function Select<T extends number | string>({
options,
onChange,
value,
className,
disabled,
id,
required,
}: Props<T>) {
return (
<select
value={value}
disabled={disabled}
id={id}
required={required}
className={clsx(className, 'form-control')}
onChange={handleChange}
>
{options.map((item) => (
<option value={item.value}>{item.label}</option>
))}
</select>
);
function handleChange(e: FormEvent<HTMLSelectElement>) {
const { selectedIndex } = e.currentTarget;
const option = options[selectedIndex];
onChange(option.value);
}
}

View File

@ -0,0 +1,25 @@
import { Meta, Story } from '@storybook/react';
import { useState } from 'react';
import { TextInput } from './TextInput';
export default {
title: 'Components/Form/TextInput',
args: {
disabled: false,
},
} as Meta;
interface Args {
disabled?: boolean;
}
export function TextField({ disabled }: Args) {
const [value, setValue] = useState('');
return <TextInput value={value} onChange={setValue} disabled={disabled} />;
}
export const DisabledTextField: Story<Args> = TextField.bind({});
DisabledTextField.args = {
disabled: true,
};

View File

@ -0,0 +1,34 @@
import clsx from 'clsx';
import { HTMLInputTypeAttribute } from 'react';
import { BaseInput } from './BaseInput';
import { ChangeProps, InputProps } from './types';
interface TextInputProps extends InputProps, ChangeProps<string> {
type?: HTMLInputTypeAttribute;
readonly?: boolean;
}
export function TextInput({
id,
type = 'text',
value,
className,
onChange,
disabled,
readonly,
required,
}: TextInputProps) {
return (
<BaseInput
id={id}
type={type}
className={clsx(className, 'form-control')}
value={value}
onChange={onChange}
disabled={disabled}
readonly={readonly}
required={required}
/>
);
}

View File

@ -0,0 +1,25 @@
import { BaseInput } from './BaseInput';
import { ChangeProps, InputProps } from './types';
interface Props extends InputProps, ChangeProps<string> {
rows?: number;
}
export function Textarea({
rows,
className,
onChange,
value,
id,
}: Props & InputProps) {
return (
<BaseInput
component="textarea"
id={id}
rows={rows}
className={className}
value={value}
onChange={onChange}
/>
);
}

View File

@ -0,0 +1,3 @@
export { NumberInput } from './NumberInput';
export { TextInput } from './TextInput';
export { Select } from './Select';

View File

@ -0,0 +1,11 @@
export interface InputProps {
id?: string;
className?: string;
required?: boolean;
disabled?: boolean;
}
export interface ChangeProps<T> {
value: T;
onChange(value: T): void;
}

5593
yarn.lock

File diff suppressed because it is too large Load Diff