mirror of https://github.com/grafana/grafana
Forms migration: Old Select to Legacy namespace (#23200)
* Export other components as Legacy * More Select Legacy * Add namespacing to more files * Export new elements * Move Legacy Select folder * Let's not forget the scss file * Move new Select folder * Move new Select from Forms namespace * Little oopsie * Fix errors * Fix merge issuespull/23278/head
parent
15bff3114f
commit
b34281e250
@ -0,0 +1,97 @@ |
||||
import React, { PureComponent, ReactElement } from 'react'; |
||||
import Select from './Select'; |
||||
import { PopoverContent } from '../../../Tooltip/Tooltip'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
|
||||
interface ButtonComponentProps { |
||||
label: ReactElement | string | undefined; |
||||
className: string | undefined; |
||||
iconClass?: string; |
||||
} |
||||
|
||||
const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => { |
||||
const { label, className, iconClass } = buttonProps; |
||||
|
||||
return ( |
||||
<div // changed to div because of FireFox on MacOs issue below |
||||
ref={props.innerRef} |
||||
className={`btn navbar-button navbar-button--tight ${className}`} |
||||
onClick={props.selectProps.menuIsOpen ? props.selectProps.onMenuClose : props.selectProps.onMenuOpen} |
||||
onBlur={props.selectProps.onMenuClose} |
||||
tabIndex={0} // necessary to get onBlur to work https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
|
||||
> |
||||
<div className="select-button"> |
||||
{iconClass && <i className={`select-button-icon ${iconClass}`} />} |
||||
<span className="select-button-value">{label ? label : ''}</span> |
||||
{!props.menuIsOpen && <i className="fa fa-caret-down fa-fw" />} |
||||
{props.menuIsOpen && <i className="fa fa-caret-up fa-fw" />} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export interface Props<T> { |
||||
className: string | undefined; |
||||
options: Array<SelectableValue<T>>; |
||||
value?: SelectableValue<T>; |
||||
label?: ReactElement | string; |
||||
iconClass?: string; |
||||
components?: any; |
||||
maxMenuHeight?: number; |
||||
onChange: (item: SelectableValue<T>) => void; |
||||
tooltipContent?: PopoverContent; |
||||
isMenuOpen?: boolean; |
||||
onOpenMenu?: () => void; |
||||
onCloseMenu?: () => void; |
||||
tabSelectsValue?: boolean; |
||||
autoFocus?: boolean; |
||||
} |
||||
|
||||
export class ButtonSelect<T> extends PureComponent<Props<T>> { |
||||
onChange = (item: SelectableValue<T>) => { |
||||
const { onChange } = this.props; |
||||
onChange(item); |
||||
}; |
||||
|
||||
render() { |
||||
const { |
||||
className, |
||||
options, |
||||
value, |
||||
label, |
||||
iconClass, |
||||
components, |
||||
maxMenuHeight, |
||||
tooltipContent, |
||||
isMenuOpen, |
||||
onOpenMenu, |
||||
onCloseMenu, |
||||
tabSelectsValue, |
||||
autoFocus = true, |
||||
} = this.props; |
||||
const combinedComponents = { |
||||
...components, |
||||
Control: ButtonComponent({ label, className, iconClass }), |
||||
}; |
||||
|
||||
return ( |
||||
<Select |
||||
autoFocus={autoFocus} |
||||
backspaceRemovesValue={false} |
||||
isClearable={false} |
||||
isSearchable={false} |
||||
options={options} |
||||
onChange={this.onChange} |
||||
value={value} |
||||
isOpen={isMenuOpen} |
||||
onOpenMenu={onOpenMenu} |
||||
onCloseMenu={onCloseMenu} |
||||
maxMenuHeight={maxMenuHeight} |
||||
components={combinedComponents} |
||||
className="gf-form-select-box-button-select" |
||||
tooltipContent={tooltipContent} |
||||
tabSelectsValue={tabSelectsValue} |
||||
/> |
||||
); |
||||
} |
||||
} |
||||
@ -0,0 +1,18 @@ |
||||
import React from 'react'; |
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select'; |
||||
|
||||
export const IndicatorsContainer = (props: any) => { |
||||
const isOpen = props.selectProps.menuIsOpen; |
||||
return ( |
||||
<components.IndicatorsContainer {...props}> |
||||
<span |
||||
className={`gf-form-select-box__select-arrow ${isOpen ? `gf-form-select-box__select-arrow--reversed` : ''}`} |
||||
/> |
||||
</components.IndicatorsContainer> |
||||
); |
||||
}; |
||||
|
||||
export default IndicatorsContainer; |
||||
@ -0,0 +1,101 @@ |
||||
import React, { useState, useCallback } from 'react'; |
||||
import { action } from '@storybook/addon-actions'; |
||||
import { withKnobs, object } from '@storybook/addon-knobs'; |
||||
import { withCenteredStory } from '../../../../utils/storybook/withCenteredStory'; |
||||
import { UseState } from '../../../../utils/storybook/UseState'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { Select, AsyncSelect } from './Select'; |
||||
|
||||
export default { |
||||
title: 'General/Select/Select', |
||||
component: Select, |
||||
decorators: [withCenteredStory, withKnobs], |
||||
}; |
||||
|
||||
const intialState: SelectableValue<string> = { label: 'A label', value: 'A value' }; |
||||
|
||||
const options = object<Array<SelectableValue<string>>>('Options:', [ |
||||
intialState, |
||||
{ label: 'Another label', value: 'Another value 1' }, |
||||
{ label: 'Another label', value: 'Another value 2' }, |
||||
{ label: 'Another label', value: 'Another value 3' }, |
||||
{ label: 'Another label', value: 'Another value 4' }, |
||||
{ label: 'Another label', value: 'Another value 5' }, |
||||
{ label: 'Another label', value: 'Another value ' }, |
||||
]); |
||||
|
||||
export const basic = () => { |
||||
const value = object<SelectableValue<string>>('Selected Value:', intialState); |
||||
|
||||
return ( |
||||
<UseState initialState={value}> |
||||
{(value, updateValue) => { |
||||
return ( |
||||
<Select |
||||
placeholder="Choose..." |
||||
options={options} |
||||
width={20} |
||||
onChange={value => { |
||||
action('onChanged fired')(value); |
||||
updateValue(value); |
||||
}} |
||||
/> |
||||
); |
||||
}} |
||||
</UseState> |
||||
); |
||||
}; |
||||
|
||||
export const withAllowCustomValue = () => { |
||||
// @ts-ignore
|
||||
const value = object<SelectableValue<string>>('Selected Value:', null); |
||||
|
||||
return ( |
||||
<UseState initialState={value}> |
||||
{(value, updateValue) => { |
||||
return ( |
||||
<Select |
||||
// value={value}
|
||||
placeholder="Choose..." |
||||
options={options} |
||||
width={20} |
||||
allowCustomValue={true} |
||||
onChange={value => { |
||||
action('onChanged fired')(value); |
||||
updateValue(value); |
||||
}} |
||||
/> |
||||
); |
||||
}} |
||||
</UseState> |
||||
); |
||||
}; |
||||
|
||||
export const asyncSelect = () => { |
||||
const [isLoading, setIsLoading] = useState<boolean>(true); |
||||
const [value, setValue] = useState(); |
||||
const loadAsyncOptions = useCallback( |
||||
inputValue => { |
||||
return new Promise<Array<SelectableValue<string>>>(resolve => { |
||||
setTimeout(() => { |
||||
setIsLoading(false); |
||||
resolve(options.filter(option => option.label && option.label.includes(inputValue))); |
||||
}, 1000); |
||||
}); |
||||
}, |
||||
[value] |
||||
); |
||||
return ( |
||||
<AsyncSelect |
||||
value={value} |
||||
defaultOptions |
||||
width={20} |
||||
isLoading={isLoading} |
||||
loadOptions={loadAsyncOptions} |
||||
onChange={value => { |
||||
action('onChange')(value); |
||||
setValue(value); |
||||
}} |
||||
/> |
||||
); |
||||
}; |
||||
@ -0,0 +1,327 @@ |
||||
// Libraries
|
||||
import classNames from 'classnames'; |
||||
import React, { PureComponent } from 'react'; |
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { default as ReactSelect } from '@torkelo/react-select'; |
||||
// @ts-ignore
|
||||
import Creatable from '@torkelo/react-select/creatable'; |
||||
// @ts-ignore
|
||||
import { CreatableProps } from 'react-select'; |
||||
// @ts-ignore
|
||||
import { default as ReactAsyncSelect } from '@torkelo/react-select/async'; |
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select'; |
||||
|
||||
// Components
|
||||
import { SelectOption } from './SelectOption'; |
||||
import { SelectOptionGroup } from '../../../Select/SelectOptionGroup'; |
||||
import { SingleValue } from '../../../Select/SingleValue'; |
||||
import { SelectCommonProps, SelectAsyncProps } from '../../../Select/types'; |
||||
import IndicatorsContainer from './IndicatorsContainer'; |
||||
import NoOptionsMessage from './NoOptionsMessage'; |
||||
import resetSelectStyles from '../../../Select/resetSelectStyles'; |
||||
import { CustomScrollbar } from '../../../CustomScrollbar/CustomScrollbar'; |
||||
import { PopoverContent } from '../../../Tooltip/Tooltip'; |
||||
import { Tooltip } from '../../../Tooltip/Tooltip'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
|
||||
/** |
||||
* Changes in new selects: |
||||
* - noOptionsMessage & loadingMessage is of string type |
||||
* - isDisabled is renamed to disabled |
||||
*/ |
||||
type LegacyCommonProps<T> = Omit<SelectCommonProps<T>, 'noOptionsMessage' | 'disabled' | 'value'>; |
||||
|
||||
interface AsyncProps<T> extends LegacyCommonProps<T>, Omit<SelectAsyncProps<T>, 'loadingMessage'> { |
||||
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) => { |
||||
return ( |
||||
<components.MenuList {...props}> |
||||
<CustomScrollbar autoHide={false} autoHeightMax="inherit"> |
||||
{props.children} |
||||
</CustomScrollbar> |
||||
</components.MenuList> |
||||
); |
||||
}; |
||||
export class Select<T> extends PureComponent<LegacySelectProps<T>> { |
||||
static defaultProps: Partial<LegacySelectProps<any>> = { |
||||
className: '', |
||||
isDisabled: false, |
||||
isSearchable: true, |
||||
isClearable: false, |
||||
isMulti: false, |
||||
openMenuOnFocus: false, |
||||
autoFocus: false, |
||||
isLoading: false, |
||||
backspaceRemovesValue: true, |
||||
maxMenuHeight: 300, |
||||
tabSelectsValue: true, |
||||
allowCustomValue: false, |
||||
components: { |
||||
Option: SelectOption, |
||||
SingleValue, |
||||
IndicatorsContainer, |
||||
MenuList, |
||||
Group: SelectOptionGroup, |
||||
}, |
||||
}; |
||||
|
||||
render() { |
||||
const { |
||||
defaultValue, |
||||
getOptionLabel, |
||||
getOptionValue, |
||||
onChange, |
||||
options, |
||||
placeholder, |
||||
width, |
||||
value, |
||||
className, |
||||
isDisabled, |
||||
isLoading, |
||||
isSearchable, |
||||
isClearable, |
||||
backspaceRemovesValue, |
||||
isMulti, |
||||
autoFocus, |
||||
openMenuOnFocus, |
||||
onBlur, |
||||
maxMenuHeight, |
||||
noOptionsMessage, |
||||
isOpen, |
||||
components, |
||||
tooltipContent, |
||||
tabSelectsValue, |
||||
onCloseMenu, |
||||
onOpenMenu, |
||||
allowCustomValue, |
||||
formatCreateLabel, |
||||
} = this.props; |
||||
|
||||
let widthClass = ''; |
||||
if (width) { |
||||
widthClass = 'width-' + width; |
||||
} |
||||
|
||||
let SelectComponent: ReactSelect | Creatable = ReactSelect; |
||||
const creatableOptions: any = {}; |
||||
|
||||
if (allowCustomValue) { |
||||
SelectComponent = Creatable; |
||||
creatableOptions.formatCreateLabel = formatCreateLabel ?? ((input: string) => input); |
||||
} |
||||
|
||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className); |
||||
const selectComponents = { ...Select.defaultProps.components, ...components }; |
||||
return ( |
||||
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}> |
||||
{(onOpenMenuInternal, onCloseMenuInternal) => { |
||||
return ( |
||||
<SelectComponent |
||||
captureMenuScroll={false} |
||||
classNamePrefix="gf-form-select-box" |
||||
className={selectClassNames} |
||||
components={selectComponents} |
||||
defaultValue={defaultValue} |
||||
value={value} |
||||
getOptionLabel={getOptionLabel} |
||||
getOptionValue={getOptionValue} |
||||
menuShouldScrollIntoView={false} |
||||
isSearchable={isSearchable} |
||||
onChange={onChange} |
||||
options={options} |
||||
placeholder={placeholder || 'Choose'} |
||||
styles={resetSelectStyles()} |
||||
isDisabled={isDisabled} |
||||
isLoading={isLoading} |
||||
isClearable={isClearable} |
||||
autoFocus={autoFocus} |
||||
onBlur={onBlur} |
||||
openMenuOnFocus={openMenuOnFocus} |
||||
maxMenuHeight={maxMenuHeight} |
||||
noOptionsMessage={() => noOptionsMessage} |
||||
isMulti={isMulti} |
||||
backspaceRemovesValue={backspaceRemovesValue} |
||||
menuIsOpen={isOpen} |
||||
onMenuOpen={onOpenMenuInternal} |
||||
onMenuClose={onCloseMenuInternal} |
||||
tabSelectsValue={tabSelectsValue} |
||||
{...creatableOptions} |
||||
/> |
||||
); |
||||
}} |
||||
</WrapInTooltip> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export class AsyncSelect<T> extends PureComponent<AsyncProps<T>> { |
||||
static defaultProps: Partial<AsyncProps<any>> = { |
||||
className: '', |
||||
components: {}, |
||||
loadingMessage: () => 'Loading...', |
||||
isDisabled: false, |
||||
isClearable: false, |
||||
isMulti: false, |
||||
isSearchable: true, |
||||
backspaceRemovesValue: true, |
||||
autoFocus: false, |
||||
openMenuOnFocus: false, |
||||
maxMenuHeight: 300, |
||||
}; |
||||
|
||||
render() { |
||||
const { |
||||
defaultValue, |
||||
getOptionLabel, |
||||
getOptionValue, |
||||
onChange, |
||||
placeholder, |
||||
width, |
||||
value, |
||||
className, |
||||
loadOptions, |
||||
defaultOptions, |
||||
isLoading, |
||||
loadingMessage, |
||||
noOptionsMessage, |
||||
isDisabled, |
||||
isSearchable, |
||||
isClearable, |
||||
backspaceRemovesValue, |
||||
autoFocus, |
||||
onBlur, |
||||
openMenuOnFocus, |
||||
maxMenuHeight, |
||||
isMulti, |
||||
tooltipContent, |
||||
onCloseMenu, |
||||
onOpenMenu, |
||||
isOpen, |
||||
} = this.props; |
||||
|
||||
let widthClass = ''; |
||||
if (width) { |
||||
widthClass = 'width-' + width; |
||||
} |
||||
|
||||
const selectClassNames = classNames('gf-form-input', 'gf-form-input--form-dropdown', widthClass, className); |
||||
|
||||
return ( |
||||
<WrapInTooltip onCloseMenu={onCloseMenu} onOpenMenu={onOpenMenu} tooltipContent={tooltipContent} isOpen={isOpen}> |
||||
{(onOpenMenuInternal, onCloseMenuInternal) => { |
||||
return ( |
||||
<ReactAsyncSelect |
||||
captureMenuScroll={false} |
||||
classNamePrefix="gf-form-select-box" |
||||
className={selectClassNames} |
||||
components={{ |
||||
Option: SelectOption, |
||||
SingleValue, |
||||
IndicatorsContainer, |
||||
NoOptionsMessage, |
||||
}} |
||||
defaultValue={defaultValue} |
||||
value={value} |
||||
getOptionLabel={getOptionLabel} |
||||
getOptionValue={getOptionValue} |
||||
menuShouldScrollIntoView={false} |
||||
onChange={onChange} |
||||
loadOptions={loadOptions} |
||||
isLoading={isLoading} |
||||
defaultOptions={defaultOptions} |
||||
placeholder={placeholder || 'Choose'} |
||||
styles={resetSelectStyles()} |
||||
loadingMessage={() => loadingMessage} |
||||
noOptionsMessage={noOptionsMessage} |
||||
isDisabled={isDisabled} |
||||
isSearchable={isSearchable} |
||||
isClearable={isClearable} |
||||
autoFocus={autoFocus} |
||||
onBlur={onBlur} |
||||
openMenuOnFocus={openMenuOnFocus} |
||||
maxMenuHeight={maxMenuHeight} |
||||
isMulti={isMulti} |
||||
backspaceRemovesValue={backspaceRemovesValue} |
||||
/> |
||||
); |
||||
}} |
||||
</WrapInTooltip> |
||||
); |
||||
} |
||||
} |
||||
|
||||
export interface TooltipWrapperProps { |
||||
children: (onOpenMenu: () => void, onCloseMenu: () => void) => React.ReactNode; |
||||
onOpenMenu?: () => void; |
||||
onCloseMenu?: () => void; |
||||
isOpen?: boolean; |
||||
tooltipContent?: PopoverContent; |
||||
} |
||||
|
||||
export interface TooltipWrapperState { |
||||
isOpenInternal: boolean; |
||||
} |
||||
|
||||
export class WrapInTooltip extends PureComponent<TooltipWrapperProps, TooltipWrapperState> { |
||||
state: TooltipWrapperState = { |
||||
isOpenInternal: false, |
||||
}; |
||||
|
||||
onOpenMenu = () => { |
||||
const { onOpenMenu } = this.props; |
||||
if (onOpenMenu) { |
||||
onOpenMenu(); |
||||
} |
||||
this.setState({ isOpenInternal: true }); |
||||
}; |
||||
|
||||
onCloseMenu = () => { |
||||
const { onCloseMenu } = this.props; |
||||
if (onCloseMenu) { |
||||
onCloseMenu(); |
||||
} |
||||
this.setState({ isOpenInternal: false }); |
||||
}; |
||||
|
||||
render() { |
||||
const { children, isOpen, tooltipContent } = this.props; |
||||
const { isOpenInternal } = this.state; |
||||
|
||||
let showTooltip: boolean | undefined = undefined; |
||||
|
||||
if (isOpenInternal || isOpen) { |
||||
showTooltip = false; |
||||
} |
||||
|
||||
if (tooltipContent) { |
||||
return ( |
||||
<Tooltip show={showTooltip} content={tooltipContent} placement="bottom"> |
||||
<div> |
||||
{/* div needed for tooltip */} |
||||
{children(this.onOpenMenu, this.onCloseMenu)} |
||||
</div> |
||||
</Tooltip> |
||||
); |
||||
} else { |
||||
return <div>{children(this.onOpenMenu, this.onCloseMenu)}</div>; |
||||
} |
||||
} |
||||
} |
||||
|
||||
export default Select; |
||||
@ -1,89 +0,0 @@ |
||||
import React from 'react'; |
||||
import { css } from 'emotion'; |
||||
import { GrafanaTheme } from '@grafana/data'; |
||||
|
||||
import { Button, ButtonVariant, ButtonProps } from '../../Button'; |
||||
import { ComponentSize } from '../../../types/size'; |
||||
import { SelectCommonProps, CustomControlProps } from './types'; |
||||
import { SelectBase } from './SelectBase'; |
||||
import { stylesFactory, 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?: ComponentSize; |
||||
} |
||||
|
||||
interface SelectButtonProps extends Omit<ButtonProps, 'icon'> { |
||||
icon?: IconType; |
||||
isOpen?: boolean; |
||||
} |
||||
|
||||
const SelectButton = React.forwardRef<HTMLButtonElement, SelectButtonProps>( |
||||
({ icon, children, isOpen, ...buttonProps }, ref) => { |
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({ |
||||
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 styles = getStyles(useTheme()); |
||||
const buttonIcon = `fa fa-${icon}`; |
||||
const caretIcon = isOpen ? 'caret-up' : 'caret-down'; |
||||
return ( |
||||
<Button {...buttonProps} ref={ref} 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={React.forwardRef<any, CustomControlProps<T>>(({ onBlur, onClick, value, isOpen }, ref) => { |
||||
return ( |
||||
<SelectButton {...buttonProps} ref={ref} onBlur={onBlur} onClick={onClick} isOpen={isOpen}> |
||||
{value ? value.label : placeholder} |
||||
</SelectButton> |
||||
); |
||||
})} |
||||
/> |
||||
); |
||||
} |
||||
@ -1,24 +0,0 @@ |
||||
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; |
||||
` |
||||
)} |
||||
ref={ref} |
||||
> |
||||
{children} |
||||
</div> |
||||
); |
||||
}); |
||||
@ -1,300 +0,0 @@ |
||||
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'; |
||||
import { generateOptions } from './mockOptions'; |
||||
|
||||
export default { |
||||
title: 'Forms/Select', |
||||
component: Select, |
||||
decorators: [withCenteredStory, withHorizontallyCenteredStory], |
||||
}; |
||||
|
||||
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 SelectWithOptionDescriptions = () => { |
||||
// TODO this is not working with new Select
|
||||
|
||||
const [value, setValue] = useState<number>(); |
||||
const options = [ |
||||
{ label: 'Basic option', value: 0 }, |
||||
{ label: 'Option with description', value: 1, description: 'this is a description' }, |
||||
{ |
||||
label: 'Option with description and image', |
||||
value: 2, |
||||
description: 'This is a very elaborate description, describing all the wonders in the world.', |
||||
imgUrl: 'https://placekitten.com/40/40', |
||||
}, |
||||
]; |
||||
|
||||
return ( |
||||
<> |
||||
<Select |
||||
options={options} |
||||
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={React.forwardRef(({ isOpen, value, ...otherProps }, ref) => { |
||||
return ( |
||||
<Button {...otherProps} ref={ref}> |
||||
{' '} |
||||
{isOpen ? 'Open' : 'Closed'} |
||||
</Button> |
||||
); |
||||
})} |
||||
{...getDynamicProps()} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export const autoMenuPlacement = () => { |
||||
const [value, setValue] = useState<SelectableValue<string>>(); |
||||
|
||||
return ( |
||||
<> |
||||
<div style={{ height: '95vh', display: 'flex', alignItems: 'flex-end' }}> |
||||
<Select |
||||
options={generateOptions()} |
||||
value={value} |
||||
onChange={v => { |
||||
setValue(v); |
||||
}} |
||||
size="md" |
||||
{...getDynamicProps()} |
||||
/> |
||||
</div> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export const customValueCreation = () => { |
||||
const [value, setValue] = useState<SelectableValue<string>>(); |
||||
const [customOptions, setCustomOptions] = useState<Array<SelectableValue<string>>>([]); |
||||
const options = generateOptions(); |
||||
return ( |
||||
<> |
||||
<Select |
||||
options={[...options, ...customOptions]} |
||||
value={value} |
||||
onChange={v => { |
||||
setValue(v); |
||||
}} |
||||
size="md" |
||||
allowCustomValue |
||||
onCreateOption={v => { |
||||
const customValue: SelectableValue<string> = { value: kebabCase(v), label: v }; |
||||
setCustomOptions([...customOptions, customValue]); |
||||
setValue(customValue); |
||||
}} |
||||
{...getDynamicProps()} |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
@ -1,33 +0,0 @@ |
||||
import React from 'react'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { SelectCommonProps, MultiSelectCommonProps, SelectAsyncProps } from './types'; |
||||
import { SelectBase } 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>; |
||||
invalid?: boolean; |
||||
} |
||||
|
||||
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 />; |
||||
} |
||||
@ -1,11 +1,11 @@ |
||||
import React from 'react'; |
||||
import { FieldConfigEditorProps, SelectFieldConfigSettings } from '@grafana/data'; |
||||
import Forms from '../Forms'; |
||||
import { Select } from '../Select/Select'; |
||||
|
||||
export function SelectValueEditor<T>({ |
||||
value, |
||||
onChange, |
||||
item, |
||||
}: FieldConfigEditorProps<T, SelectFieldConfigSettings<T>>) { |
||||
return <Forms.Select<T> defaultValue={value} onChange={e => onChange(e.value)} options={item.settings?.options} />; |
||||
return <Select<T> defaultValue={value} onChange={e => onChange(e.value)} options={item.settings?.options} />; |
||||
} |
||||
|
||||
@ -1,97 +1,89 @@ |
||||
import React, { PureComponent, ReactElement } from 'react'; |
||||
import Select from './Select'; |
||||
import { PopoverContent } from '../Tooltip/Tooltip'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
import React from 'react'; |
||||
import { css } from 'emotion'; |
||||
import { GrafanaTheme } from '@grafana/data'; |
||||
|
||||
interface ButtonComponentProps { |
||||
label: ReactElement | string | undefined; |
||||
className: string | undefined; |
||||
iconClass?: string; |
||||
} |
||||
|
||||
const ButtonComponent = (buttonProps: ButtonComponentProps) => (props: any) => { |
||||
const { label, className, iconClass } = buttonProps; |
||||
import { Button, ButtonVariant, ButtonProps } from '../Button'; |
||||
import { ComponentSize } from '../../types/size'; |
||||
import { SelectCommonProps, CustomControlProps } from './types'; |
||||
import { SelectBase } from './SelectBase'; |
||||
import { stylesFactory, useTheme } from '../../themes'; |
||||
import { Icon } from '../Icon/Icon'; |
||||
import { IconType } from '../Icon/types'; |
||||
|
||||
return ( |
||||
<div // changed to div because of FireFox on MacOs issue below |
||||
ref={props.innerRef} |
||||
className={`btn navbar-button navbar-button--tight ${className}`} |
||||
onClick={props.selectProps.menuIsOpen ? props.selectProps.onMenuClose : props.selectProps.onMenuOpen} |
||||
onBlur={props.selectProps.onMenuClose} |
||||
tabIndex={0} // necessary to get onBlur to work https://developer.mozilla.org/en-US/docs/Web/HTML/Element/button#Clicking_and_focus
|
||||
> |
||||
<div className="select-button"> |
||||
{iconClass && <i className={`select-button-icon ${iconClass}`} />} |
||||
<span className="select-button-value">{label ? label : ''}</span> |
||||
{!props.menuIsOpen && <i className="fa fa-caret-down fa-fw" />} |
||||
{props.menuIsOpen && <i className="fa fa-caret-up fa-fw" />} |
||||
</div> |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export interface Props<T> { |
||||
className: string | undefined; |
||||
options: Array<SelectableValue<T>>; |
||||
value?: SelectableValue<T>; |
||||
label?: ReactElement | string; |
||||
iconClass?: string; |
||||
components?: any; |
||||
maxMenuHeight?: number; |
||||
onChange: (item: SelectableValue<T>) => void; |
||||
tooltipContent?: PopoverContent; |
||||
isMenuOpen?: boolean; |
||||
onOpenMenu?: () => void; |
||||
onCloseMenu?: () => void; |
||||
tabSelectsValue?: boolean; |
||||
autoFocus?: boolean; |
||||
interface ButtonSelectProps<T> extends Omit<SelectCommonProps<T>, 'renderControl' | 'size' | 'prefix'> { |
||||
icon?: IconType; |
||||
variant?: ButtonVariant; |
||||
size?: ComponentSize; |
||||
} |
||||
|
||||
export class ButtonSelect<T> extends PureComponent<Props<T>> { |
||||
onChange = (item: SelectableValue<T>) => { |
||||
const { onChange } = this.props; |
||||
onChange(item); |
||||
}; |
||||
|
||||
render() { |
||||
const { |
||||
className, |
||||
options, |
||||
value, |
||||
label, |
||||
iconClass, |
||||
components, |
||||
maxMenuHeight, |
||||
tooltipContent, |
||||
isMenuOpen, |
||||
onOpenMenu, |
||||
onCloseMenu, |
||||
tabSelectsValue, |
||||
autoFocus = true, |
||||
} = this.props; |
||||
const combinedComponents = { |
||||
...components, |
||||
Control: ButtonComponent({ label, className, iconClass }), |
||||
}; |
||||
interface SelectButtonProps extends Omit<ButtonProps, 'icon'> { |
||||
icon?: IconType; |
||||
isOpen?: boolean; |
||||
} |
||||
|
||||
const SelectButton = React.forwardRef<HTMLButtonElement, SelectButtonProps>( |
||||
({ icon, children, isOpen, ...buttonProps }, ref) => { |
||||
const getStyles = stylesFactory((theme: GrafanaTheme) => ({ |
||||
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 styles = getStyles(useTheme()); |
||||
const buttonIcon = `fa fa-${icon}`; |
||||
const caretIcon = isOpen ? 'caret-up' : 'caret-down'; |
||||
return ( |
||||
<Select |
||||
autoFocus={autoFocus} |
||||
backspaceRemovesValue={false} |
||||
isClearable={false} |
||||
isSearchable={false} |
||||
options={options} |
||||
onChange={this.onChange} |
||||
value={value} |
||||
isOpen={isMenuOpen} |
||||
onOpenMenu={onOpenMenu} |
||||
onCloseMenu={onCloseMenu} |
||||
maxMenuHeight={maxMenuHeight} |
||||
components={combinedComponents} |
||||
className="gf-form-select-box-button-select" |
||||
tooltipContent={tooltipContent} |
||||
tabSelectsValue={tabSelectsValue} |
||||
/> |
||||
<Button {...buttonProps} ref={ref} 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={React.forwardRef<any, CustomControlProps<T>>(({ onBlur, onClick, value, isOpen }, ref) => { |
||||
return ( |
||||
<SelectButton {...buttonProps} ref={ref} onBlur={onBlur} onClick={onClick} isOpen={isOpen}> |
||||
{value ? value.label : placeholder} |
||||
</SelectButton> |
||||
); |
||||
})} |
||||
/> |
||||
); |
||||
} |
||||
|
||||
@ -1,5 +1,5 @@ |
||||
import React from 'react'; |
||||
import { Icon } from '../../Icon/Icon'; |
||||
import { Icon } from '../Icon/Icon'; |
||||
|
||||
interface DropdownIndicatorProps { |
||||
isOpen: boolean; |
||||
@ -1,18 +1,24 @@ |
||||
import React from 'react'; |
||||
import { useTheme } from '../../themes/ThemeContext'; |
||||
import { getInputStyles } from '../Forms/Input/Input'; |
||||
import { cx, css } from 'emotion'; |
||||
|
||||
// Ignoring because I couldn't get @types/react-select work wih Torkel's fork
|
||||
// @ts-ignore
|
||||
import { components } from '@torkelo/react-select'; |
||||
export const IndicatorsContainer = React.forwardRef<HTMLDivElement, React.PropsWithChildren<any>>((props, ref) => { |
||||
const { children } = props; |
||||
const theme = useTheme(); |
||||
const styles = getInputStyles({ theme, invalid: false }); |
||||
|
||||
export const IndicatorsContainer = (props: any) => { |
||||
const isOpen = props.selectProps.menuIsOpen; |
||||
return ( |
||||
<components.IndicatorsContainer {...props}> |
||||
<span |
||||
className={`gf-form-select-box__select-arrow ${isOpen ? `gf-form-select-box__select-arrow--reversed` : ''}`} |
||||
/> |
||||
</components.IndicatorsContainer> |
||||
<div |
||||
className={cx( |
||||
styles.suffix, |
||||
css` |
||||
position: relative; |
||||
` |
||||
)} |
||||
ref={ref} |
||||
> |
||||
{children} |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
export default IndicatorsContainer; |
||||
}); |
||||
|
||||
@ -1,9 +1,9 @@ |
||||
import React from 'react'; |
||||
import { useTheme } from '../../../themes/ThemeContext'; |
||||
import { getFocusCss, sharedInputStyle } from '../commonStyles'; |
||||
import { getInputStyles } from '../Input/Input'; |
||||
import { useTheme } from '../../themes/ThemeContext'; |
||||
import { getFocusCss, sharedInputStyle } from '../Forms/commonStyles'; |
||||
import { getInputStyles } from '../Forms/Input/Input'; |
||||
import { cx, css } from 'emotion'; |
||||
import { stylesFactory } from '../../../themes'; |
||||
import { stylesFactory } from '../../themes'; |
||||
import { GrafanaTheme } from '@grafana/data'; |
||||
|
||||
interface InputControlProps { |
||||
@ -1,7 +1,7 @@ |
||||
import React from 'react'; |
||||
import { useTheme } from '../../../themes'; |
||||
import { useTheme } from '../../themes'; |
||||
import { getSelectStyles } from './getSelectStyles'; |
||||
import { Icon } from '../../Icon/Icon'; |
||||
import { Icon } from '../Icon/Icon'; |
||||
|
||||
interface MultiValueContainerProps { |
||||
innerProps: any; |
||||
@ -1,101 +1,300 @@ |
||||
import React, { useState, useCallback } from 'react'; |
||||
import { action } from '@storybook/addon-actions'; |
||||
import { withKnobs, object } from '@storybook/addon-knobs'; |
||||
import { withCenteredStory } from '../../utils/storybook/withCenteredStory'; |
||||
import { UseState } from '../../utils/storybook/UseState'; |
||||
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 { Select, AsyncSelect } from './Select'; |
||||
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'; |
||||
import { generateOptions } from './mockOptions'; |
||||
|
||||
export default { |
||||
title: 'General/Select/Select', |
||||
title: 'Forms/Select', |
||||
component: Select, |
||||
decorators: [withCenteredStory, withKnobs], |
||||
decorators: [withCenteredStory, withHorizontallyCenteredStory], |
||||
}; |
||||
|
||||
const intialState: SelectableValue<string> = { label: 'A label', value: 'A value' }; |
||||
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); |
||||
|
||||
const options = object<Array<SelectableValue<string>>>('Options:', [ |
||||
intialState, |
||||
{ label: 'Another label', value: 'Another value 1' }, |
||||
{ label: 'Another label', value: 'Another value 2' }, |
||||
{ label: 'Another label', value: 'Another value 3' }, |
||||
{ label: 'Another label', value: 'Another value 4' }, |
||||
{ label: 'Another label', value: 'Another value 5' }, |
||||
{ label: 'Another label', value: 'Another value ' }, |
||||
]); |
||||
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 = object<SelectableValue<string>>('Selected Value:', intialState); |
||||
const [value, setValue] = useState<SelectableValue<string>>(); |
||||
|
||||
return ( |
||||
<UseState initialState={value}> |
||||
{(value, updateValue) => { |
||||
return ( |
||||
<Select |
||||
placeholder="Choose..." |
||||
options={options} |
||||
width={20} |
||||
onChange={value => { |
||||
action('onChanged fired')(value); |
||||
updateValue(value); |
||||
}} |
||||
/> |
||||
); |
||||
}} |
||||
</UseState> |
||||
<> |
||||
<Select |
||||
options={generateOptions()} |
||||
value={value} |
||||
onChange={v => { |
||||
setValue(v); |
||||
}} |
||||
size="md" |
||||
{...getDynamicProps()} |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export const withAllowCustomValue = () => { |
||||
// @ts-ignore
|
||||
const value = object<SelectableValue<string>>('Selected Value:', null); |
||||
|
||||
/** |
||||
* Uses plain values instead of SelectableValue<T> |
||||
*/ |
||||
export const basicSelectPlainValue = () => { |
||||
const [value, setValue] = useState<string>(); |
||||
return ( |
||||
<UseState initialState={value}> |
||||
{(value, updateValue) => { |
||||
return ( |
||||
<Select |
||||
// value={value}
|
||||
placeholder="Choose..." |
||||
options={options} |
||||
width={20} |
||||
allowCustomValue={true} |
||||
onChange={value => { |
||||
action('onChanged fired')(value); |
||||
updateValue(value); |
||||
}} |
||||
/> |
||||
); |
||||
}} |
||||
</UseState> |
||||
<> |
||||
<Select |
||||
options={generateOptions()} |
||||
value={value} |
||||
onChange={v => { |
||||
setValue(v.value); |
||||
}} |
||||
size="md" |
||||
{...getDynamicProps()} |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export const asyncSelect = () => { |
||||
const [isLoading, setIsLoading] = useState<boolean>(true); |
||||
const [value, setValue] = useState(); |
||||
const loadAsyncOptions = useCallback( |
||||
inputValue => { |
||||
return new Promise<Array<SelectableValue<string>>>(resolve => { |
||||
setTimeout(() => { |
||||
setIsLoading(false); |
||||
resolve(options.filter(option => option.label && option.label.includes(inputValue))); |
||||
}, 1000); |
||||
}); |
||||
/** |
||||
* Uses plain values instead of SelectableValue<T> |
||||
*/ |
||||
export const SelectWithOptionDescriptions = () => { |
||||
// TODO this is not working with new Select
|
||||
|
||||
const [value, setValue] = useState<number>(); |
||||
const options = [ |
||||
{ label: 'Basic option', value: 0 }, |
||||
{ label: 'Option with description', value: 1, description: 'this is a description' }, |
||||
{ |
||||
label: 'Option with description and image', |
||||
value: 2, |
||||
description: 'This is a very elaborate description, describing all the wonders in the world.', |
||||
imgUrl: 'https://placekitten.com/40/40', |
||||
}, |
||||
[value] |
||||
]; |
||||
|
||||
return ( |
||||
<> |
||||
<Select |
||||
options={options} |
||||
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 ( |
||||
<AsyncSelect |
||||
value={value} |
||||
<> |
||||
<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 |
||||
width={20} |
||||
isLoading={isLoading} |
||||
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} |
||||
onChange={value => { |
||||
action('onChange')(value); |
||||
setValue(value); |
||||
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={React.forwardRef(({ isOpen, value, ...otherProps }, ref) => { |
||||
return ( |
||||
<Button {...otherProps} ref={ref}> |
||||
{' '} |
||||
{isOpen ? 'Open' : 'Closed'} |
||||
</Button> |
||||
); |
||||
})} |
||||
{...getDynamicProps()} |
||||
/> |
||||
); |
||||
}; |
||||
|
||||
export const autoMenuPlacement = () => { |
||||
const [value, setValue] = useState<SelectableValue<string>>(); |
||||
|
||||
return ( |
||||
<> |
||||
<div style={{ height: '95vh', display: 'flex', alignItems: 'flex-end' }}> |
||||
<Select |
||||
options={generateOptions()} |
||||
value={value} |
||||
onChange={v => { |
||||
setValue(v); |
||||
}} |
||||
size="md" |
||||
{...getDynamicProps()} |
||||
/> |
||||
</div> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export const customValueCreation = () => { |
||||
const [value, setValue] = useState<SelectableValue<string>>(); |
||||
const [customOptions, setCustomOptions] = useState<Array<SelectableValue<string>>>([]); |
||||
const options = generateOptions(); |
||||
return ( |
||||
<> |
||||
<Select |
||||
options={[...options, ...customOptions]} |
||||
value={value} |
||||
onChange={v => { |
||||
setValue(v); |
||||
}} |
||||
size="md" |
||||
allowCustomValue |
||||
onCreateOption={v => { |
||||
const customValue: SelectableValue<string> = { value: kebabCase(v), label: v }; |
||||
setCustomOptions([...customOptions, customValue]); |
||||
setValue(customValue); |
||||
}} |
||||
{...getDynamicProps()} |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
@ -1,10 +1,10 @@ |
||||
import React from 'react'; |
||||
import { useTheme } from '../../../themes/ThemeContext'; |
||||
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'; |
||||
import { Icon } from '../Icon/Icon'; |
||||
import { CustomScrollbar } from '../CustomScrollbar/CustomScrollbar'; |
||||
|
||||
interface SelectMenuProps { |
||||
maxHeight: number; |
||||
@ -1,6 +1,6 @@ |
||||
import React from 'react'; |
||||
import { cx } from 'emotion'; |
||||
import { useTheme } from '../../../themes/ThemeContext'; |
||||
import { useTheme } from '../../themes/ThemeContext'; |
||||
import { getSelectStyles } from './getSelectStyles'; |
||||
|
||||
export const ValueContainer = (props: any) => { |
||||
@ -1,5 +1,5 @@ |
||||
import { stylesFactory } from '../../../themes/stylesFactory'; |
||||
import { selectThemeVariant as stv } from '../../../themes/selectThemeVariant'; |
||||
import { stylesFactory } from '../../themes/stylesFactory'; |
||||
import { selectThemeVariant as stv } from '../../themes/selectThemeVariant'; |
||||
import { css } from 'emotion'; |
||||
import { GrafanaTheme } from '@grafana/data'; |
||||
|
||||
Loading…
Reference in new issue