mirror of https://github.com/portainer/portainer
feat(app): introduce input-group component [EE-2062] (#6135)
parent
9ad626b36e
commit
830286c332
|
@ -836,6 +836,10 @@ json-tree .branch-preview {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.space-y-8 > * + * {
|
||||||
|
margin-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
.my-8 {
|
.my-8 {
|
||||||
margin-top: 2rem;
|
margin-top: 2rem;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
|
|
|
@ -16,6 +16,7 @@ export function NumberInput({
|
||||||
value,
|
value,
|
||||||
className,
|
className,
|
||||||
readonly,
|
readonly,
|
||||||
|
placeholder,
|
||||||
onChange,
|
onChange,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
return (
|
return (
|
||||||
|
@ -27,6 +28,7 @@ export function NumberInput({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
required={required}
|
required={required}
|
||||||
|
placeholder={placeholder}
|
||||||
onChange={(value) => onChange(parseFloat(value))}
|
onChange={(value) => onChange(parseFloat(value))}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -20,6 +20,7 @@ export function Select<T extends number | string>({
|
||||||
disabled,
|
disabled,
|
||||||
id,
|
id,
|
||||||
required,
|
required,
|
||||||
|
placeholder,
|
||||||
}: Props<T>) {
|
}: Props<T>) {
|
||||||
return (
|
return (
|
||||||
<select
|
<select
|
||||||
|
@ -28,6 +29,7 @@ export function Select<T extends number | string>({
|
||||||
id={id}
|
id={id}
|
||||||
required={required}
|
required={required}
|
||||||
className={clsx(className, 'form-control')}
|
className={clsx(className, 'form-control')}
|
||||||
|
placeholder={placeholder}
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
>
|
>
|
||||||
{options.map((item) => (
|
{options.map((item) => (
|
||||||
|
|
|
@ -17,6 +17,7 @@ export function TextInput({
|
||||||
disabled,
|
disabled,
|
||||||
readonly,
|
readonly,
|
||||||
required,
|
required,
|
||||||
|
placeholder,
|
||||||
}: TextInputProps) {
|
}: TextInputProps) {
|
||||||
return (
|
return (
|
||||||
<BaseInput
|
<BaseInput
|
||||||
|
@ -28,6 +29,7 @@ export function TextInput({
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
readonly={readonly}
|
readonly={readonly}
|
||||||
required={required}
|
required={required}
|
||||||
|
placeholder={placeholder}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,9 @@ export function Textarea({
|
||||||
onChange,
|
onChange,
|
||||||
value,
|
value,
|
||||||
id,
|
id,
|
||||||
|
placeholder,
|
||||||
|
disabled,
|
||||||
|
required,
|
||||||
}: Props & InputProps) {
|
}: Props & InputProps) {
|
||||||
return (
|
return (
|
||||||
<BaseInput
|
<BaseInput
|
||||||
|
@ -20,6 +23,9 @@ export function Textarea({
|
||||||
className={className}
|
className={className}
|
||||||
value={value}
|
value={value}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
|
placeholder={placeholder}
|
||||||
|
disabled={disabled}
|
||||||
|
required={required}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ export interface InputProps {
|
||||||
className?: string;
|
className?: string;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ChangeProps<T> {
|
export interface ChangeProps<T> {
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
import { Meta } from '@storybook/react';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
|
import { InputGroup } from '.';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
component: InputGroup,
|
||||||
|
title: 'Components/Form/InputGroup',
|
||||||
|
} as Meta;
|
||||||
|
|
||||||
|
export function BasicExample() {
|
||||||
|
const [value1, setValue1] = useState('');
|
||||||
|
const [valueNumber, setValueNumber] = useState(0);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.Addon>@</InputGroup.Addon>
|
||||||
|
<InputGroup.Input
|
||||||
|
value={value1}
|
||||||
|
onChange={setValue1}
|
||||||
|
placeholder="Username"
|
||||||
|
aria-describedby="basic-addon1"
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.Input
|
||||||
|
value={value1}
|
||||||
|
onChange={setValue1}
|
||||||
|
placeholder="Recipient's username"
|
||||||
|
aria-describedby="basic-addon2"
|
||||||
|
/>
|
||||||
|
<InputGroup.Addon>@example.com</InputGroup.Addon>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.Addon>$</InputGroup.Addon>
|
||||||
|
<InputGroup.NumberInput
|
||||||
|
value={valueNumber}
|
||||||
|
onChange={setValueNumber}
|
||||||
|
aria-label="Amount (to the nearest dollar)"
|
||||||
|
/>
|
||||||
|
<InputGroup.Addon>.00</InputGroup.Addon>
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<label htmlFor="basic-url">Your vanity URL</label>
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.Addon>https://example.com/users/</InputGroup.Addon>
|
||||||
|
<InputGroup.Input
|
||||||
|
value={value1}
|
||||||
|
onChange={setValue1}
|
||||||
|
id="basic-url"
|
||||||
|
aria-describedby="basic-addon3"
|
||||||
|
/>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Addons() {
|
||||||
|
const [value1, setValue1] = useState('');
|
||||||
|
const [value2, setValue2] = useState('');
|
||||||
|
return (
|
||||||
|
<div className="row">
|
||||||
|
<div className="col-lg-6">
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.ButtonWrapper>
|
||||||
|
<button className="btn btn-default" type="button">
|
||||||
|
Go!
|
||||||
|
</button>
|
||||||
|
</InputGroup.ButtonWrapper>
|
||||||
|
<InputGroup.Input value={value1} onChange={setValue1} />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
<div className="col-lg-6">
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.Input value={value2} onChange={setValue2} />
|
||||||
|
<InputGroup.Addon>
|
||||||
|
<input type="checkbox" />
|
||||||
|
</InputGroup.Addon>
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Sizing() {
|
||||||
|
const [value, setValue] = useState('');
|
||||||
|
return (
|
||||||
|
<div className="space-y-8">
|
||||||
|
<InputGroup size="small">
|
||||||
|
<InputGroup.Addon>Small</InputGroup.Addon>
|
||||||
|
<InputGroup.Input value={value} onChange={setValue} />
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputGroup>
|
||||||
|
<InputGroup.Addon>Default</InputGroup.Addon>
|
||||||
|
<InputGroup.Input value={value} onChange={setValue} />
|
||||||
|
</InputGroup>
|
||||||
|
|
||||||
|
<InputGroup size="large">
|
||||||
|
<InputGroup.Addon>Large</InputGroup.Addon>
|
||||||
|
<InputGroup.Input value={value} onChange={setValue} />
|
||||||
|
</InputGroup>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
|
@ -0,0 +1,37 @@
|
||||||
|
import clsx from 'clsx';
|
||||||
|
import { createContext, PropsWithChildren, useContext } from 'react';
|
||||||
|
|
||||||
|
const Context = createContext<null | boolean>(null);
|
||||||
|
|
||||||
|
type Size = 'small' | 'large';
|
||||||
|
|
||||||
|
export function useInputGroupContext() {
|
||||||
|
const context = useContext(Context);
|
||||||
|
|
||||||
|
if (context == null) {
|
||||||
|
throw new Error('Should be inside a InputGroup component');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
size?: Size;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function InputGroup({ children, size }: PropsWithChildren<Props>) {
|
||||||
|
return (
|
||||||
|
<Context.Provider value>
|
||||||
|
<div className={clsx('input-group', sizeClass(size))}>{children}</div>
|
||||||
|
</Context.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function sizeClass(size?: Size) {
|
||||||
|
switch (size) {
|
||||||
|
case 'large':
|
||||||
|
return 'input-group-lg';
|
||||||
|
case 'small':
|
||||||
|
return 'input-group-sm';
|
||||||
|
default:
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { useInputGroupContext } from './InputGroup';
|
||||||
|
|
||||||
|
export function InputGroupAddon({ children }: PropsWithChildren<unknown>) {
|
||||||
|
useInputGroupContext();
|
||||||
|
|
||||||
|
return <span className="input-group-addon">{children}</span>;
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
import { PropsWithChildren } from 'react';
|
||||||
|
|
||||||
|
import { useInputGroupContext } from './InputGroup';
|
||||||
|
|
||||||
|
export function InputGroupButtonWrapper({
|
||||||
|
children,
|
||||||
|
}: PropsWithChildren<unknown>) {
|
||||||
|
useInputGroupContext();
|
||||||
|
|
||||||
|
return <span className="input-group-btn">{children}</span>;
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
import { NumberInput, TextInput } from '../Input';
|
||||||
|
|
||||||
|
import { InputGroup as MainComponent } from './InputGroup';
|
||||||
|
import { InputGroupAddon } from './InputGroupAddon';
|
||||||
|
import { InputGroupButtonWrapper } from './InputGroupButtonWrapper';
|
||||||
|
|
||||||
|
interface InputGroupSubComponents {
|
||||||
|
Addon: typeof InputGroupAddon;
|
||||||
|
ButtonWrapper: typeof InputGroupButtonWrapper;
|
||||||
|
Input: typeof TextInput;
|
||||||
|
NumberInput: typeof NumberInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
const InputGroup: typeof MainComponent &
|
||||||
|
InputGroupSubComponents = MainComponent as typeof MainComponent &
|
||||||
|
InputGroupSubComponents;
|
||||||
|
|
||||||
|
InputGroup.Addon = InputGroupAddon;
|
||||||
|
InputGroup.ButtonWrapper = InputGroupButtonWrapper;
|
||||||
|
InputGroup.Input = TextInput;
|
||||||
|
InputGroup.NumberInput = NumberInput;
|
||||||
|
|
||||||
|
export { InputGroup };
|
Loading…
Reference in New Issue