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