mirror of https://github.com/grafana/grafana
ButtonSelect & RefreshPicker: Rewrite of components to use new emotion based ToolbarButton & Menu (#30510)
* ButtonSelect: Trying to rewrite the button select to use ToggleButtonGroup & Menu * minor update * Progress * Updated * Moving all the explore scenarios into the refresh picker component * Minor fixes * Fixed responsive part of run button * More minor fixes * typescript fix * Update packages/grafana-ui/src/components/Icon/Icon.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Update packages/grafana-ui/src/components/Menu/Menu.tsx Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com> * Review feedback fixes and more * Fixes small ts issue * Updated return to dashboard button and tests, moved ButtonSelect out of LegacyForms * fixed ts issue * Fixed test Co-authored-by: Alex Khomenko <Clarity-89@users.noreply.github.com>pull/30572/head
parent
8c1a79f24b
commit
2a21f067b7
@ -0,0 +1,45 @@ |
||||
import React, { FC } 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 { ButtonSelect } from './ButtonSelect'; |
||||
import { DashboardStoryCanvas } from '../../utils/storybook/DashboardStoryCanvas'; |
||||
|
||||
export default { |
||||
title: 'Forms/Select/ButtonSelect', |
||||
component: ButtonSelect, |
||||
decorators: [withCenteredStory, withKnobs], |
||||
}; |
||||
|
||||
export const Basic: FC = () => { |
||||
const initialState: SelectableValue<string> = { label: 'A label', value: 'A value' }; |
||||
const value = object<SelectableValue<string>>('Selected Value:', initialState); |
||||
const options = object<Array<SelectableValue<string>>>('Options:', [ |
||||
initialState, |
||||
{ label: 'Another label', value: 'Another value' }, |
||||
]); |
||||
|
||||
return ( |
||||
<DashboardStoryCanvas> |
||||
<UseState initialState={value}> |
||||
{(value, updateValue) => { |
||||
return ( |
||||
<div style={{ marginLeft: '100px', position: 'relative', display: 'inline-block' }}> |
||||
<ButtonSelect |
||||
value={value} |
||||
options={options} |
||||
onChange={(value) => { |
||||
action('onChanged fired')(value); |
||||
updateValue(value as any); |
||||
}} |
||||
className="refresh-select" |
||||
/> |
||||
</div> |
||||
); |
||||
}} |
||||
</UseState> |
||||
</DashboardStoryCanvas> |
||||
); |
||||
}; |
@ -0,0 +1,91 @@ |
||||
import React, { useState, HTMLAttributes } from 'react'; |
||||
import { PopoverContent } from '../Tooltip/Tooltip'; |
||||
import { GrafanaTheme, SelectableValue } from '@grafana/data'; |
||||
import { ButtonVariant, ToolbarButton } from '../Button'; |
||||
import { ClickOutsideWrapper } from '../ClickOutsideWrapper/ClickOutsideWrapper'; |
||||
import { css } from 'emotion'; |
||||
import { useStyles } from '../../themes/ThemeContext'; |
||||
import { Menu, MenuItemsGroup } from '../Menu/Menu'; |
||||
|
||||
export interface Props<T> extends HTMLAttributes<HTMLButtonElement> { |
||||
className?: string; |
||||
options: Array<SelectableValue<T>>; |
||||
value?: SelectableValue<T>; |
||||
maxMenuHeight?: number; |
||||
onChange: (item: SelectableValue<T>) => void; |
||||
tooltipContent?: PopoverContent; |
||||
narrow?: boolean; |
||||
variant?: ButtonVariant; |
||||
} |
||||
|
||||
/** |
||||
* @internal |
||||
* A temporary component until we have a proper dropdown component |
||||
*/ |
||||
export const ButtonSelect = React.memo(<T,>(props: Props<T>) => { |
||||
const { className, options, value, onChange, narrow, variant, ...restProps } = props; |
||||
const [isOpen, setIsOpen] = useState<boolean>(false); |
||||
const styles = useStyles(getStyles); |
||||
|
||||
const onCloseMenu = () => { |
||||
setIsOpen(false); |
||||
}; |
||||
|
||||
const onToggle = (event: React.MouseEvent) => { |
||||
event.stopPropagation(); |
||||
event.preventDefault(); |
||||
setIsOpen(!isOpen); |
||||
}; |
||||
|
||||
const onChangeInternal = (item: SelectableValue<T>) => { |
||||
onChange(item); |
||||
setIsOpen(false); |
||||
}; |
||||
|
||||
const menuGroup: MenuItemsGroup = { |
||||
items: options.map((item) => ({ |
||||
label: (item.label || item.value) as string, |
||||
onClick: () => onChangeInternal(item), |
||||
active: item.value === value?.value, |
||||
})), |
||||
}; |
||||
|
||||
return ( |
||||
<> |
||||
<ToolbarButton |
||||
className={className} |
||||
isOpen={isOpen} |
||||
onClick={onToggle} |
||||
narrow={narrow} |
||||
variant={variant} |
||||
{...restProps} |
||||
> |
||||
{value?.label || value?.value} |
||||
</ToolbarButton> |
||||
{isOpen && ( |
||||
<div className={styles.menuWrapper}> |
||||
<ClickOutsideWrapper onClick={onCloseMenu} parent={document}> |
||||
<Menu items={[menuGroup]} /> |
||||
</ClickOutsideWrapper> |
||||
</div> |
||||
)} |
||||
</> |
||||
); |
||||
}); |
||||
|
||||
ButtonSelect.displayName = 'ButtonSelect'; |
||||
|
||||
const getStyles = (theme: GrafanaTheme) => { |
||||
return { |
||||
wrapper: css` |
||||
position: relative; |
||||
display: inline-flex; |
||||
`,
|
||||
menuWrapper: css` |
||||
position: absolute; |
||||
z-index: ${theme.zIndex.dropdown}; |
||||
top: ${theme.spacing.formButtonHeight + 1}px; |
||||
right: 0; |
||||
`,
|
||||
}; |
||||
}; |
@ -1,42 +0,0 @@ |
||||
import React from 'react'; |
||||
import { action } from '@storybook/addon-actions'; |
||||
import { withKnobs, object, text } from '@storybook/addon-knobs'; |
||||
import { withCenteredStory } from '../../../../utils/storybook/withCenteredStory'; |
||||
import { UseState } from '../../../../utils/storybook/UseState'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
import { ButtonSelect } from './ButtonSelect'; |
||||
|
||||
export default { |
||||
title: 'Forms/Select/ButtonSelect', |
||||
component: ButtonSelect, |
||||
decorators: [withCenteredStory, withKnobs], |
||||
}; |
||||
|
||||
export const basic = () => { |
||||
const initialState: SelectableValue<string> = { label: 'A label', value: 'A value' }; |
||||
const value = object<SelectableValue<string>>('Selected Value:', initialState); |
||||
const options = object<Array<SelectableValue<string>>>('Options:', [ |
||||
initialState, |
||||
{ label: 'Another label', value: 'Another value' }, |
||||
]); |
||||
|
||||
return ( |
||||
<UseState initialState={value}> |
||||
{(value, updateValue) => { |
||||
return ( |
||||
<ButtonSelect |
||||
value={value} |
||||
options={options} |
||||
onChange={(value) => { |
||||
action('onChanged fired')(value); |
||||
updateValue(value); |
||||
}} |
||||
label={value.label ? value.label : ''} |
||||
className="refresh-select" |
||||
iconClass={text('iconClass', 'clock-nine')} |
||||
/> |
||||
); |
||||
}} |
||||
</UseState> |
||||
); |
||||
}; |
@ -1,101 +0,0 @@ |
||||
import React, { PureComponent, ReactElement } from 'react'; |
||||
import Select from './Select'; |
||||
import { PopoverContent } from '../../../Tooltip/Tooltip'; |
||||
import { Icon } from '../../../Icon/Icon'; |
||||
import { IconName } from '../../../../types'; |
||||
import { SelectableValue } from '@grafana/data'; |
||||
|
||||
interface ButtonComponentProps { |
||||
label: ReactElement | string | undefined; |
||||
className: string | undefined; |
||||
iconClass?: string; |
||||
} |
||||
|
||||
// eslint-disable-next-line react/display-name
|
||||
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 && <Icon className={'select-button-icon'} name={iconClass as IconName} size="lg" />} |
||||
<span className="select-button-value">{label ? label : ''}</span> |
||||
{!props.menuIsOpen && <Icon name="angle-down" style={{ marginBottom: 0 }} size="lg" />} |
||||
{props.menuIsOpen && <Icon name="angle-up" style={{ marginBottom: 0 }} size="lg" />} |
||||
</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} |
||||
/> |
||||
); |
||||
} |
||||
} |
@ -1,50 +1,4 @@ |
||||
.refresh-picker { |
||||
position: relative; |
||||
display: none; |
||||
|
||||
.refresh-picker-buttons { |
||||
display: flex; |
||||
} |
||||
|
||||
.navbar-button--border-right-0 { |
||||
border-right: 0; |
||||
} |
||||
|
||||
.gf-form-input--form-dropdown { |
||||
position: static; |
||||
} |
||||
|
||||
.gf-form-select-box__menu { |
||||
position: absolute; |
||||
left: 0; |
||||
width: 100%; |
||||
} |
||||
|
||||
&--off { |
||||
.select-button-value { |
||||
display: none; |
||||
} |
||||
} |
||||
|
||||
&--live { |
||||
.select-button-value { |
||||
animation: liveText 2s infinite; |
||||
} |
||||
} |
||||
|
||||
@include media-breakpoint-up(xs) { |
||||
display: block; |
||||
} |
||||
} |
||||
|
||||
@keyframes liveText { |
||||
0% { |
||||
color: $orange; |
||||
} |
||||
50% { |
||||
color: $yellow; |
||||
} |
||||
100% { |
||||
color: $orange; |
||||
} |
||||
margin-left: 10px; |
||||
} |
||||
|
@ -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 { IconName } from '../../types'; |
||||
|
||||
interface ButtonSelectProps<T> extends Omit<SelectCommonProps<T>, 'renderControl' | 'size' | 'prefix'> { |
||||
icon?: IconName; |
||||
variant?: ButtonVariant; |
||||
size?: ComponentSize; |
||||
} |
||||
|
||||
interface SelectButtonProps extends Omit<ButtonProps, 'icon'> { |
||||
icon?: IconName; |
||||
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()); |
||||
return ( |
||||
<Button {...buttonProps} ref={ref} icon={icon}> |
||||
<span className={styles.wrapper}> |
||||
<span>{children}</span> |
||||
<span className={styles.caretWrap}> |
||||
<Icon name={isOpen ? 'angle-up' : 'angle-down'} /> |
||||
</span> |
||||
</span> |
||||
</Button> |
||||
); |
||||
} |
||||
); |
||||
SelectButton.displayName = 'SelectButton'; |
||||
|
||||
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} |
||||
// eslint-disable-next-line react/display-name
|
||||
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> |
||||
); |
||||
})} |
||||
/> |
||||
); |
||||
} |
@ -0,0 +1,21 @@ |
||||
import React, { FC } from 'react'; |
||||
import { css } from 'emotion'; |
||||
import { useTheme } from '../../themes'; |
||||
|
||||
export interface Props { |
||||
children?: React.ReactNode; |
||||
} |
||||
|
||||
export const DashboardStoryCanvas: FC<Props> = ({ children }) => { |
||||
const theme = useTheme(); |
||||
const style = css` |
||||
width: 100%; |
||||
height: 100%; |
||||
padding: 32px; |
||||
background: ${theme.colors.dashboardBg}; |
||||
`;
|
||||
|
||||
return <div className={style}>{children}</div>; |
||||
}; |
||||
|
||||
DashboardStoryCanvas.displayName = 'DashboardStoryCanvas'; |
@ -0,0 +1,29 @@ |
||||
import React, { FC } from 'react'; |
||||
import { css } from 'emotion'; |
||||
import { useTheme } from '../../themes/ThemeContext'; |
||||
|
||||
export interface Props { |
||||
name: string; |
||||
children?: React.ReactNode; |
||||
} |
||||
|
||||
export const StoryExample: FC<Props> = ({ name, children }) => { |
||||
const theme = useTheme(); |
||||
const style = css` |
||||
width: 100%; |
||||
padding: 16px; |
||||
`;
|
||||
const heading = css` |
||||
color: ${theme.colors.textWeak}; |
||||
margin-bottom: 16px; |
||||
`;
|
||||
|
||||
return ( |
||||
<div className={style}> |
||||
<h5 className={heading}>{name}</h5> |
||||
{children} |
||||
</div> |
||||
); |
||||
}; |
||||
|
||||
StoryExample.displayName = 'StoryExample'; |
Loading…
Reference in new issue