Input: Width prop (#23615)

* Add width property

* Remove unused import

* Spelling mistake

* Add width to interface

* Make width optional

* Remove size

* Update snapshot

* Remove size from places

* Add size prop for button

* Update width

* Update snapshots
This commit is contained in:
Tobias Skarhed
2020-04-21 10:42:57 +02:00
committed by GitHub
parent a2d741f60f
commit 9bbc007cb9
40 changed files with 94 additions and 131 deletions

View File

@@ -57,7 +57,6 @@ export const withCustomValue = () => {
formatCreateLabel={val => onCreateLabel + val}
initialValue="Custom Initial Value"
onSelect={val => console.log(val)}
size="md"
/>
);
};

View File

@@ -3,7 +3,6 @@ import { Icon } from '../Icon/Icon';
import RCCascader from 'rc-cascader';
import { Select } from '../Select/Select';
import { FormInputSize } from '../Forms/types';
import { Input } from '../Input/Input';
import { SelectableValue } from '@grafana/data';
import { css } from 'emotion';
@@ -15,7 +14,8 @@ interface CascaderProps {
placeholder?: string;
options: CascaderOption[];
onSelect(val: string): void;
size?: FormInputSize;
/** Sets the width to a multiple of 8px. Should only be used with inline forms. Setting width of the container is preferred in other cases.*/
width?: number;
initialValue?: string;
allowCustomValue?: boolean;
/** A function for formatting the message for custom value creation. Only applies when allowCustomValue is set to true*/
@@ -174,7 +174,7 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
};
render() {
const { size, allowCustomValue, placeholder } = this.props;
const { allowCustomValue, placeholder, width } = this.props;
const { focusCascade, isSearching, searchableOptions, rcValue, activeLabel } = this.state;
return (
@@ -187,9 +187,9 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
onChange={this.onSelect}
onBlur={this.onBlur}
options={searchableOptions}
size={size}
onCreateOption={this.onCreateOption}
formatCreateLabel={this.props.formatCreateLabel}
width={width}
/>
) : (
<RCCascader
@@ -206,7 +206,7 @@ export class Cascader extends React.PureComponent<CascaderProps, CascaderState>
>
<div className={disableDivFocus}>
<Input
size={size}
width={width}
placeholder={placeholder}
onBlur={this.onBlurCascade}
value={activeLabel}

View File

@@ -70,24 +70,18 @@ const renderForm = (defaultValues?: Partial<FormDTO>) => (
<Legend>Edit user</Legend>
<Field label="Name" invalid={!!errors.name} error="Name is required">
<Input name="name" placeholder="Roger Waters" size="md" ref={register({ required: true })} />
<Input name="name" placeholder="Roger Waters" ref={register({ required: true })} />
</Field>
<Field label="Email" invalid={!!errors.email} error="E-mail is required">
<Input
id="email"
name="email"
placeholder="roger.waters@grafana.com"
size="md"
ref={register({ required: true })}
/>
<Input id="email" name="email" placeholder="roger.waters@grafana.com" ref={register({ required: true })} />
</Field>
<Field label="Username">
<Input name="username" placeholder="mr.waters" size="md" ref={register} />
<Input name="username" placeholder="mr.waters" ref={register} />
</Field>
<Field label="Nested object">
<Input name="nested.path" placeholder="Nested path" size="md" ref={register} />
<Input name="nested.path" placeholder="Nested path" ref={register} />
</Field>
<Field label="Textarea" invalid={!!errors.text} error="Text is required">
@@ -185,7 +179,6 @@ export const asyncValidation = () => {
<Input
name="name"
placeholder="Roger Waters"
size="md"
ref={register({ validate: validateAsync(passAsyncValidation) })}
/>
</Field>

View File

@@ -1,6 +1,7 @@
import React, { useEffect } from 'react';
import { useForm, Mode, OnSubmit, DeepPartial } from 'react-hook-form';
import { FormAPI } from '../../types';
import { css } from 'emotion';
interface FormProps<T> {
validateOn?: Mode;
@@ -9,6 +10,8 @@ interface FormProps<T> {
defaultValues?: DeepPartial<T>;
onSubmit: OnSubmit<T>;
children: (api: FormAPI<T>) => React.ReactNode;
/** Sets max-width for container. Use it instead of setting individual widths on inputs.*/
maxWidth?: number;
}
export function Form<T>({
@@ -18,6 +21,7 @@ export function Form<T>({
validateFieldsOnMount,
children,
validateOn = 'onSubmit',
maxWidth = 400,
}: FormProps<T>) {
const { handleSubmit, register, errors, control, triggerValidation, getValues, formState } = useForm<T>({
mode: validateOn,
@@ -30,5 +34,14 @@ export function Form<T>({
}
}, []);
return <form onSubmit={handleSubmit(onSubmit)}>{children({ register, errors, control, getValues, formState })}</form>;
return (
<form
className={css`
max-width: ${maxWidth}px;
`}
onSubmit={handleSubmit(onSubmit)}
>
{children({ register, errors, control, getValues, formState })}
</form>
);
}

View File

@@ -12,11 +12,11 @@ Used for regular text input. For an array of data or tree-structured data, consi
To add more context to the input you can add either text or an icon before or after the input. You can use the `prefix` and `suffix` props for this. Try some examples in the canvas!
```jsx
<Input prefix={<Icon name="search" />} size="sm" />
<Input prefix={<Icon name="search" />} />
```
<Preview>
<Input prefix={<Icon name="search" />} size="sm" />
<Input prefix={<Icon name="search" />} />
</Preview>
## Usage in forms with Field

View File

@@ -50,6 +50,7 @@ export const simple = () => {
const VISUAL_GROUP = 'Visual options';
// ---
const width = number('Width', 0, undefined, VISUAL_GROUP);
const placeholder = text('Placeholder', 'Enter your name here...', VISUAL_GROUP);
const before = boolean('Addon before', false, VISUAL_GROUP);
const after = boolean('Addon after', false, VISUAL_GROUP);
@@ -84,6 +85,7 @@ export const simple = () => {
<div style={{ width: containerWidth }}>
<Input
disabled={disabled}
width={width}
prefix={prefixEl}
invalid={invalid}
suffix={suffixEl}

View File

@@ -1,13 +1,14 @@
import React, { HTMLProps, ReactNode } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { css, cx } from 'emotion';
import { getFocusStyle, inputSizes, sharedInputStyle } from '../Forms/commonStyles';
import { getFocusStyle, sharedInputStyle } from '../Forms/commonStyles';
import { stylesFactory, useTheme } from '../../themes';
import { Icon } from '../Icon/Icon';
import { useClientRect } from '../../utils/useClientRect';
import { FormInputSize } from '../Forms/types';
export interface Props extends Omit<HTMLProps<HTMLInputElement>, 'prefix' | 'size'> {
/** Sets the width to a multiple of 8px. Should only be used with inline forms. Setting width of the container is preferred in other cases.*/
width?: number;
/** Show an invalid state around the input */
invalid?: boolean;
/** Show an icon as a prefix in the input */
@@ -20,15 +21,15 @@ export interface Props extends Omit<HTMLProps<HTMLInputElement>, 'prefix' | 'siz
addonBefore?: ReactNode;
/** Add a component as an addon after the input */
addonAfter?: ReactNode;
size?: FormInputSize;
}
interface StyleDeps {
theme: GrafanaTheme;
invalid: boolean;
width?: number;
}
export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDeps) => {
export const getInputStyles = stylesFactory(({ theme, invalid = false, width }: StyleDeps) => {
const { palette, colors } = theme;
const borderRadius = theme.border.radius.sm;
const height = theme.spacing.formInputHeight;
@@ -56,7 +57,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe
css`
label: input-wrapper;
display: flex;
width: 100%;
width: ${width ? `${8 * width}px` : '100%'};
height: ${height}px;
border-radius: ${borderRadius};
&:hover {
@@ -213,7 +214,7 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe
});
export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const { className, addonAfter, addonBefore, prefix, suffix, invalid, loading, size = 'auto', ...restProps } = props;
const { className, addonAfter, addonBefore, prefix, suffix, invalid, loading, width = 0, ...restProps } = props;
/**
* Prefix & suffix are positioned absolutely within inputWrapper. We use client rects below to apply correct padding to the input
* when prefix/suffix is larger than default (28px = 16px(icon) + 12px(left/right paddings)).
@@ -223,10 +224,10 @@ export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const [suffixRect, suffixRef] = useClientRect<HTMLDivElement>();
const theme = useTheme();
const styles = getInputStyles({ theme, invalid: !!invalid });
const styles = getInputStyles({ theme, invalid: !!invalid, width });
return (
<div className={cx(styles.wrapper, inputSizes()[size], className)}>
<div className={cx(styles.wrapper, className)}>
{!!addonBefore && <div className={styles.addon}>{addonBefore}</div>}
<div className={styles.inputWrapper}>

View File

@@ -29,7 +29,7 @@ Used for horizontally aligning several elements (e.g. Button, Select) with a pre
<Preview>
<HorizontalGroup>
<Select
size="sm"
width={25}
onChange={() => {}}
options={[
{ value: 1, label: "Option 1" },
@@ -37,7 +37,7 @@ Used for horizontally aligning several elements (e.g. Button, Select) with a pre
]}
/>
<Select
size="sm"
width={25}
onChange={() => {}}
options={[
{ value: 1, label: "Option 1" },
@@ -45,7 +45,7 @@ Used for horizontally aligning several elements (e.g. Button, Select) with a pre
]}
/>
<Select
size="sm"
width={25}
onChange={() => {}}
options={[
{ value: 1, label: "Option 1" },
@@ -69,7 +69,7 @@ Used for vertically aligning several elements (e.g. Button, Select) with a prede
<Preview>
<VerticalGroup justify="center">
<Select
size="sm"
width={25}
onChange={() => {}}
options={[
{ value: 1, label: "Option 1" },
@@ -77,7 +77,7 @@ Used for vertically aligning several elements (e.g. Button, Select) with a prede
]}
/>
<Select
size="sm"
width={25}
onChange={() => {}}
options={[
{ value: 1, label: "Option 1" },
@@ -85,7 +85,7 @@ Used for vertically aligning several elements (e.g. Button, Select) with a prede
]}
/>
<Select
size="sm"
width={25}
onChange={() => {}}
options={[
{ value: 1, label: "Option 1" },

View File

@@ -80,7 +80,6 @@ const basicSelectAsync = () => {
onChange={v => {
setValue(v);
}}
size="md"
/>
);
};
@@ -117,7 +116,6 @@ const multiSelect = () => {
onChange={v => {
setValue(v);
}}
size="md"
/>
</>
);

View File

@@ -3,7 +3,7 @@ import { Select, AsyncSelect, MultiSelect, AsyncMultiSelect } from './Select';
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
import { SelectableValue } from '@grafana/data';
import { getAvailableIcons, IconName } from '../../types';
import { select, boolean } from '@storybook/addon-knobs';
import { select, boolean, number } from '@storybook/addon-knobs';
import { Icon } from '../Icon/Icon';
import { Button } from '../Button';
import { ButtonSelect } from './ButtonSelect';
@@ -50,6 +50,7 @@ const getKnobs = () => {
const VISUAL_GROUP = 'Visual options';
// ---
const prefix = select('Prefix', prefixSuffixOpts, null, VISUAL_GROUP);
const width = number('Width', 0, undefined, VISUAL_GROUP);
let prefixEl: any = prefix;
if (prefix && prefix.match(/icon-/g)) {
@@ -57,6 +58,7 @@ const getKnobs = () => {
}
return {
width,
disabled,
invalid,
loading,
@@ -67,6 +69,7 @@ const getKnobs = () => {
const getDynamicProps = () => {
const knobs = getKnobs();
return {
width: knobs.width,
disabled: knobs.disabled,
isLoading: knobs.loading,
invalid: knobs.invalid,
@@ -85,7 +88,6 @@ export const basic = () => {
onChange={v => {
setValue(v);
}}
size="md"
{...getDynamicProps()}
/>
</>
@@ -105,7 +107,6 @@ export const basicSelectPlainValue = () => {
onChange={v => {
setValue(v.value);
}}
size="md"
{...getDynamicProps()}
/>
</>
@@ -138,7 +139,6 @@ export const SelectWithOptionDescriptions = () => {
onChange={v => {
setValue(v.value);
}}
size="md"
{...getDynamicProps()}
/>
</>
@@ -159,7 +159,6 @@ export const multiPlainValue = () => {
onChange={v => {
setValue(v.map((v: any) => v.value));
}}
size="md"
{...getDynamicProps()}
/>
</>
@@ -177,7 +176,6 @@ export const multiSelect = () => {
onChange={v => {
setValue(v);
}}
size="md"
{...getDynamicProps()}
/>
</>
@@ -195,7 +193,6 @@ export const multiSelectAsync = () => {
onChange={v => {
setValue(v);
}}
size="md"
allowCustomValue
{...getDynamicProps()}
/>
@@ -212,7 +209,6 @@ export const buttonSelect = () => {
onChange={v => {
setValue(v);
}}
size="md"
allowCustomValue
icon={icon}
{...getDynamicProps()}
@@ -231,7 +227,6 @@ export const basicSelectAsync = () => {
onChange={v => {
setValue(v);
}}
size="md"
{...getDynamicProps()}
/>
);
@@ -247,7 +242,6 @@ export const customizedControl = () => {
onChange={v => {
setValue(v);
}}
size="md"
renderControl={React.forwardRef(({ isOpen, value, ...otherProps }, ref) => {
return (
<Button {...otherProps} ref={ref}>
@@ -266,14 +260,13 @@ export const autoMenuPlacement = () => {
return (
<>
<div style={{ height: '95vh', display: 'flex', alignItems: 'flex-end' }}>
<div style={{ width: '100%', height: '95vh', display: 'flex', alignItems: 'flex-end' }}>
<Select
options={generateOptions()}
value={value}
onChange={v => {
setValue(v);
}}
size="md"
{...getDynamicProps()}
/>
</div>
@@ -293,7 +286,6 @@ export const customValueCreation = () => {
onChange={v => {
setValue(v);
}}
size="md"
allowCustomValue
onCreateOption={v => {
const customValue: SelectableValue<string> = { value: kebabCase(v), label: v };

View File

@@ -1,5 +1,4 @@
import React, { useCallback } from 'react';
import { deprecationWarning } from '@grafana/data';
// @ts-ignore
import { default as ReactSelect } from '@torkelo/react-select';
// @ts-ignore
@@ -11,7 +10,6 @@ import { default as AsyncCreatable } from '@torkelo/react-select/async-creatable
import { Icon } from '../Icon/Icon';
import { css, cx } from 'emotion';
import { inputSizesPixels } from '../Forms/commonStyles';
import resetSelectStyles from './resetSelectStyles';
import { SelectMenu, SelectMenuOptions } from './SelectMenu';
import { IndicatorsContainer } from './IndicatorsContainer';
@@ -100,7 +98,6 @@ export function SelectBase<T>({
placeholder = 'Choose',
prefix,
renderControl,
size = 'auto',
tabSelectsValue = true,
className,
value,
@@ -178,13 +175,6 @@ export function SelectBase<T>({
value: isMulti ? selectedValue : selectedValue[0],
};
// width property is deprecated in favor of size or className
let widthClass = '';
if (width) {
deprecationWarning('Select', 'width property', 'size or className');
widthClass = 'width-' + width;
}
if (allowCustomValue) {
ReactSelectComponent = Creatable;
creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`);
@@ -275,10 +265,10 @@ export function SelectBase<T>({
}),
container: () => ({
position: 'relative',
width: inputSizesPixels(size),
width: width ? `${8 * width}px` : '100%',
}),
}}
className={cx('select-container', widthClass, className)}
className={cx('select-container', className)}
{...commonSelectProps}
{...creatableProps}
{...asyncSelectProps}

View File

@@ -1,6 +1,5 @@
import { SelectableValue } from '@grafana/data';
import React from 'react';
import { FormInputSize } from '../Forms/types';
export type SelectValue<T> = T | SelectableValue<T> | T[] | Array<SelectableValue<T>>;
@@ -45,9 +44,9 @@ export interface SelectCommonProps<T> {
prefix?: JSX.Element | string | null;
/** Use a custom element to control Select. A proper ref to the renderControl is needed if 'portal' isn't set to null*/
renderControl?: ControlComponent<T>;
size?: FormInputSize;
tabSelectsValue?: boolean;
value?: SelectValue<T>;
/** Sets the width to a multiple of 8px. Should only be used with inline forms. Setting width of the container is preferred in other cases.*/
width?: number;
}

View File

@@ -25,7 +25,6 @@ TimeZonePickerStories.add('default', () => {
action('on selected')(newValue);
updateValue({ value: newValue });
}}
size="sm"
/>
);
}}

View File

@@ -1,16 +1,15 @@
import React, { FC } from 'react';
import { getTimeZoneGroups } from '@grafana/data';
import { Cascader } from '../index';
import { FormInputSize } from '../Forms/types';
interface Props {
value: string;
size?: FormInputSize;
width?: number;
onChange: (newValue: string) => void;
}
export const TimeZonePicker: FC<Props> = ({ onChange, value, size = 'md' }) => {
export const TimeZonePicker: FC<Props> = ({ onChange, value, width }) => {
const timeZoneGroups = getTimeZoneGroups();
const groupOptions = timeZoneGroups.map(group => {
@@ -41,7 +40,7 @@ export const TimeZonePicker: FC<Props> = ({ onChange, value, size = 'md' }) => {
options={groupOptions}
initialValue={selectedValue?.value}
onSelect={(newValue: string) => onChange(newValue)}
size={size}
width={width}
placeholder="Select timezone"
/>
);

View File

@@ -195,7 +195,7 @@ exports[`TimePicker renders buttons correctly 1`] = `
"listItem": "-1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.3)",
},
"spacing": Object {
"d": "14px",
"d": "16px",
"formButtonHeight": 32,
"formFieldsetMargin": "16px",
"formInputAffixPaddingHorizontal": "4px",
@@ -511,7 +511,7 @@ exports[`TimePicker renders content correctly after beeing open 1`] = `
"listItem": "-1px -1px 0 0 hsla(0, 0%, 100%, 0.1), 1px 1px 0 0 rgba(0, 0, 0, 0.3)",
},
"spacing": Object {
"d": "14px",
"d": "16px",
"formButtonHeight": 32,
"formFieldsetMargin": "16px",
"formInputAffixPaddingHorizontal": "4px",

View File

@@ -25,7 +25,7 @@ export function ValuePicker<T>({
options,
onChange,
variant,
size,
size = 'sm',
isFullWidth = true,
}: ValuePickerProps<T>) {
const [isPicking, setIsPicking] = useState(false);

View File

@@ -75,7 +75,7 @@ const theme: GrafanaThemeCommons = {
},
spacing: {
insetSquishMd: '4px 8px',
d: '14px',
d: '16px',
xxs: '2px',
xs: '4px',
sm: '8px',