Commit f91c7a81 by Tobias Skarhed Committed by GitHub

Forms migration: Move Input folders (#23313)

* Remove exports

* Move folders

* Fix errors
parent 56687a08
......@@ -4,7 +4,7 @@ import RCCascader from 'rc-cascader';
import { Select } from '../Select/Select';
import { FormInputSize } from '../Forms/types';
import { Input } from '../Forms/Input/Input';
import { Input } from '../Input/Input';
import { SelectableValue } from '@grafana/data';
import { css } from 'emotion';
import { onChangeCascader } from './optionMappings';
......
......@@ -3,7 +3,7 @@ import React, { useState } from 'react';
import { storiesOf } from '@storybook/react';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { ClipboardButton } from './ClipboardButton';
import { Input } from '../Input/Input';
import { Input } from '../Forms/Legacy/Input/Input';
import { text } from '@storybook/addon-knobs';
const getKnobs = () => {
......
......@@ -3,7 +3,7 @@ import tinycolor from 'tinycolor2';
import debounce from 'lodash/debounce';
import { ColorPickerProps } from './ColorPickerPopover';
import { Input } from '../Input/Input';
import { Input } from '../Forms/Legacy/Input/Input';
interface ColorInputState {
previousColor: string;
......
......@@ -9,7 +9,7 @@ import { DataSourceSettings } from '@grafana/data';
import { HttpSettingsProps } from './types';
import { CustomHeadersSettings } from './CustomHeadersSettings';
import { Select } from '../Forms/Legacy/Select/Select';
import { Input } from '../Input/Input';
import { Input } from '../Forms/Legacy/Input/Input';
import { FormField } from '../FormField/FormField';
import { FormLabel } from '../FormLabel/FormLabel';
import { Switch } from '../Switch/Switch';
......
import React, { useState, useCallback } from 'react';
import { boolean, number, text } from '@storybook/addon-knobs';
import { Field } from './Field';
import { Input } from './Input/Input';
import { Input } from '../Input/Input';
import { Switch } from './Switch';
import mdx from './Field.mdx';
......
......@@ -4,7 +4,7 @@ import { Legend } from './Legend';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { withStoryContainer } from '../../utils/storybook/withStoryContainer';
import { Field } from './Field';
import { Input } from './Input/Input';
import { Input } from '../Input/Input';
import { Button } from '../Button';
import { Form } from './Form';
import { Switch } from './Switch';
......
import React, { useState } from 'react';
import { boolean, text, select, number } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../../utils/storybook/withCenteredStory';
import { Input } from './Input';
import { Button } from '../../Button';
import mdx from './Input.mdx';
import { getAvailableIcons, IconType } from '../../Icon/types';
import { KeyValue } from '@grafana/data';
import { Icon } from '../../Icon/Icon';
import { Field } from '../Field';
export default {
title: 'Forms/Input',
component: Input,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
export const simple = () => {
const prefixSuffixOpts = {
None: null,
Text: '$',
...getAvailableIcons().reduce<KeyValue<string>>((prev, c) => {
return {
...prev,
[`Icon: ${c}`]: `icon-${c}`,
};
}, {}),
};
const BEHAVIOUR_GROUP = 'Behaviour props';
// ---
const type = select(
'Type',
{
text: 'text',
password: 'password',
number: 'number',
},
'text',
BEHAVIOUR_GROUP
);
const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP);
const invalid = boolean('Invalid', false, BEHAVIOUR_GROUP);
const loading = boolean('Loading', false, BEHAVIOUR_GROUP);
const VISUAL_GROUP = 'Visual options';
// ---
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);
const addonAfter = <Button variant="secondary">Load</Button>;
const addonBefore = <div style={{ display: 'flex', alignItems: 'center', padding: '5px' }}>Input</div>;
const prefix = select('Prefix', prefixSuffixOpts, null, VISUAL_GROUP);
const suffix = select('Suffix', prefixSuffixOpts, null, VISUAL_GROUP);
let prefixEl: any = prefix;
if (prefix && prefix.match(/icon-/g)) {
prefixEl = <Icon name={prefix.replace(/icon-/g, '') as IconType} />;
}
let suffixEl: any = suffix;
if (suffix && suffix.match(/icon-/g)) {
suffixEl = <Icon name={suffix.replace(/icon-/g, '') as IconType} />;
}
const CONTAINER_GROUP = 'Container options';
// ---
const containerWidth = number(
'Container width',
300,
{
range: true,
min: 100,
max: 500,
step: 10,
},
CONTAINER_GROUP
);
return (
<div style={{ width: containerWidth }}>
<Input
disabled={disabled}
invalid={invalid}
prefix={prefixEl}
suffix={suffixEl}
loading={loading}
addonBefore={before && addonBefore}
addonAfter={after && addonAfter}
type={type}
placeholder={placeholder}
/>
</div>
);
};
export const withFieldValidation = () => {
const [value, setValue] = useState('');
return (
<div>
<Field invalid={value === ''} error={value === '' ? 'This input is required' : ''}>
<Input value={value} onChange={e => setValue(e.currentTarget.value)} />
</Field>
</div>
);
};
import React, { HTMLProps, ReactNode } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { css, cx } from 'emotion';
import { getFocusStyle, inputSizes, sharedInputStyle } from '../commonStyles';
import { stylesFactory, useTheme } from '../../../themes';
import { Icon } from '../../Icon/Icon';
import { useClientRect } from '../../../utils/useClientRect';
import { FormInputSize } from '../types';
export interface Props extends Omit<HTMLProps<HTMLInputElement>, 'prefix' | 'size'> {
/** Show an invalid state around the input */
invalid?: boolean;
/** Show an icon as a prefix in the input */
prefix?: JSX.Element | string | null;
/** Show an icon as a suffix in the input */
suffix?: JSX.Element | string | null;
/** Show a loading indicator as a suffix in the input */
loading?: boolean;
/** Add a component as an addon before the input */
addonBefore?: ReactNode;
/** Add a component as an addon after the input */
addonAfter?: ReactNode;
size?: FormInputSize;
}
interface StyleDeps {
theme: GrafanaTheme;
invalid: boolean;
}
export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDeps) => {
const colors = theme.colors;
const borderRadius = theme.border.radius.sm;
const height = theme.spacing.formInputHeight;
const prefixSuffixStaticWidth = '28px';
const prefixSuffix = css`
position: absolute;
top: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
flex-grow: 0;
flex-shrink: 0;
font-size: ${theme.typography.size.md};
height: 100%;
/* Min width specified for prefix/suffix classes used outside React component*/
min-width: ${prefixSuffixStaticWidth};
`;
return {
// Wraps inputWrapper and addons
wrapper: cx(
css`
label: input-wrapper;
display: flex;
width: 100%;
height: ${height};
border-radius: ${borderRadius};
&:hover {
> .prefix,
.suffix,
.input {
border-color: ${invalid ? colors.redBase : colors.formInputBorder};
}
// only show number buttons on hover
input[type='number'] {
-moz-appearance: number-input;
-webkit-appearance: number-input;
appearance: textfield;
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: inner-spin-button !important;
opacity: 1;
}
}
`
),
// Wraps input and prefix/suffix
inputWrapper: css`
label: input-inputWrapper;
position: relative;
flex-grow: 1;
/* we want input to be above addons, especially for focused state */
z-index: 1;
/* when input rendered with addon before only*/
&:not(:first-child):last-child {
> input {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
/* when input rendered with addon after only*/
&:first-child:not(:last-child) {
> input {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
/* when rendered with addon before and after */
&:not(:first-child):not(:last-child) {
> input {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
input {
/* paddings specified for classes used outside React component */
&:not(:first-child) {
padding-left: ${prefixSuffixStaticWidth};
}
&:not(:last-child) {
padding-right: ${prefixSuffixStaticWidth};
}
&[readonly] {
cursor: default;
}
}
`,
input: cx(
getFocusStyle(theme),
sharedInputStyle(theme, invalid),
css`
label: input-input;
position: relative;
z-index: 0;
flex-grow: 1;
border-radius: ${borderRadius};
height: 100%;
width: 100%;
`
),
inputDisabled: css`
background-color: ${colors.formInputBgDisabled};
color: ${colors.formInputDisabledText};
`,
addon: css`
label: input-addon;
display: flex;
justify-content: center;
align-items: center;
flex-grow: 0;
flex-shrink: 0;
position: relative;
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
> :last-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
> :first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
> *:focus {
/* we want anything that has focus and is an addon to be above input */
z-index: 2;
}
`,
prefix: cx(
prefixSuffix,
css`
label: input-prefix;
padding-left: ${theme.spacing.sm};
padding-right: ${theme.spacing.xs};
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
`
),
suffix: cx(
prefixSuffix,
css`
label: input-suffix;
padding-right: ${theme.spacing.sm};
padding-left: ${theme.spacing.xs};
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
right: 0;
`
),
loadingIndicator: css`
& + * {
margin-left: ${theme.spacing.xs};
}
`,
};
});
export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const { className, addonAfter, addonBefore, prefix, suffix, invalid, loading, size = 'auto', ...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)).
* Thanks to that prefix/suffix do not overflow the input element itself.
*/
const [prefixRect, prefixRef] = useClientRect<HTMLDivElement>();
const [suffixRect, suffixRef] = useClientRect<HTMLDivElement>();
const theme = useTheme();
const styles = getInputStyles({ theme, invalid: !!invalid });
return (
<div className={cx(styles.wrapper, inputSizes()[size], className)}>
{!!addonBefore && <div className={styles.addon}>{addonBefore}</div>}
<div className={styles.inputWrapper}>
{prefix && (
<div className={styles.prefix} ref={prefixRef}>
{prefix}
</div>
)}
<input
ref={ref}
className={styles.input}
{...restProps}
style={{
paddingLeft: prefixRect ? prefixRect.width : undefined,
paddingRight: suffixRect ? suffixRect.width : undefined,
}}
/>
{(suffix || loading) && (
<div className={styles.suffix} ref={suffixRef}>
{loading && <Icon name="spinner" className={cx('fa-spin', styles.loadingIndicator)} />}
{suffix}
</div>
)}
</div>
{!!addonAfter && <div className={styles.addon}>{addonAfter}</div>}
</div>
);
});
Input.displayName = 'Input';
import React, { useState } from 'react';
import { zip, fromPairs } from 'lodash';
import { storiesOf } from '@storybook/react';
import { withCenteredStory } from '../../../../utils/storybook/withCenteredStory';
import { Input } from './Input';
import { text, select } from '@storybook/addon-knobs';
import { EventsWithValidation } from '../../../../utils';
const getKnobs = () => {
return {
validation: text('Validation regex (will do a partial match if you do not anchor it)', ''),
validationErrorMessage: text('Validation error message', 'Input not valid'),
validationEvent: select(
'Validation event',
fromPairs(zip(Object.keys(EventsWithValidation), Object.values(EventsWithValidation))),
EventsWithValidation.onBlur
),
};
};
const Wrapper = () => {
const { validation, validationErrorMessage, validationEvent } = getKnobs();
const [value, setValue] = useState('');
const validations = {
[validationEvent]: [
{
rule: (value: string) => {
return !!value.match(validation);
},
errorMessage: validationErrorMessage,
},
],
};
return <Input value={value} onChange={e => setValue(e.currentTarget.value)} validationEvents={validations} />;
};
const story = storiesOf('General/Input', module);
story.addDecorator(withCenteredStory);
story.add('input', () => <Wrapper />);
......@@ -2,8 +2,8 @@ import React from 'react';
import renderer from 'react-test-renderer';
import { shallow } from 'enzyme';
import { Input } from './Input';
import { EventsWithValidation } from '../../utils';
import { ValidationEvents } from '../../types';
import { EventsWithValidation } from '../../../../utils';
import { ValidationEvents } from '../../../../types';
const TEST_ERROR_MESSAGE = 'Value must be empty or less than 3 chars';
const testBlurValidation: ValidationEvents = {
......
import React, { PureComponent, ChangeEvent } from 'react';
import classNames from 'classnames';
import { validate, EventsWithValidation, hasValidationEvent } from '../../../../utils';
import { ValidationEvents, ValidationRule } from '../../../../types';
export enum LegacyInputStatus {
Invalid = 'invalid',
Valid = 'valid',
}
interface Props extends React.HTMLProps<HTMLInputElement> {
validationEvents?: ValidationEvents;
hideErrorMessage?: boolean;
inputRef?: React.LegacyRef<HTMLInputElement>;
// Override event props and append status as argument
onBlur?: (event: React.FocusEvent<HTMLInputElement>, status?: LegacyInputStatus) => void;
onFocus?: (event: React.FocusEvent<HTMLInputElement>, status?: LegacyInputStatus) => void;
onChange?: (event: React.ChangeEvent<HTMLInputElement>, status?: LegacyInputStatus) => void;
}
interface State {
error: string | null;
}
export class Input extends PureComponent<Props, State> {
static defaultProps = {
className: '',
};
state: State = {
error: null,
};
get status() {
return this.state.error ? LegacyInputStatus.Invalid : LegacyInputStatus.Valid;
}
get isInvalid() {
return this.status === LegacyInputStatus.Invalid;
}
validatorAsync = (validationRules: ValidationRule[]) => {
return (evt: ChangeEvent<HTMLInputElement>) => {
const errors = validate(evt.target.value, validationRules);
this.setState(prevState => {
return { ...prevState, error: errors ? errors[0] : null };
});
};
};
populateEventPropsWithStatus = (restProps: any, validationEvents: ValidationEvents | undefined) => {
const inputElementProps = { ...restProps };
if (!validationEvents) {
return inputElementProps;
}
Object.keys(EventsWithValidation).forEach(eventName => {
if (hasValidationEvent(eventName as EventsWithValidation, validationEvents) || restProps[eventName]) {
inputElementProps[eventName] = async (evt: ChangeEvent<HTMLInputElement>) => {
evt.persist(); // Needed for async. https://reactjs.org/docs/events.html#event-pooling
if (hasValidationEvent(eventName as EventsWithValidation, validationEvents)) {
await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]);
}
if (restProps[eventName]) {
restProps[eventName].apply(null, [evt, this.status]);
}
};
}
});
return inputElementProps;
};
render() {
const { validationEvents, className, hideErrorMessage, inputRef, ...restProps } = this.props;
const { error } = this.state;
const inputClassName = classNames('gf-form-input', { invalid: this.isInvalid }, className);
const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents);
return (
<div style={{ flexGrow: 1 }}>
<input {...inputElementProps} ref={inputRef} className={inputClassName} />
{error && !hideErrorMessage && <span>{error}</span>}
</div>
);
}
}
......@@ -5,7 +5,7 @@ import { getLegendStyles } from './Legend';
import { getFieldValidationMessageStyles } from './FieldValidationMessage';
import { getButtonStyles, ButtonVariant } from '../Button';
import { ComponentSize } from '../../types/size';
import { getInputStyles } from './Input/Input';
import { getInputStyles } from '../Input/Input';
import { getSwitchStyles } from './Switch';
import { getCheckboxStyles } from './Checkbox';
......
import { Controller as InputControl } from 'react-hook-form';
import { getFormStyles } from './getFormStyles';
import { Label } from './Label';
// To be removed
import { Input } from './Input/Input';
import { RadioButtonGroup } from './RadioButtonGroup/RadioButtonGroup';
import { Form } from './Form';
import { Field } from './Field';
......@@ -16,8 +14,6 @@ const Forms = {
Switch,
getFormStyles,
Label,
// To be removed
Input,
Form,
Field,
InputControl,
......
import React, { ChangeEvent, useState } from 'react';
import { css } from 'emotion';
import { Forms } from '../index';
import { Input } from '../Input/Input';
import { Field } from '../Forms/Field';
import { Icon } from './Icon';
import { getAvailableIcons, IconType } from './types';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
......@@ -79,13 +80,13 @@ export const simple = () => {
width: 100%;
`}
>
<Forms.Field
<Field
className={css`
width: 300px;
`}
>
<Forms.Input onChange={searchIcon} placeholder="Search icons by name" />
</Forms.Field>
<Input onChange={searchIcon} placeholder="Search icons by name" />
</Field>
<div
className={css`
display: flex;
......
import React, { useState } from 'react';
import { zip, fromPairs } from 'lodash';
import { storiesOf } from '@storybook/react';
import { boolean, text, select, number } from '@storybook/addon-knobs';
import { withCenteredStory } from '../../utils/storybook/withCenteredStory';
import { Input } from './Input';
import { text, select } from '@storybook/addon-knobs';
import { EventsWithValidation } from '../../utils';
const getKnobs = () => {
return {
validation: text('Validation regex (will do a partial match if you do not anchor it)', ''),
validationErrorMessage: text('Validation error message', 'Input not valid'),
validationEvent: select(
'Validation event',
fromPairs(zip(Object.keys(EventsWithValidation), Object.values(EventsWithValidation))),
EventsWithValidation.onBlur
),
};
import { Button } from '../Button';
import mdx from './Input.mdx';
import { getAvailableIcons, IconType } from '../Icon/types';
import { KeyValue } from '@grafana/data';
import { Icon } from '../Icon/Icon';
import { Field } from '../Forms/Field';
export default {
title: 'Forms/Input',
component: Input,
decorators: [withCenteredStory],
parameters: {
docs: {
page: mdx,
},
},
};
const Wrapper = () => {
const { validation, validationErrorMessage, validationEvent } = getKnobs();
const [value, setValue] = useState('');
const validations = {
[validationEvent]: [
{
rule: (value: string) => {
return !!value.match(validation);
},
errorMessage: validationErrorMessage,
},
],
export const simple = () => {
const prefixSuffixOpts = {
None: null,
Text: '$',
...getAvailableIcons().reduce<KeyValue<string>>((prev, c) => {
return {
...prev,
[`Icon: ${c}`]: `icon-${c}`,
};
}, {}),
};
return <Input value={value} onChange={e => setValue(e.currentTarget.value)} validationEvents={validations} />;
const BEHAVIOUR_GROUP = 'Behaviour props';
// ---
const type = select(
'Type',
{
text: 'text',
password: 'password',
number: 'number',
},
'text',
BEHAVIOUR_GROUP
);
const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP);
const invalid = boolean('Invalid', false, BEHAVIOUR_GROUP);
const loading = boolean('Loading', false, BEHAVIOUR_GROUP);
const VISUAL_GROUP = 'Visual options';
// ---
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);
const addonAfter = <Button variant="secondary">Load</Button>;
const addonBefore = <div style={{ display: 'flex', alignItems: 'center', padding: '5px' }}>Input</div>;
const prefix = select('Prefix', prefixSuffixOpts, null, VISUAL_GROUP);
const suffix = select('Suffix', prefixSuffixOpts, null, VISUAL_GROUP);
let prefixEl: any = prefix;
if (prefix && prefix.match(/icon-/g)) {
prefixEl = <Icon name={prefix.replace(/icon-/g, '') as IconType} />;
}
let suffixEl: any = suffix;
if (suffix && suffix.match(/icon-/g)) {
suffixEl = <Icon name={suffix.replace(/icon-/g, '') as IconType} />;
}
const CONTAINER_GROUP = 'Container options';
// ---
const containerWidth = number(
'Container width',
300,
{
range: true,
min: 100,
max: 500,
step: 10,
},
CONTAINER_GROUP
);
return (
<div style={{ width: containerWidth }}>
<Input
disabled={disabled}
invalid={invalid}
prefix={prefixEl}
suffix={suffixEl}
loading={loading}
addonBefore={before && addonBefore}
addonAfter={after && addonAfter}
type={type}
placeholder={placeholder}
/>
</div>
);
};
const story = storiesOf('General/Input', module);
story.addDecorator(withCenteredStory);
story.add('input', () => <Wrapper />);
export const withFieldValidation = () => {
const [value, setValue] = useState('');
return (
<div>
<Field invalid={value === ''} error={value === '' ? 'This input is required' : ''}>
<Input value={value} onChange={e => setValue(e.currentTarget.value)} />
</Field>
</div>
);
};
import React, { PureComponent, ChangeEvent } from 'react';
import classNames from 'classnames';
import { validate, EventsWithValidation, hasValidationEvent } from '../../utils';
import { ValidationEvents, ValidationRule } from '../../types';
export enum LegacyInputStatus {
Invalid = 'invalid',
Valid = 'valid',
}
interface Props extends React.HTMLProps<HTMLInputElement> {
validationEvents?: ValidationEvents;
hideErrorMessage?: boolean;
inputRef?: React.LegacyRef<HTMLInputElement>;
import React, { HTMLProps, ReactNode } from 'react';
import { GrafanaTheme } from '@grafana/data';
import { css, cx } from 'emotion';
import { getFocusStyle, inputSizes, sharedInputStyle } from '../Forms/commonStyles';
import { stylesFactory, useTheme } from '../../themes';
import { Icon } from '../Icon/Icon';
import { useClientRect } from '../../utils/useClientRect';
import { FormInputSize } from '../Forms/types';
// Override event props and append status as argument
onBlur?: (event: React.FocusEvent<HTMLInputElement>, status?: LegacyInputStatus) => void;
onFocus?: (event: React.FocusEvent<HTMLInputElement>, status?: LegacyInputStatus) => void;
onChange?: (event: React.ChangeEvent<HTMLInputElement>, status?: LegacyInputStatus) => void;
export interface Props extends Omit<HTMLProps<HTMLInputElement>, 'prefix' | 'size'> {
/** Show an invalid state around the input */
invalid?: boolean;
/** Show an icon as a prefix in the input */
prefix?: JSX.Element | string | null;
/** Show an icon as a suffix in the input */
suffix?: JSX.Element | string | null;
/** Show a loading indicator as a suffix in the input */
loading?: boolean;
/** Add a component as an addon before the input */
addonBefore?: ReactNode;
/** Add a component as an addon after the input */
addonAfter?: ReactNode;
size?: FormInputSize;
}
interface State {
error: string | null;
interface StyleDeps {
theme: GrafanaTheme;
invalid: boolean;
}
export class Input extends PureComponent<Props, State> {
static defaultProps = {
className: '',
};
export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDeps) => {
const colors = theme.colors;
const borderRadius = theme.border.radius.sm;
const height = theme.spacing.formInputHeight;
state: State = {
error: null,
};
const prefixSuffixStaticWidth = '28px';
const prefixSuffix = css`
position: absolute;
top: 0;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
flex-grow: 0;
flex-shrink: 0;
font-size: ${theme.typography.size.md};
height: 100%;
/* Min width specified for prefix/suffix classes used outside React component*/
min-width: ${prefixSuffixStaticWidth};
`;
get status() {
return this.state.error ? LegacyInputStatus.Invalid : LegacyInputStatus.Valid;
}
get isInvalid() {
return this.status === LegacyInputStatus.Invalid;
}
validatorAsync = (validationRules: ValidationRule[]) => {
return (evt: ChangeEvent<HTMLInputElement>) => {
const errors = validate(evt.target.value, validationRules);
this.setState(prevState => {
return { ...prevState, error: errors ? errors[0] : null };
});
};
};
return {
// Wraps inputWrapper and addons
wrapper: cx(
css`
label: input-wrapper;
display: flex;
width: 100%;
height: ${height};
border-radius: ${borderRadius};
&:hover {
> .prefix,
.suffix,
.input {
border-color: ${invalid ? colors.redBase : colors.formInputBorder};
}
populateEventPropsWithStatus = (restProps: any, validationEvents: ValidationEvents | undefined) => {
const inputElementProps = { ...restProps };
if (!validationEvents) {
return inputElementProps;
}
Object.keys(EventsWithValidation).forEach(eventName => {
if (hasValidationEvent(eventName as EventsWithValidation, validationEvents) || restProps[eventName]) {
inputElementProps[eventName] = async (evt: ChangeEvent<HTMLInputElement>) => {
evt.persist(); // Needed for async. https://reactjs.org/docs/events.html#event-pooling
if (hasValidationEvent(eventName as EventsWithValidation, validationEvents)) {
await this.validatorAsync(validationEvents[eventName]).apply(this, [evt]);
// only show number buttons on hover
input[type='number'] {
-moz-appearance: number-input;
-webkit-appearance: number-input;
appearance: textfield;
}
if (restProps[eventName]) {
restProps[eventName].apply(null, [evt, this.status]);
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: inner-spin-button !important;
opacity: 1;
}
};
}
`
),
// Wraps input and prefix/suffix
inputWrapper: css`
label: input-inputWrapper;
position: relative;
flex-grow: 1;
/* we want input to be above addons, especially for focused state */
z-index: 1;
/* when input rendered with addon before only*/
&:not(:first-child):last-child {
> input {
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
/* when input rendered with addon after only*/
&:first-child:not(:last-child) {
> input {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
/* when rendered with addon before and after */
&:not(:first-child):not(:last-child) {
> input {
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
input {
/* paddings specified for classes used outside React component */
&:not(:first-child) {
padding-left: ${prefixSuffixStaticWidth};
}
&:not(:last-child) {
padding-right: ${prefixSuffixStaticWidth};
}
&[readonly] {
cursor: default;
}
}
});
return inputElementProps;
`,
input: cx(
getFocusStyle(theme),
sharedInputStyle(theme, invalid),
css`
label: input-input;
position: relative;
z-index: 0;
flex-grow: 1;
border-radius: ${borderRadius};
height: 100%;
width: 100%;
`
),
inputDisabled: css`
background-color: ${colors.formInputBgDisabled};
color: ${colors.formInputDisabledText};
`,
addon: css`
label: input-addon;
display: flex;
justify-content: center;
align-items: center;
flex-grow: 0;
flex-shrink: 0;
position: relative;
&:first-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
> :last-child {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
}
&:last-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
> :first-child {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
}
> *:focus {
/* we want anything that has focus and is an addon to be above input */
z-index: 2;
}
`,
prefix: cx(
prefixSuffix,
css`
label: input-prefix;
padding-left: ${theme.spacing.sm};
padding-right: ${theme.spacing.xs};
border-right: none;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
`
),
suffix: cx(
prefixSuffix,
css`
label: input-suffix;
padding-right: ${theme.spacing.sm};
padding-left: ${theme.spacing.xs};
border-left: none;
border-top-left-radius: 0;
border-bottom-left-radius: 0;
right: 0;
`
),
loadingIndicator: css`
& + * {
margin-left: ${theme.spacing.xs};
}
`,
};
});
render() {
const { validationEvents, className, hideErrorMessage, inputRef, ...restProps } = this.props;
const { error } = this.state;
const inputClassName = classNames('gf-form-input', { invalid: this.isInvalid }, className);
const inputElementProps = this.populateEventPropsWithStatus(restProps, validationEvents);
export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
const { className, addonAfter, addonBefore, prefix, suffix, invalid, loading, size = 'auto', ...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)).
* Thanks to that prefix/suffix do not overflow the input element itself.
*/
const [prefixRect, prefixRef] = useClientRect<HTMLDivElement>();
const [suffixRect, suffixRef] = useClientRect<HTMLDivElement>();
return (
<div style={{ flexGrow: 1 }}>
<input {...inputElementProps} ref={inputRef} className={inputClassName} />
{error && !hideErrorMessage && <span>{error}</span>}
const theme = useTheme();
const styles = getInputStyles({ theme, invalid: !!invalid });
return (
<div className={cx(styles.wrapper, inputSizes()[size], className)}>
{!!addonBefore && <div className={styles.addon}>{addonBefore}</div>}
<div className={styles.inputWrapper}>
{prefix && (
<div className={styles.prefix} ref={prefixRef}>
{prefix}
</div>
)}
<input
ref={ref}
className={styles.input}
{...restProps}
style={{
paddingLeft: prefixRect ? prefixRect.width : undefined,
paddingRight: suffixRect ? suffixRect.width : undefined,
}}
/>
{(suffix || loading) && (
<div className={styles.suffix} ref={suffixRef}>
{loading && <Icon name="spinner" className={cx('fa-spin', styles.loadingIndicator)} />}
{suffix}
</div>
)}
</div>
);
}
}
{!!addonAfter && <div className={styles.addon}>{addonAfter}</div>}
</div>
);
});
Input.displayName = 'Input';
......@@ -5,7 +5,7 @@ import {
toFloatOrUndefined,
NumberFieldConfigSettings,
} from '@grafana/data';
import { Input } from '../Forms/Input/Input';
import { Input } from '../Input/Input';
export const NumberValueEditor: React.FC<FieldConfigEditorProps<number, NumberFieldConfigSettings>> = ({
value,
......
import React from 'react';
import { FieldConfigEditorProps, StringFieldConfigSettings } from '@grafana/data';
import { Input } from '../Forms/Input/Input';
import { Input } from '../Input/Input';
export const StringValueEditor: React.FC<FieldConfigEditorProps<string, StringFieldConfigSettings>> = ({
value,
......
import React from 'react';
import { useTheme } from '../../themes/ThemeContext';
import { getInputStyles } from '../Forms/Input/Input';
import { getInputStyles } from '../Input/Input';
import { cx, css } from 'emotion';
export const IndicatorsContainer = React.forwardRef<HTMLDivElement, React.PropsWithChildren<any>>((props, ref) => {
......
import React from 'react';
import { useTheme } from '../../themes/ThemeContext';
import { getFocusCss, sharedInputStyle } from '../Forms/commonStyles';
import { getInputStyles } from '../Forms/Input/Input';
import { getInputStyles } from '../Input/Input';
import { cx, css } from 'emotion';
import { stylesFactory } from '../../themes';
import { GrafanaTheme } from '@grafana/data';
......
......@@ -2,7 +2,7 @@ import React, { ChangeEvent, KeyboardEvent, PureComponent } from 'react';
import { css, cx } from 'emotion';
import { stylesFactory } from '../../themes/stylesFactory';
import { Button } from '../Button';
import { Input } from '../Input/Input';
import { Input } from '../Forms/Legacy/Input/Input';
import { TagItem } from './TagItem';
interface Props {
......
......@@ -3,7 +3,7 @@ import { Threshold, sortThresholds, ThresholdsConfig, ThresholdsMode, Selectable
import { colors } from '../../utils';
import { getColorFromHexRgbOrName } from '@grafana/data';
import { ThemeContext } from '../../themes/ThemeContext';
import { Input } from '../Input/Input';
import { Input } from '../Forms/Legacy/Input/Input';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { css } from 'emotion';
import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup';
......
......@@ -10,7 +10,7 @@ import {
} from '@grafana/data';
import { colors } from '../../utils';
import { ThemeContext } from '../../themes/ThemeContext';
import { Input } from '../Forms/Input/Input';
import { Input } from '../Input/Input';
import { ColorPicker } from '../ColorPicker/ColorPicker';
import { stylesFactory } from '../../themes';
import { Icon } from '../Icon/Icon';
......
......@@ -4,7 +4,7 @@ import { stringToDateTimeType, isValidTimeString } from '../time';
import { mapStringsToTimeRange } from './mapper';
import { TimePickerCalendar } from './TimePickerCalendar';
import Forms from '../../Forms';
import { Input } from '../../Forms/Input/Input';
import { Input } from '../../Input/Input';
import { Button } from '../../Button';
interface Props {
......
......@@ -2,7 +2,7 @@ import React, { ChangeEvent, PureComponent } from 'react';
import { FormField } from '../FormField/FormField';
import { FormLabel } from '../FormLabel/FormLabel';
import { Input } from '../Input/Input';
import { Input } from '../Forms/Legacy/Input/Input';
import { Select } from '../Forms/Legacy/Select/Select';
import { MappingType, ValueMapping } from '@grafana/data';
......
......@@ -2,7 +2,7 @@ import React, { ChangeEvent } from 'react';
import { HorizontalGroup } from '../Layout/Layout';
import { Select } from '../index';
import Forms from '../Forms';
import { Input } from '../Forms/Input/Input';
import { Input } from '../Input/Input';
import { MappingType, RangeMap, ValueMap, ValueMapping } from '@grafana/data';
import * as styleMixins from '../../themes/mixins';
import { useTheme } from '../../themes';
......
......@@ -139,7 +139,7 @@ export { ButtonSelect } from './Select/ButtonSelect';
export { HorizontalGroup, VerticalGroup, Container } from './Layout/Layout';
export { RadioButtonGroup } from './Forms/RadioButtonGroup/RadioButtonGroup';
export { Input } from './Forms/Input/Input';
export { Input } from './Input/Input';
// Legacy forms
......@@ -150,7 +150,7 @@ import { NoOptionsMessage } from './Forms/Legacy/Select/NoOptionsMessage';
import { ButtonSelect } from './Forms/Legacy/Select/ButtonSelect';
//Input
import { Input, LegacyInputStatus } from './Input/Input';
import { Input, LegacyInputStatus } from './Forms/Legacy/Input/Input';
// Export these until Enterprise migrations have been merged
// export { Input, InputStatus}
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment