mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Forms: revamped select (#21092)
This commit is contained in:
parent
eac66813ca
commit
630f75d3ef
@ -29,6 +29,7 @@
|
|||||||
"@grafana/slate-react": "0.22.9-grafana",
|
"@grafana/slate-react": "0.22.9-grafana",
|
||||||
"@torkelo/react-select": "2.1.1",
|
"@torkelo/react-select": "2.1.1",
|
||||||
"@types/react-color": "2.17.0",
|
"@types/react-color": "2.17.0",
|
||||||
|
"@types/react-select": "2.0.15",
|
||||||
"@types/slate": "0.47.1",
|
"@types/slate": "0.47.1",
|
||||||
"@types/slate-react": "0.22.5",
|
"@types/slate-react": "0.22.5",
|
||||||
"bizcharts": "^3.5.5",
|
"bizcharts": "^3.5.5",
|
||||||
|
@ -4,13 +4,14 @@ import { Button, LinkButton } from './Button';
|
|||||||
import withPropsCombinations from 'react-storybook-addon-props-combinations';
|
import withPropsCombinations from 'react-storybook-addon-props-combinations';
|
||||||
import { action } from '@storybook/addon-actions';
|
import { action } from '@storybook/addon-actions';
|
||||||
import { ThemeableCombinationsRowRenderer } from '../../utils/storybook/CombinationsRowRenderer';
|
import { ThemeableCombinationsRowRenderer } from '../../utils/storybook/CombinationsRowRenderer';
|
||||||
import { select, boolean } from '@storybook/addon-knobs';
|
import { boolean } from '@storybook/addon-knobs';
|
||||||
|
import { getIconKnob } from '../../utils/storybook/knobs';
|
||||||
|
|
||||||
const ButtonStories = storiesOf('UI/Button', module);
|
const ButtonStories = storiesOf('UI/Button', module);
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
onClick: [action('Button clicked')],
|
onClick: [action('Button clicked')],
|
||||||
children: ['Click, click!'],
|
children: ['Click click!'],
|
||||||
};
|
};
|
||||||
|
|
||||||
const variants = {
|
const variants = {
|
||||||
@ -35,15 +36,10 @@ ButtonStories.add('as button element', () => renderButtonStory(Button));
|
|||||||
ButtonStories.add('as link element', () => renderButtonStory(LinkButton));
|
ButtonStories.add('as link element', () => renderButtonStory(LinkButton));
|
||||||
|
|
||||||
ButtonStories.add('with icon', () => {
|
ButtonStories.add('with icon', () => {
|
||||||
const iconKnob = select(
|
const icon = getIconKnob();
|
||||||
'Icon',
|
return withPropsCombinations(
|
||||||
{
|
Button,
|
||||||
Plus: 'fa fa-plus',
|
{ ...variants, ...defaultProps, icon: [icon && `fa fa-${icon}`] },
|
||||||
User: 'fa fa-user',
|
combinationOptions
|
||||||
Gear: 'fa fa-gear',
|
)();
|
||||||
Annotation: 'gicon gicon-annotation',
|
|
||||||
},
|
|
||||||
'fa fa-plus'
|
|
||||||
);
|
|
||||||
return withPropsCombinations(Button, { ...variants, ...defaultProps, icon: [iconKnob] }, combinationOptions)();
|
|
||||||
});
|
});
|
||||||
|
@ -18,12 +18,13 @@ type CommonProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
type ButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
export const Button: React.FunctionComponent<ButtonProps> = props => {
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
const { size, variant, icon, children, className, styles: stylesProp, ...buttonProps } = props;
|
const { size, variant, icon, children, className, styles: stylesProp, ...buttonProps } = props;
|
||||||
|
|
||||||
// Default this to 'button', otherwise html defaults to 'submit' which then submits any form it is in.
|
// Default this to 'button', otherwise html defaults to 'submit' which then submits any form it is in.
|
||||||
buttonProps.type = buttonProps.type || 'button';
|
buttonProps.type = buttonProps.type || 'button';
|
||||||
|
|
||||||
const styles: ButtonStyles =
|
const styles: ButtonStyles =
|
||||||
stylesProp ||
|
stylesProp ||
|
||||||
getButtonStyles({
|
getButtonStyles({
|
||||||
@ -32,14 +33,14 @@ export const Button: React.FunctionComponent<ButtonProps> = props => {
|
|||||||
variant: variant || 'primary',
|
variant: variant || 'primary',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buttonClassName = cx(styles.button, icon && styles.buttonWithIcon, icon && !children && styles.iconButton);
|
||||||
return (
|
return (
|
||||||
<button className={cx(styles.button, className)} {...buttonProps}>
|
<button className={buttonClassName} {...buttonProps} ref={ref}>
|
||||||
<ButtonContent iconClassName={styles.icon} className={styles.iconWrap} icon={icon}>
|
<ButtonContent icon={icon}>{children}</ButtonContent>
|
||||||
{children}
|
|
||||||
</ButtonContent>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
Button.displayName = 'Button';
|
Button.displayName = 'Button';
|
||||||
|
|
||||||
type LinkButtonProps = CommonProps &
|
type LinkButtonProps = CommonProps &
|
||||||
@ -48,7 +49,8 @@ type LinkButtonProps = CommonProps &
|
|||||||
// disabled.
|
// disabled.
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
export const LinkButton: React.FunctionComponent<LinkButtonProps> = props => {
|
|
||||||
|
export const LinkButton = React.forwardRef<HTMLAnchorElement, LinkButtonProps>((props, ref) => {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
const { size, variant, icon, children, className, styles: stylesProp, ...anchorProps } = props;
|
const { size, variant, icon, children, className, styles: stylesProp, ...anchorProps } = props;
|
||||||
const styles: ButtonStyles =
|
const styles: ButtonStyles =
|
||||||
@ -59,12 +61,11 @@ export const LinkButton: React.FunctionComponent<LinkButtonProps> = props => {
|
|||||||
variant: variant || 'primary',
|
variant: variant || 'primary',
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const buttonClassName = cx(styles.button, icon && styles.buttonWithIcon, icon && !children && styles.iconButton);
|
||||||
return (
|
return (
|
||||||
<a className={cx(styles.button, className)} {...anchorProps}>
|
<a className={buttonClassName} {...anchorProps} ref={ref}>
|
||||||
<ButtonContent iconClassName={styles.icon} className={styles.iconWrap} icon={icon}>
|
<ButtonContent icon={icon}>{children}</ButtonContent>
|
||||||
{children}
|
|
||||||
</ButtonContent>
|
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
LinkButton.displayName = 'LinkButton';
|
LinkButton.displayName = 'LinkButton';
|
||||||
|
@ -1,39 +1,44 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import cx from 'classnames';
|
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { stylesFactory } from '../../themes';
|
import { stylesFactory, useTheme } from '../../themes';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
const getStyles = stylesFactory(() => ({
|
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||||
content: css`
|
content: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
height: 100%;
|
||||||
|
`,
|
||||||
|
|
||||||
|
icon: css`
|
||||||
|
& + * {
|
||||||
|
margin-left: ${theme.spacing.sm};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
icon?: string;
|
icon?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
iconClassName?: string;
|
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
};
|
};
|
||||||
export function ButtonContent(props: Props) {
|
export function ButtonContent(props: Props) {
|
||||||
const { icon, className, iconClassName, children } = props;
|
const { icon, children } = props;
|
||||||
const styles = getStyles();
|
const theme = useTheme();
|
||||||
if (icon && children) {
|
const styles = getStyles(theme);
|
||||||
return (
|
|
||||||
<span className={cx(styles.content, className)}>
|
|
||||||
<i className={cx([icon, iconClassName])} />
|
|
||||||
|
|
||||||
<span>{children}</span>
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (icon) {
|
|
||||||
return (
|
|
||||||
<span className={cx(styles.content, className)}>
|
|
||||||
<i className={cx([icon, iconClassName])} />
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return <span className={styles.content}>{children}</span>;
|
const iconElement = icon && (
|
||||||
|
<span className={styles.icon}>
|
||||||
|
<i className={icon} />
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span className={styles.content}>
|
||||||
|
{iconElement}
|
||||||
|
<span>{children}</span>
|
||||||
|
</span>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`Button renders correct html 1`] = `"<button class=\\"css-1ffx2x7-button\\" type=\\"button\\"><span class=\\"css-16tmax3 css-1dxly9g-button-icon-wrap\\"><i class=\\"fa fa-plus\\"></i> <span>Click me</span></span></button>"`;
|
exports[`Button renders correct html 1`] = `"<button class=\\"css-1ffx2x7-button css-5383lb\\" type=\\"button\\"><span class=\\"css-1beih13\\"><span class=\\"css-xhe2zh\\"><i class=\\"fa fa-plus\\"></i></span><span>Click me</span></span></button>"`;
|
||||||
|
|
||||||
exports[`LinkButton renders correct html 1`] = `"<a class=\\"css-1ffx2x7-button\\"><span class=\\"css-16tmax3 css-1dxly9g-button-icon-wrap\\"><i class=\\"fa fa-plus\\"></i> <span>Click me</span></span></a>"`;
|
exports[`LinkButton renders correct html 1`] = `"<a class=\\"css-1ffx2x7-button css-5383lb\\"><span class=\\"css-1beih13\\"><span class=\\"css-xhe2zh\\"><i class=\\"fa fa-plus\\"></i></span><span>Click me</span></span></a>"`;
|
||||||
|
@ -120,10 +120,18 @@ export const getButtonStyles = stylesFactory(({ theme, size, variant }: StyleDep
|
|||||||
box-shadow: none;
|
box-shadow: none;
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
buttonWithIcon: css`
|
||||||
|
padding-left: ${theme.spacing.sm};
|
||||||
|
`,
|
||||||
|
// used for buttons with icon onlys
|
||||||
|
iconButton: css`
|
||||||
|
padding-right: 0;
|
||||||
|
`,
|
||||||
iconWrap: css`
|
iconWrap: css`
|
||||||
label: button-icon-wrap;
|
label: button-icon-wrap;
|
||||||
display: flex;
|
& + * {
|
||||||
align-items: center;
|
margin-left: ${theme.spacing.sm};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
@ -12,6 +12,8 @@ export interface StyleDeps {
|
|||||||
|
|
||||||
export interface ButtonStyles {
|
export interface ButtonStyles {
|
||||||
button: string;
|
button: string;
|
||||||
|
buttonWithIcon: string;
|
||||||
|
iconButton: string;
|
||||||
iconWrap: string;
|
iconWrap: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
}
|
}
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { select, text } from '@storybook/addon-knobs';
|
||||||
import { Button, ButtonVariant } from './Button';
|
import { Button, ButtonVariant } from './Button';
|
||||||
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
|
import { withCenteredStory, withHorizontallyCenteredStory } from '../../utils/storybook/withCenteredStory';
|
||||||
import { select, text } from '@storybook/addon-knobs';
|
import { getIconKnob } from '../../utils/storybook/knobs';
|
||||||
import { ButtonSize } from '../Button/types';
|
import { ButtonSize } from '../Button/types';
|
||||||
import mdx from './Button.mdx';
|
import mdx from './Button.mdx';
|
||||||
|
|
||||||
@ -24,9 +25,10 @@ export const simple = () => {
|
|||||||
const variant = select('Variant', variants, 'primary');
|
const variant = select('Variant', variants, 'primary');
|
||||||
const size = select('Size', sizes, 'md');
|
const size = select('Size', sizes, 'md');
|
||||||
const buttonText = text('text', 'Button');
|
const buttonText = text('text', 'Button');
|
||||||
|
const icon = getIconKnob();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant={variant as ButtonVariant} size={size as ButtonSize}>
|
<Button variant={variant as ButtonVariant} size={size as ButtonSize} icon={icon && `fa fa-${icon}`}>
|
||||||
{buttonText}
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
@ -76,6 +76,7 @@ const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) =>
|
|||||||
|
|
||||||
// Need to do this because of mismatch between variants in standard buttons and here
|
// Need to do this because of mismatch between variants in standard buttons and here
|
||||||
type StyleProps = Omit<StyleDeps, 'variant'> & { variant: ButtonVariant };
|
type StyleProps = Omit<StyleDeps, 'variant'> & { variant: ButtonVariant };
|
||||||
|
|
||||||
export const getButtonStyles = stylesFactory(({ theme, size, variant }: StyleProps) => {
|
export const getButtonStyles = stylesFactory(({ theme, size, variant }: StyleProps) => {
|
||||||
const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size);
|
const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size);
|
||||||
const { background, borderColor, variantStyles } = getPropertiesForVariant(theme, variant);
|
const { background, borderColor, variantStyles } = getPropertiesForVariant(theme, variant);
|
||||||
@ -83,7 +84,6 @@ export const getButtonStyles = stylesFactory(({ theme, size, variant }: StylePro
|
|||||||
return {
|
return {
|
||||||
button: cx(
|
button: cx(
|
||||||
css`
|
css`
|
||||||
position: relative;
|
|
||||||
label: button;
|
label: button;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -111,10 +111,18 @@ export const getButtonStyles = stylesFactory(({ theme, size, variant }: StylePro
|
|||||||
${variantStyles}
|
${variantStyles}
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
|
buttonWithIcon: css`
|
||||||
|
padding-left: ${theme.spacing.sm};
|
||||||
|
`,
|
||||||
|
// used for buttons with icon only
|
||||||
|
iconButton: css`
|
||||||
|
padding-right: 0;
|
||||||
|
`,
|
||||||
iconWrap: css`
|
iconWrap: css`
|
||||||
label: button-icon-wrap;
|
label: button-icon-wrap;
|
||||||
display: flex;
|
& + * {
|
||||||
align-items: center;
|
margin-left: ${theme.spacing.sm};
|
||||||
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@ -130,25 +138,25 @@ type CommonProps = {
|
|||||||
className?: string;
|
className?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
export type ButtonProps = CommonProps & ButtonHTMLAttributes<HTMLButtonElement>;
|
||||||
|
|
||||||
export const Button = (props: ButtonProps) => {
|
export const Button = React.forwardRef<HTMLButtonElement, ButtonProps>((props, ref) => {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
const styles = getButtonStyles({
|
const styles = getButtonStyles({
|
||||||
theme,
|
theme,
|
||||||
size: props.size || 'md',
|
size: props.size || 'md',
|
||||||
variant: props.variant || 'primary',
|
variant: props.variant || 'primary',
|
||||||
});
|
});
|
||||||
return <DefaultButton {...props} styles={styles} />;
|
return <DefaultButton {...props} styles={styles} ref={ref} />;
|
||||||
};
|
});
|
||||||
|
|
||||||
type ButtonLinkProps = CommonProps & AnchorHTMLAttributes<HTMLAnchorElement>;
|
type ButtonLinkProps = CommonProps & AnchorHTMLAttributes<HTMLAnchorElement>;
|
||||||
export const LinkButton = (props: ButtonLinkProps) => {
|
export const LinkButton = React.forwardRef<HTMLAnchorElement, ButtonLinkProps>((props, ref) => {
|
||||||
const theme = useContext(ThemeContext);
|
const theme = useContext(ThemeContext);
|
||||||
const styles = getButtonStyles({
|
const styles = getButtonStyles({
|
||||||
theme,
|
theme,
|
||||||
size: props.size || 'md',
|
size: props.size || 'md',
|
||||||
variant: props.variant || 'primary',
|
variant: props.variant || 'primary',
|
||||||
});
|
});
|
||||||
return <DefaultLinkButton {...props} styles={styles} />;
|
return <DefaultLinkButton {...props} styles={styles} ref={ref} />;
|
||||||
};
|
});
|
||||||
|
@ -55,10 +55,15 @@ export const simple = () => {
|
|||||||
const addonAfter = <Button variant="secondary">Load</Button>;
|
const addonAfter = <Button variant="secondary">Load</Button>;
|
||||||
const addonBefore = <div style={{ display: 'flex', alignItems: 'center', padding: '5px' }}>Input</div>;
|
const addonBefore = <div style={{ display: 'flex', alignItems: 'center', padding: '5px' }}>Input</div>;
|
||||||
const prefix = select('Prefix', prefixSuffixOpts, null, VISUAL_GROUP);
|
const prefix = select('Prefix', prefixSuffixOpts, null, VISUAL_GROUP);
|
||||||
|
const suffix = select('Suffix', prefixSuffixOpts, null, VISUAL_GROUP);
|
||||||
let prefixEl: any = prefix;
|
let prefixEl: any = prefix;
|
||||||
if (prefix && prefix.match(/icon-/g)) {
|
if (prefix && prefix.match(/icon-/g)) {
|
||||||
prefixEl = <Icon name={prefix.replace(/icon-/g, '') as IconType} />;
|
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 CONTAINER_GROUP = 'Container options';
|
||||||
// ---
|
// ---
|
||||||
@ -80,6 +85,7 @@ export const simple = () => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
invalid={invalid}
|
invalid={invalid}
|
||||||
prefix={prefixEl}
|
prefix={prefixEl}
|
||||||
|
suffix={suffixEl}
|
||||||
loading={loading}
|
loading={loading}
|
||||||
addonBefore={before && addonBefore}
|
addonBefore={before && addonBefore}
|
||||||
addonAfter={after && addonAfter}
|
addonAfter={after && addonAfter}
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React, { FC, HTMLProps, ReactNode } from 'react';
|
import React, { HTMLProps, ReactNode } from 'react';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
import { css, cx } from 'emotion';
|
import { css, cx } from 'emotion';
|
||||||
import { getFocusStyle, inputSizes, sharedInputStyle } from '../commonStyles';
|
import { getFocusStyle, inputSizes, sharedInputStyle } from '../commonStyles';
|
||||||
@ -12,6 +12,8 @@ export interface Props extends Omit<HTMLProps<HTMLInputElement>, 'prefix' | 'siz
|
|||||||
invalid?: boolean;
|
invalid?: boolean;
|
||||||
/** Show an icon as a prefix in the input */
|
/** Show an icon as a prefix in the input */
|
||||||
prefix?: JSX.Element | string | null;
|
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 */
|
/** Show a loading indicator as a suffix in the input */
|
||||||
loading?: boolean;
|
loading?: boolean;
|
||||||
/** Add a component as an addon before the input */
|
/** Add a component as an addon before the input */
|
||||||
@ -110,6 +112,9 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe
|
|||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
padding-right: ${prefixSuffixStaticWidth};
|
padding-right: ${prefixSuffixStaticWidth};
|
||||||
}
|
}
|
||||||
|
&[readonly] {
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
|
||||||
@ -124,8 +129,6 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe
|
|||||||
border-radius: ${borderRadius};
|
border-radius: ${borderRadius};
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
padding: 0 ${theme.spacing.sm} 0 ${theme.spacing.sm};
|
|
||||||
font-size: ${theme.typography.size.md};
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Restoring increase/decrease spinner on number inputs. Overwriting rules implemented in
|
Restoring increase/decrease spinner on number inputs. Overwriting rules implemented in
|
||||||
@ -143,6 +146,10 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
|
inputDisabled: css`
|
||||||
|
background-color: ${colors.formInputBgDisabled};
|
||||||
|
color: ${colors.formInputDisabledText};
|
||||||
|
`,
|
||||||
addon: css`
|
addon: css`
|
||||||
label: input-addon;
|
label: input-addon;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -184,7 +191,6 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe
|
|||||||
border-right: none;
|
border-right: none;
|
||||||
border-top-right-radius: 0;
|
border-top-right-radius: 0;
|
||||||
border-bottom-right-radius: 0;
|
border-bottom-right-radius: 0;
|
||||||
left: 0;
|
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
suffix: cx(
|
suffix: cx(
|
||||||
@ -199,11 +205,16 @@ export const getInputStyles = stylesFactory(({ theme, invalid = false }: StyleDe
|
|||||||
right: 0;
|
right: 0;
|
||||||
`
|
`
|
||||||
),
|
),
|
||||||
|
loadingIndicator: css`
|
||||||
|
& + * {
|
||||||
|
margin-left: ${theme.spacing.xs};
|
||||||
|
}
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Input: FC<Props> = props => {
|
export const Input = React.forwardRef<HTMLInputElement, Props>((props, ref) => {
|
||||||
const { addonAfter, addonBefore, prefix, invalid, loading, size = 'auto', ...restProps } = props;
|
const { 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
|
* 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)).
|
* when prefix/suffix is larger than default (28px = 16px(icon) + 12px(left/right paddings)).
|
||||||
@ -227,6 +238,7 @@ export const Input: FC<Props> = props => {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<input
|
<input
|
||||||
|
ref={ref}
|
||||||
className={styles.input}
|
className={styles.input}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
style={{
|
style={{
|
||||||
@ -235,9 +247,10 @@ export const Input: FC<Props> = props => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{loading && (
|
{(suffix || loading) && (
|
||||||
<div className={styles.suffix} ref={suffixRef}>
|
<div className={styles.suffix} ref={suffixRef}>
|
||||||
<Icon name="spinner" className="fa-spin" />
|
{loading && <Icon name="spinner" className={cx('fa-spin', styles.loadingIndicator)} />}
|
||||||
|
{suffix}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -245,4 +258,4 @@ export const Input: FC<Props> = props => {
|
|||||||
{!!addonAfter && <div className={styles.addon}>{addonAfter}</div>}
|
{!!addonAfter && <div className={styles.addon}>{addonAfter}</div>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Button, ButtonVariant, ButtonProps } from '../Button';
|
||||||
|
import { ButtonSize } from '../../Button/types';
|
||||||
|
import { SelectCommonProps, SelectBase } from './SelectBase';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { useTheme } from '../../../themes';
|
||||||
|
import { Icon } from '../../Icon/Icon';
|
||||||
|
import { IconType } from '../../Icon/types';
|
||||||
|
|
||||||
|
interface ButtonSelectProps<T> extends Omit<SelectCommonProps<T>, 'renderControl' | 'size' | 'prefix'> {
|
||||||
|
icon?: IconType;
|
||||||
|
variant?: ButtonVariant;
|
||||||
|
size?: ButtonSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface SelectButtonProps extends Omit<ButtonProps, 'icon'> {
|
||||||
|
icon?: IconType;
|
||||||
|
isOpen?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectButton: React.FC<SelectButtonProps> = ({ icon, children, isOpen, ...buttonProps }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = {
|
||||||
|
wrapper: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
max-width: 200px;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`,
|
||||||
|
iconWrap: css`
|
||||||
|
padding: 0 15px 0 0;
|
||||||
|
`,
|
||||||
|
caretWrap: css`
|
||||||
|
padding-left: ${theme.spacing.sm};
|
||||||
|
margin-left: ${theme.spacing.sm};
|
||||||
|
margin-right: -${theme.spacing.sm};
|
||||||
|
height: 100%;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
const buttonIcon = `fa fa-${icon}`;
|
||||||
|
const caretIcon = isOpen ? 'caret-up' : 'caret-down';
|
||||||
|
return (
|
||||||
|
<Button {...buttonProps} icon={buttonIcon}>
|
||||||
|
<span className={styles.wrapper}>
|
||||||
|
<span>{children}</span>
|
||||||
|
<span className={styles.caretWrap}>
|
||||||
|
<Icon name={caretIcon} />
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ButtonSelect<T>({
|
||||||
|
placeholder,
|
||||||
|
icon,
|
||||||
|
variant = 'primary',
|
||||||
|
size = 'md',
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
...selectProps
|
||||||
|
}: ButtonSelectProps<T>) {
|
||||||
|
const buttonProps = {
|
||||||
|
icon,
|
||||||
|
variant,
|
||||||
|
size,
|
||||||
|
className,
|
||||||
|
disabled,
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SelectBase
|
||||||
|
{...selectProps}
|
||||||
|
renderControl={({ onBlur, onClick, value, isOpen }) => {
|
||||||
|
return (
|
||||||
|
<SelectButton {...buttonProps} onBlur={onBlur} onClick={onClick} isOpen={isOpen}>
|
||||||
|
{value ? value.label : placeholder}
|
||||||
|
</SelectButton>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Icon } from '../../Icon/Icon';
|
||||||
|
|
||||||
|
interface DropdownIndicatorProps {
|
||||||
|
isOpen: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DropdownIndicator: React.FC<DropdownIndicatorProps> = ({ isOpen }) => {
|
||||||
|
const icon = isOpen ? 'caret-up' : 'caret-down';
|
||||||
|
return <Icon name={icon} />;
|
||||||
|
};
|
@ -0,0 +1,25 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTheme } from '../../../themes/ThemeContext';
|
||||||
|
import { getInputStyles } from '../Input/Input';
|
||||||
|
import { cx, css } from 'emotion';
|
||||||
|
|
||||||
|
export const IndicatorsContainer = React.forwardRef<HTMLDivElement, React.PropsWithChildren<any>>((props, ref) => {
|
||||||
|
const { children } = props;
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getInputStyles({ theme, invalid: false });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={cx(
|
||||||
|
styles.suffix,
|
||||||
|
css`
|
||||||
|
position: relative;
|
||||||
|
top: auto;
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
@ -0,0 +1,71 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTheme } from '../../../themes/ThemeContext';
|
||||||
|
import { getFocusCss, sharedInputStyle } from '../commonStyles';
|
||||||
|
import { getInputStyles } from '../Input/Input';
|
||||||
|
import { cx, css } from 'emotion';
|
||||||
|
import { stylesFactory } from '../../../themes';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
|
interface InputControlProps {
|
||||||
|
/** Show an icon as a prefix in the input */
|
||||||
|
prefix?: JSX.Element | string | null;
|
||||||
|
focused: boolean;
|
||||||
|
invalid: boolean;
|
||||||
|
disabled: boolean;
|
||||||
|
innerProps: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getInputControlStyles = stylesFactory(
|
||||||
|
(theme: GrafanaTheme, invalid: boolean, focused: boolean, disabled: boolean, withPrefix: boolean) => {
|
||||||
|
const styles = getInputStyles({ theme, invalid });
|
||||||
|
|
||||||
|
return {
|
||||||
|
wrapper: cx(
|
||||||
|
styles.wrapper,
|
||||||
|
sharedInputStyle(theme, invalid),
|
||||||
|
focused &&
|
||||||
|
css`
|
||||||
|
${getFocusCss(theme)}
|
||||||
|
`,
|
||||||
|
disabled && styles.inputDisabled,
|
||||||
|
css`
|
||||||
|
min-height: 32px;
|
||||||
|
height: auto;
|
||||||
|
flex-direction: row;
|
||||||
|
padding-right: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
align-items: center;
|
||||||
|
cursor: default;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: space-between;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
`,
|
||||||
|
withPrefix &&
|
||||||
|
css`
|
||||||
|
padding-left: 0;
|
||||||
|
`
|
||||||
|
),
|
||||||
|
prefix: cx(
|
||||||
|
styles.prefix,
|
||||||
|
css`
|
||||||
|
position: relative;
|
||||||
|
`
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export const InputControl = React.forwardRef<HTMLDivElement, React.PropsWithChildren<InputControlProps>>(
|
||||||
|
function InputControl({ focused, invalid, disabled, children, innerProps, prefix, ...otherProps }, ref) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getInputControlStyles(theme, invalid, focused, disabled, !!prefix);
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper} {...innerProps} ref={ref}>
|
||||||
|
{prefix && <div className={cx(styles.prefix)}>{prefix}</div>}
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,33 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTheme } from '../../../themes';
|
||||||
|
import { getSelectStyles } from './getSelectStyles';
|
||||||
|
import { Icon } from '../../Icon/Icon';
|
||||||
|
|
||||||
|
interface MultiValueContainerProps {
|
||||||
|
innerProps: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiValueContainer: React.FC<MultiValueContainerProps> = ({ innerProps, children }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getSelectStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div {...innerProps} className={styles.multiValueContainer}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export type MultiValueRemoveProps = {
|
||||||
|
innerProps: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MultiValueRemove: React.FC<MultiValueRemoveProps> = ({ children, innerProps }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getSelectStyles(theme);
|
||||||
|
return (
|
||||||
|
<div {...innerProps} className={styles.multiValueRemove}>
|
||||||
|
<Icon name="times" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
265
packages/grafana-ui/src/components/Forms/Select/Select.story.tsx
Normal file
265
packages/grafana-ui/src/components/Forms/Select/Select.story.tsx
Normal file
@ -0,0 +1,265 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { Select, AsyncSelect, MultiSelect, AsyncMultiSelect } from './Select';
|
||||||
|
import { withCenteredStory, withHorizontallyCenteredStory } from '../../../utils/storybook/withCenteredStory';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { getAvailableIcons, IconType } from '../../Icon/types';
|
||||||
|
import { select, boolean } from '@storybook/addon-knobs';
|
||||||
|
import { Icon } from '../../Icon/Icon';
|
||||||
|
import { Button } from '../Button';
|
||||||
|
import { ButtonSelect } from './ButtonSelect';
|
||||||
|
import { getIconKnob } from '../../../utils/storybook/knobs';
|
||||||
|
import kebabCase from 'lodash/kebabCase';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'UI/Forms/Select',
|
||||||
|
component: Select,
|
||||||
|
decorators: [withCenteredStory, withHorizontallyCenteredStory],
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateOptions = () => {
|
||||||
|
const values = [
|
||||||
|
'Sharilyn Markowitz',
|
||||||
|
'Naomi Striplin',
|
||||||
|
'Beau Bevel',
|
||||||
|
'Garrett Starkes',
|
||||||
|
'Hildegarde Pedro',
|
||||||
|
'Gudrun Seyler',
|
||||||
|
'Eboni Raines',
|
||||||
|
'Hye Felix',
|
||||||
|
'Chau Brito',
|
||||||
|
'Heidy Zook',
|
||||||
|
'Karima Husain',
|
||||||
|
'Virgil Mckinny',
|
||||||
|
'Kaley Dodrill',
|
||||||
|
'Sharan Ruf',
|
||||||
|
'Edgar Loveland',
|
||||||
|
'Judie Sanger',
|
||||||
|
'Season Bundrick',
|
||||||
|
'Ok Vicente',
|
||||||
|
'Garry Spitz',
|
||||||
|
'Han Harnish',
|
||||||
|
];
|
||||||
|
|
||||||
|
return values.map<SelectableValue<string>>(name => ({
|
||||||
|
value: kebabCase(name),
|
||||||
|
label: name,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadAsyncOptions = () => {
|
||||||
|
return new Promise<Array<SelectableValue<string>>>(resolve => {
|
||||||
|
setTimeout(() => {
|
||||||
|
resolve(generateOptions());
|
||||||
|
}, 2000);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getKnobs = () => {
|
||||||
|
const BEHAVIOUR_GROUP = 'Behaviour props';
|
||||||
|
const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP);
|
||||||
|
const invalid = boolean('Invalid', false, BEHAVIOUR_GROUP);
|
||||||
|
const loading = boolean('Loading', false, BEHAVIOUR_GROUP);
|
||||||
|
const prefixSuffixOpts = {
|
||||||
|
None: null,
|
||||||
|
Text: '$',
|
||||||
|
...getAvailableIcons().reduce<Record<string, string>>((prev, c) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[`Icon: ${c}`]: `icon-${c}`,
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
const VISUAL_GROUP = 'Visual options';
|
||||||
|
// ---
|
||||||
|
const prefix = select('Prefix', prefixSuffixOpts, null, VISUAL_GROUP);
|
||||||
|
|
||||||
|
let prefixEl: any = prefix;
|
||||||
|
if (prefix && prefix.match(/icon-/g)) {
|
||||||
|
prefixEl = <Icon name={prefix.replace(/icon-/g, '') as IconType} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
disabled,
|
||||||
|
invalid,
|
||||||
|
loading,
|
||||||
|
prefixEl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getDynamicProps = () => {
|
||||||
|
const knobs = getKnobs();
|
||||||
|
return {
|
||||||
|
disabled: knobs.disabled,
|
||||||
|
isLoading: knobs.loading,
|
||||||
|
invalid: knobs.invalid,
|
||||||
|
prefix: knobs.prefixEl,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const basic = () => {
|
||||||
|
const [value, setValue] = useState<SelectableValue<string>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
options={generateOptions()}
|
||||||
|
value={value}
|
||||||
|
onChange={v => {
|
||||||
|
setValue(v);
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
{...getDynamicProps()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses plain values instead of SelectableValue<T>
|
||||||
|
*/
|
||||||
|
export const basicSelectPlainValue = () => {
|
||||||
|
const [value, setValue] = useState<string>();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
options={generateOptions()}
|
||||||
|
value={value}
|
||||||
|
onChange={v => {
|
||||||
|
setValue(v.value);
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
{...getDynamicProps()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses plain values instead of SelectableValue<T>
|
||||||
|
*/
|
||||||
|
export const multiPlainValue = () => {
|
||||||
|
const [value, setValue] = useState<string[]>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MultiSelect
|
||||||
|
options={generateOptions()}
|
||||||
|
value={value}
|
||||||
|
onChange={v => {
|
||||||
|
setValue(v.map((v: any) => v.value));
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
{...getDynamicProps()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const multiSelect = () => {
|
||||||
|
const [value, setValue] = useState<Array<SelectableValue<string>>>([]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<MultiSelect
|
||||||
|
options={generateOptions()}
|
||||||
|
value={value}
|
||||||
|
onChange={v => {
|
||||||
|
setValue(v);
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
{...getDynamicProps()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const multiSelectAsync = () => {
|
||||||
|
const [value, setValue] = useState<Array<SelectableValue<string>>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncMultiSelect
|
||||||
|
loadOptions={loadAsyncOptions}
|
||||||
|
defaultOptions
|
||||||
|
value={value}
|
||||||
|
onChange={v => {
|
||||||
|
setValue(v);
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
allowCustomValue
|
||||||
|
{...getDynamicProps()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export const buttonSelect = () => {
|
||||||
|
const [value, setValue] = useState<SelectableValue<string>>();
|
||||||
|
const icon = getIconKnob();
|
||||||
|
return (
|
||||||
|
<ButtonSelect
|
||||||
|
placeholder="Select all the things..."
|
||||||
|
value={value}
|
||||||
|
options={generateOptions()}
|
||||||
|
onChange={v => {
|
||||||
|
setValue(v);
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
allowCustomValue
|
||||||
|
icon={icon}
|
||||||
|
{...getDynamicProps()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const basicSelectAsync = () => {
|
||||||
|
const [value, setValue] = useState<SelectableValue<string>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AsyncSelect
|
||||||
|
loadOptions={loadAsyncOptions}
|
||||||
|
defaultOptions
|
||||||
|
value={value}
|
||||||
|
onChange={v => {
|
||||||
|
setValue(v);
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
{...getDynamicProps()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const customizedControl = () => {
|
||||||
|
const [value, setValue] = useState<SelectableValue<string>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Select
|
||||||
|
options={generateOptions()}
|
||||||
|
value={value}
|
||||||
|
onChange={v => {
|
||||||
|
setValue(v);
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
renderControl={({ isOpen, value, ...otherProps }) => {
|
||||||
|
return <Button {...otherProps}> {isOpen ? 'Open' : 'Closed'}</Button>;
|
||||||
|
}}
|
||||||
|
{...getDynamicProps()}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const customValueCreation = () => {
|
||||||
|
const [value, setValue] = useState<SelectableValue<string>>();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Select
|
||||||
|
options={generateOptions()}
|
||||||
|
value={value}
|
||||||
|
onChange={v => {
|
||||||
|
setValue(v);
|
||||||
|
}}
|
||||||
|
size="md"
|
||||||
|
allowCustomValue
|
||||||
|
{...getDynamicProps()}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
31
packages/grafana-ui/src/components/Forms/Select/Select.tsx
Normal file
31
packages/grafana-ui/src/components/Forms/Select/Select.tsx
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { SelectCommonProps, SelectBase, MultiSelectCommonProps, SelectAsyncProps } from './SelectBase';
|
||||||
|
|
||||||
|
export function Select<T>(props: SelectCommonProps<T>) {
|
||||||
|
return <SelectBase {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MultiSelect<T>(props: MultiSelectCommonProps<T>) {
|
||||||
|
// @ts-ignore
|
||||||
|
return <SelectBase {...props} isMulti />;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AsyncSelectProps<T> extends Omit<SelectCommonProps<T>, 'options'>, SelectAsyncProps<T> {
|
||||||
|
// AsyncSelect has options stored internally. We cannot enable plain values as we don't have access to the fetched options
|
||||||
|
value?: SelectableValue<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsyncSelect<T>(props: AsyncSelectProps<T>) {
|
||||||
|
return <SelectBase {...props} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AsyncMultiSelectProps<T> extends Omit<MultiSelectCommonProps<T>, 'options'>, SelectAsyncProps<T> {
|
||||||
|
// AsyncSelect has options stored internally. We cannot enable plain values as we don't have access to the fetched options
|
||||||
|
value?: Array<SelectableValue<T>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function AsyncMultiSelect<T>(props: AsyncMultiSelectProps<T>) {
|
||||||
|
// @ts-ignore
|
||||||
|
return <SelectBase {...props} isMulti />;
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount, ReactWrapper } from 'enzyme';
|
||||||
|
import { SelectBase } from './SelectBase';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
|
const onChangeHandler = () => jest.fn();
|
||||||
|
const findMenuElement = (container: ReactWrapper) => container.find({ 'aria-label': 'Select options menu' });
|
||||||
|
const options: Array<SelectableValue<number>> = [
|
||||||
|
{
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: 'Option 2',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('SelectBase', () => {
|
||||||
|
it('renders without error', () => {
|
||||||
|
mount(<SelectBase onChange={onChangeHandler} />);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders empty options information', () => {
|
||||||
|
const container = mount(<SelectBase onChange={onChangeHandler} isOpen />);
|
||||||
|
const noopt = container.find({ 'aria-label': 'No options provided' });
|
||||||
|
expect(noopt).toHaveLength(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when openMenuOnFocus prop', () => {
|
||||||
|
describe('is provided', () => {
|
||||||
|
it('opens on focus', () => {
|
||||||
|
const container = mount(<SelectBase onChange={onChangeHandler} openMenuOnFocus />);
|
||||||
|
container.find('input').simulate('focus');
|
||||||
|
|
||||||
|
const menu = findMenuElement(container);
|
||||||
|
expect(menu).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('is not provided', () => {
|
||||||
|
it.each`
|
||||||
|
key
|
||||||
|
${'ArrowDown'}
|
||||||
|
${'ArrowUp'}
|
||||||
|
${' '}
|
||||||
|
`('opens on arrow down/up or space', ({ key }) => {
|
||||||
|
const container = mount(<SelectBase onChange={onChangeHandler} />);
|
||||||
|
const input = container.find('input');
|
||||||
|
input.simulate('focus');
|
||||||
|
input.simulate('keydown', { key });
|
||||||
|
const menu = findMenuElement(container);
|
||||||
|
expect(menu).toHaveLength(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('options', () => {
|
||||||
|
it('renders menu with provided options', () => {
|
||||||
|
const container = mount(<SelectBase options={options} onChange={onChangeHandler} isOpen />);
|
||||||
|
const menuOptions = container.find({ 'aria-label': 'Select option' });
|
||||||
|
expect(menuOptions).toHaveLength(2);
|
||||||
|
});
|
||||||
|
it('call onChange handler when option is selected', () => {
|
||||||
|
const spy = jest.fn();
|
||||||
|
const handler = (value: SelectableValue<number>) => spy(value);
|
||||||
|
const container = mount(<SelectBase options={options} onChange={handler} isOpen />);
|
||||||
|
const menuOptions = container.find({ 'aria-label': 'Select option' });
|
||||||
|
expect(menuOptions).toHaveLength(2);
|
||||||
|
const menuOption = menuOptions.first();
|
||||||
|
menuOption.simulate('click');
|
||||||
|
|
||||||
|
expect(spy).toBeCalledWith({
|
||||||
|
label: 'Option 1',
|
||||||
|
value: 1,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
315
packages/grafana-ui/src/components/Forms/Select/SelectBase.tsx
Normal file
315
packages/grafana-ui/src/components/Forms/Select/SelectBase.tsx
Normal file
@ -0,0 +1,315 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { SelectableValue, deprecationWarning } from '@grafana/data';
|
||||||
|
// @ts-ignore
|
||||||
|
import { default as ReactSelect, Creatable } from '@torkelo/react-select';
|
||||||
|
// @ts-ignore
|
||||||
|
import { default as ReactAsyncSelect } from '@torkelo/react-select/lib/Async';
|
||||||
|
|
||||||
|
import { Icon } from '../../Icon/Icon';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { inputSizes } from '../commonStyles';
|
||||||
|
import { FormInputSize } from '../types';
|
||||||
|
import resetSelectStyles from './resetSelectStyles';
|
||||||
|
import { SelectMenu, SelectMenuOptions } from './SelectMenu';
|
||||||
|
import { IndicatorsContainer } from './IndicatorsContainer';
|
||||||
|
import { ValueContainer } from './ValueContainer';
|
||||||
|
import { InputControl } from './InputControl';
|
||||||
|
import { DropdownIndicator } from './DropdownIndicator';
|
||||||
|
import { SelectOptionGroup } from './SelectOptionGroup';
|
||||||
|
import { SingleValue } from './SingleValue';
|
||||||
|
import { MultiValueContainer, MultiValueRemove } from './MultiValue';
|
||||||
|
import { useTheme } from '../../../themes';
|
||||||
|
import { getSelectStyles } from './getSelectStyles';
|
||||||
|
|
||||||
|
type SelectValue<T> = T | SelectableValue<T> | T[] | Array<SelectableValue<T>>;
|
||||||
|
|
||||||
|
export interface SelectCommonProps<T> {
|
||||||
|
className?: string;
|
||||||
|
options?: Array<SelectableValue<T>>;
|
||||||
|
defaultValue?: any;
|
||||||
|
value?: SelectValue<T>;
|
||||||
|
getOptionLabel?: (item: SelectableValue<T>) => string;
|
||||||
|
getOptionValue?: (item: SelectableValue<T>) => string;
|
||||||
|
onChange: (value: SelectableValue<T>) => {} | void;
|
||||||
|
placeholder?: string;
|
||||||
|
disabled?: boolean;
|
||||||
|
isSearchable?: boolean;
|
||||||
|
isClearable?: boolean;
|
||||||
|
autoFocus?: boolean;
|
||||||
|
openMenuOnFocus?: boolean;
|
||||||
|
onBlur?: () => void;
|
||||||
|
maxMenuHeight?: number;
|
||||||
|
isLoading?: boolean;
|
||||||
|
noOptionsMessage?: string;
|
||||||
|
isMulti?: boolean;
|
||||||
|
backspaceRemovesValue?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
components?: any;
|
||||||
|
onOpenMenu?: () => void;
|
||||||
|
onCloseMenu?: () => void;
|
||||||
|
tabSelectsValue?: boolean;
|
||||||
|
formatCreateLabel?: (input: string) => string;
|
||||||
|
allowCustomValue?: boolean;
|
||||||
|
width?: number;
|
||||||
|
size?: FormInputSize;
|
||||||
|
/** item to be rendered in front of the input */
|
||||||
|
prefix?: JSX.Element | string | null;
|
||||||
|
renderControl?: ControlComponent<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectAsyncProps<T> {
|
||||||
|
/** When specified as boolean the loadOptions will execute when component is mounted */
|
||||||
|
defaultOptions?: boolean | Array<SelectableValue<T>>;
|
||||||
|
/** Asynchroniously load select options */
|
||||||
|
loadOptions?: (query: string) => Promise<Array<SelectableValue<T>>>;
|
||||||
|
/** Message to display when options are loading */
|
||||||
|
loadingMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MultiSelectCommonProps<T> extends Omit<SelectCommonProps<T>, 'onChange' | 'isMulti' | 'value'> {
|
||||||
|
value?: Array<SelectableValue<T>> | T[];
|
||||||
|
onChange: (item: Array<SelectableValue<T>>) => {} | void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SelectBaseProps<T> extends SelectCommonProps<T>, SelectAsyncProps<T> {
|
||||||
|
invalid?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CustomControlProps<T> {
|
||||||
|
ref: React.Ref<any>;
|
||||||
|
isOpen: boolean;
|
||||||
|
/** Currently selected value */
|
||||||
|
value?: SelectableValue<T>;
|
||||||
|
/** onClick will be automatically passed to custom control allowing menu toggle */
|
||||||
|
onClick: () => void;
|
||||||
|
/** onBlur will be automatically passed to custom control closing the menu on element blur */
|
||||||
|
onBlur: () => void;
|
||||||
|
disabled: boolean;
|
||||||
|
invalid: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ControlComponent<T> = React.ComponentType<CustomControlProps<T>>;
|
||||||
|
|
||||||
|
const CustomControl = (props: any) => {
|
||||||
|
const {
|
||||||
|
children,
|
||||||
|
innerProps,
|
||||||
|
selectProps: { menuIsOpen, onMenuClose, onMenuOpen },
|
||||||
|
isFocused,
|
||||||
|
isMulti,
|
||||||
|
getValue,
|
||||||
|
innerRef,
|
||||||
|
} = props;
|
||||||
|
const selectProps = props.selectProps as SelectBaseProps<any>;
|
||||||
|
|
||||||
|
if (selectProps.renderControl) {
|
||||||
|
return React.createElement(selectProps.renderControl, {
|
||||||
|
isOpen: menuIsOpen,
|
||||||
|
value: isMulti ? getValue() : getValue()[0],
|
||||||
|
ref: innerRef,
|
||||||
|
onClick: menuIsOpen ? onMenuClose : onMenuOpen,
|
||||||
|
onBlur: onMenuClose,
|
||||||
|
disabled: !!selectProps.disabled,
|
||||||
|
invalid: !!selectProps.invalid,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InputControl
|
||||||
|
ref={innerRef}
|
||||||
|
innerProps={innerProps}
|
||||||
|
prefix={selectProps.prefix}
|
||||||
|
focused={isFocused}
|
||||||
|
invalid={!!selectProps.invalid}
|
||||||
|
disabled={!!selectProps.disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</InputControl>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SelectBase<T>({
|
||||||
|
value,
|
||||||
|
defaultValue,
|
||||||
|
options = [],
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
onCloseMenu,
|
||||||
|
onOpenMenu,
|
||||||
|
placeholder = 'Choose',
|
||||||
|
getOptionValue,
|
||||||
|
getOptionLabel,
|
||||||
|
isSearchable = true,
|
||||||
|
disabled = false,
|
||||||
|
isClearable = false,
|
||||||
|
isMulti = false,
|
||||||
|
isLoading = false,
|
||||||
|
isOpen,
|
||||||
|
autoFocus = false,
|
||||||
|
openMenuOnFocus = false,
|
||||||
|
maxMenuHeight = 300,
|
||||||
|
noOptionsMessage = 'No options found',
|
||||||
|
tabSelectsValue = true,
|
||||||
|
backspaceRemovesValue = true,
|
||||||
|
allowCustomValue = false,
|
||||||
|
size = 'auto',
|
||||||
|
prefix,
|
||||||
|
formatCreateLabel,
|
||||||
|
loadOptions,
|
||||||
|
loadingMessage = 'Loading options...',
|
||||||
|
defaultOptions,
|
||||||
|
renderControl,
|
||||||
|
width,
|
||||||
|
invalid,
|
||||||
|
components,
|
||||||
|
}: SelectBaseProps<T>) {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getSelectStyles(theme);
|
||||||
|
let Component: ReactSelect | Creatable = ReactSelect;
|
||||||
|
const creatableProps: any = {};
|
||||||
|
let asyncSelectProps: any = {};
|
||||||
|
|
||||||
|
let selectedValue = [];
|
||||||
|
if (isMulti && loadOptions) {
|
||||||
|
selectedValue = value as any;
|
||||||
|
} else {
|
||||||
|
// If option is passed as a plain value (value property from SelectableValue property)
|
||||||
|
// we are selecting the corresponding value from the options
|
||||||
|
if (isMulti && value && Array.isArray(value) && !loadOptions) {
|
||||||
|
// @ts-ignore
|
||||||
|
selectedValue = value.map(v => {
|
||||||
|
return options.filter(o => {
|
||||||
|
return v === o.value || o.value === v.value;
|
||||||
|
})[0];
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
selectedValue = options.filter(o => o.value === value || o === value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const commonSelectProps = {
|
||||||
|
autoFocus,
|
||||||
|
placeholder,
|
||||||
|
isSearchable,
|
||||||
|
// Passing isDisabled as react-select accepts this prop
|
||||||
|
isDisabled: disabled,
|
||||||
|
// Also passing disabled, as this is the new Select API, and I want to use this prop instead of react-select's one
|
||||||
|
disabled,
|
||||||
|
invalid,
|
||||||
|
prefix,
|
||||||
|
isClearable,
|
||||||
|
isLoading,
|
||||||
|
menuIsOpen: isOpen,
|
||||||
|
defaultValue,
|
||||||
|
value: isMulti ? selectedValue : selectedValue[0],
|
||||||
|
getOptionLabel,
|
||||||
|
getOptionValue,
|
||||||
|
openMenuOnFocus,
|
||||||
|
maxMenuHeight,
|
||||||
|
isMulti,
|
||||||
|
backspaceRemovesValue,
|
||||||
|
onMenuOpen: onOpenMenu,
|
||||||
|
onMenuClose: onCloseMenu,
|
||||||
|
tabSelectsValue,
|
||||||
|
options,
|
||||||
|
onChange,
|
||||||
|
onBlur,
|
||||||
|
menuShouldScrollIntoView: false,
|
||||||
|
renderControl,
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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) {
|
||||||
|
Component = Creatable;
|
||||||
|
creatableProps.formatCreateLabel = formatCreateLabel ?? ((input: string) => `Create: ${input}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instead of having AsyncSelect, as a separate component we render ReactAsyncSelect
|
||||||
|
if (loadOptions) {
|
||||||
|
Component = ReactAsyncSelect;
|
||||||
|
asyncSelectProps = {
|
||||||
|
loadOptions,
|
||||||
|
defaultOptions,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Component
|
||||||
|
components={{
|
||||||
|
MenuList: SelectMenu,
|
||||||
|
Group: SelectOptionGroup,
|
||||||
|
ValueContainer: ValueContainer,
|
||||||
|
IndicatorsContainer: IndicatorsContainer,
|
||||||
|
IndicatorSeparator: () => <></>,
|
||||||
|
Control: CustomControl,
|
||||||
|
Option: SelectMenuOptions,
|
||||||
|
ClearIndicator: (props: any) => {
|
||||||
|
const { clearValue } = props;
|
||||||
|
return (
|
||||||
|
<Icon
|
||||||
|
name="times"
|
||||||
|
onMouseDown={e => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
clearValue();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
LoadingIndicator: (props: any) => {
|
||||||
|
return <Icon name="spinner" className="fa fa-spin" />;
|
||||||
|
},
|
||||||
|
LoadingMessage: (props: any) => {
|
||||||
|
return <div className={styles.loadingMessage}>{loadingMessage}</div>;
|
||||||
|
},
|
||||||
|
NoOptionsMessage: (props: any) => {
|
||||||
|
return (
|
||||||
|
<div className={styles.loadingMessage} aria-label="No options provided">
|
||||||
|
{noOptionsMessage}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
DropdownIndicator: (props: any) => <DropdownIndicator isOpen={props.selectProps.menuIsOpen} />,
|
||||||
|
SingleValue: SingleValue,
|
||||||
|
MultiValueContainer: MultiValueContainer,
|
||||||
|
MultiValueRemove: MultiValueRemove,
|
||||||
|
...components,
|
||||||
|
}}
|
||||||
|
styles={{
|
||||||
|
...resetSelectStyles(),
|
||||||
|
singleValue: () => {
|
||||||
|
return css`
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
container: () => {
|
||||||
|
return css`
|
||||||
|
position: relative;
|
||||||
|
${inputSizes()[size]}
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
placeholder: () => {
|
||||||
|
return css`
|
||||||
|
display: inline-block;
|
||||||
|
color: hsl(0, 0%, 50%);
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
box-sizing: border-box;
|
||||||
|
line-height: 1;
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
className={widthClass}
|
||||||
|
{...commonSelectProps}
|
||||||
|
{...creatableProps}
|
||||||
|
{...asyncSelectProps}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,59 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { useTheme } from '../../../themes/ThemeContext';
|
||||||
|
import { getSelectStyles } from './getSelectStyles';
|
||||||
|
import { cx } from 'emotion';
|
||||||
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
import { Icon } from '../../Icon/Icon';
|
||||||
|
import { CustomScrollbar } from '../../CustomScrollbar/CustomScrollbar';
|
||||||
|
interface SelectMenuProps {
|
||||||
|
maxHeight: number;
|
||||||
|
innerRef: React.Ref<any>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectMenu = React.forwardRef<HTMLDivElement, React.PropsWithChildren<SelectMenuProps>>((props, ref) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getSelectStyles(theme);
|
||||||
|
const { children, maxHeight, innerRef } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.menu} ref={innerRef} style={{ maxHeight }} aria-label="Select options menu">
|
||||||
|
<CustomScrollbar autoHide={false} autoHeightMax="inherit" hideHorizontalTrack>
|
||||||
|
{children}
|
||||||
|
</CustomScrollbar>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
SelectMenu.displayName = 'SelectMenu';
|
||||||
|
|
||||||
|
interface SelectMenuOptionProps<T> {
|
||||||
|
isDisabled: boolean;
|
||||||
|
isFocused: boolean;
|
||||||
|
isSelected: boolean;
|
||||||
|
innerProps: any;
|
||||||
|
renderOptionLabel?: (value: SelectableValue<T>) => JSX.Element;
|
||||||
|
data: SelectableValue<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectMenuOptions = React.forwardRef<HTMLDivElement, React.PropsWithChildren<SelectMenuOptionProps<any>>>(
|
||||||
|
(props, ref) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getSelectStyles(theme);
|
||||||
|
const { children, innerProps, data, renderOptionLabel, isSelected, isFocused } = props;
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
className={cx(styles.option, isFocused && styles.optionFocused)}
|
||||||
|
{...innerProps}
|
||||||
|
aria-label="Select option"
|
||||||
|
>
|
||||||
|
<span>{renderOptionLabel ? renderOptionLabel(data) : children}</span>
|
||||||
|
{isSelected && (
|
||||||
|
<span>
|
||||||
|
<Icon name="check" />
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
);
|
@ -0,0 +1,95 @@
|
|||||||
|
import React, { PureComponent } from 'react';
|
||||||
|
import { css, cx } from 'emotion';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { GroupProps } from 'react-select/lib/components/Group';
|
||||||
|
import { stylesFactory, withTheme, selectThemeVariant } from '../../../themes';
|
||||||
|
import { Themeable } from '../../../types';
|
||||||
|
|
||||||
|
interface ExtendedGroupProps extends GroupProps<any>, Themeable {
|
||||||
|
data: {
|
||||||
|
label: string;
|
||||||
|
expanded: boolean;
|
||||||
|
options: any[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
expanded: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSelectOptionGroupStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
const optionBorder = selectThemeVariant(
|
||||||
|
{
|
||||||
|
light: theme.colors.gray4,
|
||||||
|
dark: theme.colors.dark9,
|
||||||
|
},
|
||||||
|
theme.type
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
header: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
justify-items: center;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 7px 10px;
|
||||||
|
width: 100%;
|
||||||
|
border-bottom: 1px solid ${optionBorder};
|
||||||
|
text-transform: capitalize;
|
||||||
|
`,
|
||||||
|
label: css`
|
||||||
|
flex-grow: 1;
|
||||||
|
`,
|
||||||
|
icon: css`
|
||||||
|
padding-right: 2px;
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
class UnthemedSelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
||||||
|
state = {
|
||||||
|
expanded: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
if (this.props.data.expanded) {
|
||||||
|
this.setState({ expanded: true });
|
||||||
|
} else if (this.props.selectProps && this.props.selectProps.value) {
|
||||||
|
const { value } = this.props.selectProps.value;
|
||||||
|
|
||||||
|
if (value && this.props.options.some(option => option.value === value)) {
|
||||||
|
this.setState({ expanded: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(nextProps: ExtendedGroupProps) {
|
||||||
|
if (nextProps.selectProps.inputValue !== '') {
|
||||||
|
this.setState({ expanded: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onToggleChildren = () => {
|
||||||
|
this.setState(prevState => ({
|
||||||
|
expanded: !prevState.expanded,
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { children, label, theme } = this.props;
|
||||||
|
const { expanded } = this.state;
|
||||||
|
const styles = getSelectOptionGroupStyles(theme);
|
||||||
|
const icon = expanded ? 'fa-caret-left' : 'fa-caret-down';
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className={styles.header} onClick={this.onToggleChildren}>
|
||||||
|
<span className={styles.label}>{label}</span>
|
||||||
|
<i className={cx('fa', icon, styles.icon)} />{' '}
|
||||||
|
</div>
|
||||||
|
{expanded && children}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SelectOptionGroup = withTheme(UnthemedSelectOptionGroup);
|
@ -1,16 +1,27 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { css, cx } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
|
||||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
import { components } from '@torkelo/react-select';
|
import { components } from '@torkelo/react-select';
|
||||||
import { useDelayedSwitch } from '../../utils/useDelayedSwitch';
|
import { useDelayedSwitch } from '../../../utils/useDelayedSwitch';
|
||||||
import { stylesFactory } from '../../themes';
|
import { stylesFactory, useTheme } from '../../../themes';
|
||||||
import { SlideOutTransition } from '../transitions/SlideOutTransition';
|
import { SlideOutTransition } from '../../transitions/SlideOutTransition';
|
||||||
import { FadeTransition } from '../transitions/FadeTransition';
|
import { FadeTransition } from '../../transitions/FadeTransition';
|
||||||
import { Spinner } from '../Spinner/Spinner';
|
import { Spinner } from '../../Spinner/Spinner';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
const getStyles = stylesFactory(() => {
|
const getStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
const singleValue = css`
|
||||||
|
label: singleValue;
|
||||||
|
color: ${theme.colors.formInputText};
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 100%;
|
||||||
|
/* padding-right: 40px; */
|
||||||
|
`;
|
||||||
const container = css`
|
const container = css`
|
||||||
width: 16px;
|
width: 16px;
|
||||||
height: 16px;
|
height: 16px;
|
||||||
@ -27,7 +38,7 @@ const getStyles = stylesFactory(() => {
|
|||||||
position: absolute;
|
position: absolute;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
return { container, item };
|
return { singleValue, container, item };
|
||||||
});
|
});
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@ -41,13 +52,14 @@ type Props = {
|
|||||||
|
|
||||||
export const SingleValue = (props: Props) => {
|
export const SingleValue = (props: Props) => {
|
||||||
const { children, data } = props;
|
const { children, data } = props;
|
||||||
const styles = getStyles();
|
const theme = useTheme();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
const loading = useDelayedSwitch(data.loading || false, { delay: 250, duration: 750 });
|
const loading = useDelayedSwitch(data.loading || false, { delay: 250, duration: 750 });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<components.SingleValue {...props}>
|
<components.SingleValue {...props}>
|
||||||
<div className={cx('gf-form-select-box__img-value')}>
|
<div className={styles.singleValue}>
|
||||||
{data.imgUrl ? (
|
{data.imgUrl ? (
|
||||||
<FadeWithImage loading={loading} imgUrl={data.imgUrl} />
|
<FadeWithImage loading={loading} imgUrl={data.imgUrl} />
|
||||||
) : (
|
) : (
|
||||||
@ -64,7 +76,8 @@ export const SingleValue = (props: Props) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const FadeWithImage = (props: { loading: boolean; imgUrl: string }) => {
|
const FadeWithImage = (props: { loading: boolean; imgUrl: string }) => {
|
||||||
const styles = getStyles();
|
const theme = useTheme();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
@ -0,0 +1,11 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cx } from 'emotion';
|
||||||
|
import { useTheme } from '../../../themes/ThemeContext';
|
||||||
|
import { getSelectStyles } from './getSelectStyles';
|
||||||
|
|
||||||
|
export const ValueContainer = (props: any) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getSelectStyles(theme);
|
||||||
|
const { children, isMulti } = props;
|
||||||
|
return <div className={cx(styles.valueContainer, isMulti && styles.valueContainerMulti)}>{children}</div>;
|
||||||
|
};
|
@ -0,0 +1,81 @@
|
|||||||
|
import { stylesFactory } from '../../../themes/stylesFactory';
|
||||||
|
import { selectThemeVariant as stv } from '../../../themes/selectThemeVariant';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
|
||||||
|
export const getSelectStyles = stylesFactory((theme: GrafanaTheme) => {
|
||||||
|
const bgColor = stv({ light: theme.colors.white, dark: theme.colors.gray15 }, theme.type);
|
||||||
|
const menuShadowColor = stv({ light: theme.colors.gray4, dark: theme.colors.black }, theme.type);
|
||||||
|
const optionBgHover = stv({ light: theme.colors.gray7, dark: theme.colors.gray10 }, theme.type);
|
||||||
|
const multiValueContainerBg = stv({ light: theme.colors.gray6, dark: theme.colors.gray05 }, theme.type);
|
||||||
|
const multiValueColor = stv({ light: theme.colors.gray25, dark: theme.colors.gray85 }, theme.type);
|
||||||
|
|
||||||
|
return {
|
||||||
|
menu: css`
|
||||||
|
background: ${bgColor};
|
||||||
|
box-shadow: 0px 4px 4px ${menuShadowColor};
|
||||||
|
position: absolute;
|
||||||
|
min-width: 100%;
|
||||||
|
`,
|
||||||
|
option: css`
|
||||||
|
padding: 8px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
flex-direction: row;
|
||||||
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
|
&:hover {
|
||||||
|
background: ${optionBgHover};
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
optionFocused: css`
|
||||||
|
background: ${optionBgHover};
|
||||||
|
border-image: linear-gradient(#f05a28 30%, #fbca0a 99%);
|
||||||
|
border-image-slice: 1;
|
||||||
|
border-style: solid;
|
||||||
|
border-top: 0;
|
||||||
|
border-right: 0;
|
||||||
|
border-bottom: 0;
|
||||||
|
border-left-width: 2px;
|
||||||
|
`,
|
||||||
|
singleValue: css`
|
||||||
|
color: ${theme.colors.formInputText};
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
box-sizing: border-box;
|
||||||
|
max-width: 100%;
|
||||||
|
`,
|
||||||
|
valueContainer: css`
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
box-sizing: border-box;
|
||||||
|
flex: 1 1 0%;
|
||||||
|
outline: none;
|
||||||
|
overflow: hidden;
|
||||||
|
`,
|
||||||
|
valueContainerMulti: css`
|
||||||
|
flex-wrap: wrap;
|
||||||
|
`,
|
||||||
|
loadingMessage: css`
|
||||||
|
padding: ${theme.spacing.sm};
|
||||||
|
text-align: center;
|
||||||
|
width: 100%;
|
||||||
|
`,
|
||||||
|
multiValueContainer: css`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
line-height: 1;
|
||||||
|
background: ${multiValueContainerBg};
|
||||||
|
border-radius: ${theme.border.radius.sm};
|
||||||
|
padding: ${theme.spacing.xs} ${theme.spacing.xxs} ${theme.spacing.xs} ${theme.spacing.sm};
|
||||||
|
margin: ${theme.spacing.xxs} ${theme.spacing.xs} ${theme.spacing.xxs} 0;
|
||||||
|
color: ${multiValueColor};
|
||||||
|
`,
|
||||||
|
multiValueRemove: css`
|
||||||
|
margin-left: ${theme.spacing.xs};
|
||||||
|
`,
|
||||||
|
};
|
||||||
|
});
|
@ -25,9 +25,10 @@ export const sharedInputStyle = (theme: GrafanaTheme, invalid = false) => {
|
|||||||
font-size: ${theme.typography.size.md};
|
font-size: ${theme.typography.size.md};
|
||||||
color: ${colors.formInputText};
|
color: ${colors.formInputText};
|
||||||
border: 1px solid ${borderColor};
|
border: 1px solid ${borderColor};
|
||||||
|
padding: 0 ${theme.spacing.sm} 0 ${theme.spacing.sm};
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: ${colors.formInputBorder};
|
border-color: ${borderColor};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
@ -53,7 +54,7 @@ export const inputSizes = () => {
|
|||||||
width: 580px;
|
width: 580px;
|
||||||
`,
|
`,
|
||||||
auto: css`
|
auto: css`
|
||||||
width: 100%;
|
width: auto;
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { getFormStyles } from './getFormStyles';
|
import { getFormStyles } from './getFormStyles';
|
||||||
import { Label } from './Label';
|
import { Label } from './Label';
|
||||||
import { Input } from './Input/Input';
|
import { Input } from './Input/Input';
|
||||||
|
import { Select } from './Select/Select';
|
||||||
import { Form } from './Form';
|
import { Form } from './Form';
|
||||||
import { Field } from './Field';
|
import { Field } from './Field';
|
||||||
import { Button } from './Button';
|
import { Button } from './Button';
|
||||||
@ -12,6 +13,7 @@ const Forms = {
|
|||||||
Form,
|
Form,
|
||||||
Field,
|
Field,
|
||||||
Button,
|
Button,
|
||||||
|
Select,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Forms;
|
export default Forms;
|
||||||
|
@ -6,6 +6,8 @@ import { IconType } from './types';
|
|||||||
export interface IconProps {
|
export interface IconProps {
|
||||||
name: IconType;
|
name: IconType;
|
||||||
className?: string;
|
className?: string;
|
||||||
|
onClick?: () => void;
|
||||||
|
onMouseDown?: React.MouseEventHandler;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getIconStyles = stylesFactory(() => {
|
const getIconStyles = stylesFactory(() => {
|
||||||
@ -23,9 +25,9 @@ const getIconStyles = stylesFactory(() => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
export const Icon: React.FC<IconProps> = ({ name, className }) => {
|
export const Icon: React.FC<IconProps> = ({ name, className, onClick, onMouseDown }) => {
|
||||||
const styles = getIconStyles();
|
const styles = getIconStyles();
|
||||||
return <i className={cx(styles.icon, 'fa', `fa-${name}`, className)} />;
|
return <i className={cx(styles.icon, 'fa', `fa-${name}`, className)} onClick={onClick} onMouseDown={onMouseDown} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
Icon.displayName = 'Icon';
|
Icon.displayName = 'Icon';
|
||||||
|
@ -24,7 +24,7 @@ SelectStories.add('default', () => {
|
|||||||
{(value, updateValue) => {
|
{(value, updateValue) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
value={value}
|
placeholder="Choose..."
|
||||||
options={options}
|
options={options}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
action('onChanged fired')(value);
|
action('onChanged fired')(value);
|
||||||
@ -39,7 +39,8 @@ SelectStories.add('default', () => {
|
|||||||
|
|
||||||
SelectStories.add('With allowCustomValue', () => {
|
SelectStories.add('With allowCustomValue', () => {
|
||||||
const intialState: SelectableValue<string> = { label: 'A label', value: 'A value' };
|
const intialState: SelectableValue<string> = { label: 'A label', value: 'A value' };
|
||||||
const value = object<SelectableValue<string>>('Selected Value:', intialState);
|
// @ts-ignore
|
||||||
|
const value = object<SelectableValue<string>>('Selected Value:', null);
|
||||||
const options = object<Array<SelectableValue<string>>>('Options:', [
|
const options = object<Array<SelectableValue<string>>>('Options:', [
|
||||||
intialState,
|
intialState,
|
||||||
{ label: 'Another label', value: 'Another value' },
|
{ label: 'Another label', value: 'Another value' },
|
||||||
@ -50,7 +51,8 @@ SelectStories.add('With allowCustomValue', () => {
|
|||||||
{(value, updateValue) => {
|
{(value, updateValue) => {
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
value={value}
|
// value={value}
|
||||||
|
placeholder="Choose..."
|
||||||
options={options}
|
options={options}
|
||||||
allowCustomValue={true}
|
allowCustomValue={true}
|
||||||
onChange={value => {
|
onChange={value => {
|
||||||
|
@ -14,54 +14,37 @@ import { components } from '@torkelo/react-select';
|
|||||||
|
|
||||||
// Components
|
// Components
|
||||||
import { SelectOption } from './SelectOption';
|
import { SelectOption } from './SelectOption';
|
||||||
import { SingleValue } from './SingleValue';
|
import { SelectOptionGroup } from '../Forms/Select/SelectOptionGroup';
|
||||||
import SelectOptionGroup from './SelectOptionGroup';
|
import { SingleValue } from '../Forms/Select/SingleValue';
|
||||||
|
import { SelectCommonProps, SelectAsyncProps } from '../Forms/Select/SelectBase';
|
||||||
import IndicatorsContainer from './IndicatorsContainer';
|
import IndicatorsContainer from './IndicatorsContainer';
|
||||||
import NoOptionsMessage from './NoOptionsMessage';
|
import NoOptionsMessage from './NoOptionsMessage';
|
||||||
import resetSelectStyles from './resetSelectStyles';
|
import resetSelectStyles from '../Forms/Select/resetSelectStyles';
|
||||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar';
|
||||||
import { PopoverContent } from '../Tooltip/Tooltip';
|
import { PopoverContent } from '../Tooltip/Tooltip';
|
||||||
import { Tooltip } from '../Tooltip/Tooltip';
|
import { Tooltip } from '../Tooltip/Tooltip';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
|
|
||||||
export interface CommonProps<T> {
|
/**
|
||||||
defaultValue?: any;
|
* Changes in new selects:
|
||||||
getOptionLabel?: (item: SelectableValue<T>) => string;
|
* - noOptionsMessage & loadingMessage is of string type
|
||||||
getOptionValue?: (item: SelectableValue<T>) => string;
|
* - isDisabled is renamed to disabled
|
||||||
onChange: (item: SelectableValue<T>) => {} | void;
|
*/
|
||||||
placeholder?: string;
|
type LegacyCommonProps<T> = Omit<SelectCommonProps<T>, 'noOptionsMessage' | 'disabled' | 'value'>;
|
||||||
width?: number;
|
|
||||||
value?: SelectableValue<T>;
|
|
||||||
className?: string;
|
|
||||||
isDisabled?: boolean;
|
|
||||||
isSearchable?: boolean;
|
|
||||||
isClearable?: boolean;
|
|
||||||
autoFocus?: boolean;
|
|
||||||
openMenuOnFocus?: boolean;
|
|
||||||
onBlur?: () => void;
|
|
||||||
maxMenuHeight?: number;
|
|
||||||
isLoading?: boolean;
|
|
||||||
noOptionsMessage?: () => string;
|
|
||||||
isMulti?: boolean;
|
|
||||||
backspaceRemovesValue?: boolean;
|
|
||||||
isOpen?: boolean;
|
|
||||||
components?: any;
|
|
||||||
tooltipContent?: PopoverContent;
|
|
||||||
onOpenMenu?: () => void;
|
|
||||||
onCloseMenu?: () => void;
|
|
||||||
tabSelectsValue?: boolean;
|
|
||||||
formatCreateLabel?: (input: string) => string;
|
|
||||||
allowCustomValue: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SelectProps<T> extends CommonProps<T> {
|
interface AsyncProps<T> extends LegacyCommonProps<T>, Omit<SelectAsyncProps<T>, 'loadingMessage'> {
|
||||||
options: Array<SelectableValue<T>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AsyncProps<T> extends CommonProps<T> {
|
|
||||||
defaultOptions: boolean;
|
|
||||||
loadOptions: (query: string) => Promise<Array<SelectableValue<T>>>;
|
|
||||||
loadingMessage?: () => string;
|
loadingMessage?: () => string;
|
||||||
|
noOptionsMessage?: () => string;
|
||||||
|
tooltipContent?: PopoverContent;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
value?: SelectableValue<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LegacySelectProps<T> extends LegacyCommonProps<T> {
|
||||||
|
tooltipContent?: PopoverContent;
|
||||||
|
noOptionsMessage?: () => string;
|
||||||
|
isDisabled?: boolean;
|
||||||
|
value?: SelectableValue<T>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MenuList = (props: any) => {
|
export const MenuList = (props: any) => {
|
||||||
@ -73,9 +56,8 @@ export const MenuList = (props: any) => {
|
|||||||
</components.MenuList>
|
</components.MenuList>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
export class Select<T> extends PureComponent<LegacySelectProps<T>> {
|
||||||
export class Select<T> extends PureComponent<SelectProps<T>> {
|
static defaultProps: Partial<LegacySelectProps<any>> = {
|
||||||
static defaultProps: Partial<SelectProps<any>> = {
|
|
||||||
className: '',
|
className: '',
|
||||||
isDisabled: false,
|
isDisabled: false,
|
||||||
isSearchable: true,
|
isSearchable: true,
|
||||||
@ -144,7 +126,6 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
|||||||
|
|
||||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className);
|
||||||
const selectComponents = { ...Select.defaultProps.components, ...components };
|
const selectComponents = { ...Select.defaultProps.components, ...components };
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
|
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}>
|
||||||
{(onOpenMenuInternal, onCloseMenuInternal) => {
|
{(onOpenMenuInternal, onCloseMenuInternal) => {
|
||||||
@ -170,7 +151,7 @@ export class Select<T> extends PureComponent<SelectProps<T>> {
|
|||||||
onBlur={onBlur}
|
onBlur={onBlur}
|
||||||
openMenuOnFocus={openMenuOnFocus}
|
openMenuOnFocus={openMenuOnFocus}
|
||||||
maxMenuHeight={maxMenuHeight}
|
maxMenuHeight={maxMenuHeight}
|
||||||
noOptionsMessage={noOptionsMessage}
|
noOptionsMessage={() => noOptionsMessage}
|
||||||
isMulti={isMulti}
|
isMulti={isMulti}
|
||||||
backspaceRemovesValue={backspaceRemovesValue}
|
backspaceRemovesValue={backspaceRemovesValue}
|
||||||
menuIsOpen={isOpen}
|
menuIsOpen={isOpen}
|
||||||
@ -262,7 +243,7 @@ export class AsyncSelect<T> extends PureComponent<AsyncProps<T>> {
|
|||||||
defaultOptions={defaultOptions}
|
defaultOptions={defaultOptions}
|
||||||
placeholder={placeholder || 'Choose'}
|
placeholder={placeholder || 'Choose'}
|
||||||
styles={resetSelectStyles()}
|
styles={resetSelectStyles()}
|
||||||
loadingMessage={loadingMessage}
|
loadingMessage={() => loadingMessage}
|
||||||
noOptionsMessage={noOptionsMessage}
|
noOptionsMessage={noOptionsMessage}
|
||||||
isDisabled={isDisabled}
|
isDisabled={isDisabled}
|
||||||
isSearchable={isSearchable}
|
isSearchable={isSearchable}
|
||||||
|
@ -1,59 +0,0 @@
|
|||||||
import React, { PureComponent } from 'react';
|
|
||||||
import { GroupProps } from 'react-select/lib/components/Group';
|
|
||||||
|
|
||||||
interface ExtendedGroupProps extends GroupProps<any> {
|
|
||||||
data: {
|
|
||||||
label: string;
|
|
||||||
expanded: boolean;
|
|
||||||
options: any[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface State {
|
|
||||||
expanded: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default class SelectOptionGroup extends PureComponent<ExtendedGroupProps, State> {
|
|
||||||
state = {
|
|
||||||
expanded: false,
|
|
||||||
};
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
if (this.props.data.expanded) {
|
|
||||||
this.setState({ expanded: true });
|
|
||||||
} else if (this.props.selectProps && this.props.selectProps.value) {
|
|
||||||
const { value } = this.props.selectProps.value;
|
|
||||||
|
|
||||||
if (value && this.props.options.some(option => option.value === value)) {
|
|
||||||
this.setState({ expanded: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(nextProps: ExtendedGroupProps) {
|
|
||||||
if (nextProps.selectProps.inputValue !== '') {
|
|
||||||
this.setState({ expanded: true });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onToggleChildren = () => {
|
|
||||||
this.setState(prevState => ({
|
|
||||||
expanded: !prevState.expanded,
|
|
||||||
}));
|
|
||||||
};
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { children, label } = this.props;
|
|
||||||
const { expanded } = this.state;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="gf-form-select-box__option-group">
|
|
||||||
<div className="gf-form-select-box__option-group__header" onClick={this.onToggleChildren}>
|
|
||||||
<span className="flex-grow-1">{label}</span>
|
|
||||||
<i className={`fa ${expanded ? 'fa-caret-left' : 'fa-caret-down'}`} />{' '}
|
|
||||||
</div>
|
|
||||||
{expanded && children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -12,7 +12,7 @@ export * from './Button/Button';
|
|||||||
export { Select, AsyncSelect } from './Select/Select';
|
export { Select, AsyncSelect } from './Select/Select';
|
||||||
export { IndicatorsContainer } from './Select/IndicatorsContainer';
|
export { IndicatorsContainer } from './Select/IndicatorsContainer';
|
||||||
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
export { NoOptionsMessage } from './Select/NoOptionsMessage';
|
||||||
export { default as resetSelectStyles } from './Select/resetSelectStyles';
|
export { default as resetSelectStyles } from './Forms/Select/resetSelectStyles';
|
||||||
export { ButtonSelect } from './Select/ButtonSelect';
|
export { ButtonSelect } from './Select/ButtonSelect';
|
||||||
export { Cascader, CascaderOption } from './Cascader/Cascader';
|
export { Cascader, CascaderOption } from './Cascader/Cascader';
|
||||||
|
|
||||||
|
16
packages/grafana-ui/src/utils/storybook/knobs.ts
Normal file
16
packages/grafana-ui/src/utils/storybook/knobs.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { select } from '@storybook/addon-knobs';
|
||||||
|
import { getAvailableIcons } from '../../components/Icon/types';
|
||||||
|
|
||||||
|
const VISUAL_GROUP = 'Visual options';
|
||||||
|
|
||||||
|
const iconOptions = {
|
||||||
|
None: undefined,
|
||||||
|
...getAvailableIcons().reduce<Record<string, string>>((prev, c) => {
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
[`Icon: ${c}`]: `${c}`,
|
||||||
|
};
|
||||||
|
}, {}),
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getIconKnob = () => select('Icon', iconOptions, undefined, VISUAL_GROUP);
|
@ -361,7 +361,7 @@ Array [
|
|||||||
className="css-0 gf-form-select-box__single-value"
|
className="css-0 gf-form-select-box__single-value"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className="gf-form-select-box__img-value"
|
className="css-38iae9-singleValue"
|
||||||
>
|
>
|
||||||
stackdriver auto
|
stackdriver auto
|
||||||
</div>
|
</div>
|
||||||
|
Loading…
Reference in New Issue
Block a user