mirror of https://github.com/portainer/portainer
feat(app): added Input components [EE-2007] (#6028)
parent
b280eb6997
commit
0ee403c1b2
|
@ -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: '',
|
||||
};
|
|
@ -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();
|
||||
});
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
export { FormControl } from './FormControl';
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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))}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
};
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
export { NumberInput } from './NumberInput';
|
||||
export { TextInput } from './TextInput';
|
||||
export { Select } from './Select';
|
|
@ -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;
|
||||
}
|
Loading…
Reference in New Issue