diff --git a/packages/grafana-ui/src/components/Forms/Button.tsx b/packages/grafana-ui/src/components/Forms/Button.tsx index c9bc7df0fec..73fe733e39e 100644 --- a/packages/grafana-ui/src/components/Forms/Button.tsx +++ b/packages/grafana-ui/src/components/Forms/Button.tsx @@ -3,7 +3,7 @@ import { css, cx } from 'emotion'; import tinycolor from 'tinycolor2'; import { selectThemeVariant, stylesFactory, ThemeContext } from '../../themes'; import { Button as DefaultButton, LinkButton as DefaultLinkButton } from '../Button/Button'; -import { getFocusStyle } from './commonStyles'; +import { getFocusStyle, getPropertiesForButtonSize } from './commonStyles'; import { ButtonSize, StyleDeps } from '../Button/types'; import { GrafanaTheme } from '@grafana/data'; @@ -21,38 +21,6 @@ const buttonVariantStyles = (from: string, to: string, textColor: string) => css } `; -const getPropertiesForSize = (theme: GrafanaTheme, size: ButtonSize) => { - switch (size) { - case 'sm': - return { - padding: `0 ${theme.spacing.sm}`, - fontSize: theme.typography.size.sm, - height: theme.height.sm, - }; - - case 'md': - return { - padding: `0 ${theme.spacing.md}`, - fontSize: theme.typography.size.md, - height: `${theme.spacing.formButtonHeight}px`, - }; - - case 'lg': - return { - padding: `0 ${theme.spacing.lg}`, - fontSize: theme.typography.size.lg, - height: theme.height.lg, - }; - - default: - return { - padding: `0 ${theme.spacing.md}`, - fontSize: theme.typography.size.base, - height: theme.height.md, - }; - } -}; - const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) => { switch (variant) { case 'secondary': @@ -96,7 +64,7 @@ const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) => // Need to do this because of mismatch between variants in standard buttons and here type StyleProps = Omit & { variant: ButtonVariant }; export const getButtonStyles = stylesFactory(({ theme, size, variant }: StyleProps) => { - const { padding, fontSize, height } = getPropertiesForSize(theme, size); + const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size); const { background, borderColor } = getPropertiesForVariant(theme, variant); return { @@ -107,14 +75,14 @@ export const getButtonStyles = stylesFactory(({ theme, size, variant }: StylePro display: inline-flex; align-items: center; font-weight: ${theme.typography.weight.semibold}; - font-size: ${fontSize}; font-family: ${theme.typography.fontFamily.sansSerif}; line-height: ${theme.typography.lineHeight.sm}; + font-size: ${fontSize}; padding: ${padding}; + height: ${height}; vertical-align: middle; cursor: pointer; border: 1px solid ${borderColor}; - height: ${height}; border-radius: ${theme.border.radius.sm}; ${background}; diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.story.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.story.tsx new file mode 100644 index 00000000000..2a0f08b50c4 --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.story.tsx @@ -0,0 +1,31 @@ +import React, { useState } from 'react'; +import { RadioButton, RadioButtonSize } from './RadioButton'; +import { boolean, select } from '@storybook/addon-knobs'; + +export default { + title: 'UI/Forms/RadioButton', + component: RadioButton, +}; + +const sizes: RadioButtonSize[] = ['sm', 'md']; + +export const simple = () => { + const [active, setActive] = useState(); + const BEHAVIOUR_GROUP = 'Behaviour props'; + const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP); + const VISUAL_GROUP = 'Visual options'; + const size = select('Size', sizes, 'md', VISUAL_GROUP); + + return ( + { + setActive(!active); + }} + > + Radio button + + ); +}; diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx new file mode 100644 index 00000000000..80edf8134cf --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButton.tsx @@ -0,0 +1,189 @@ +import React from 'react'; +import { useTheme, stylesFactory, selectThemeVariant as stv } from '../../../themes'; +import { GrafanaTheme } from '@grafana/data'; +import { css, cx } from 'emotion'; +import { getFocusCss, getPropertiesForButtonSize } from '../commonStyles'; + +export type RadioButtonSize = 'sm' | 'md'; +export interface RadioButtonProps { + size?: RadioButtonSize; + disabled?: boolean; + active: boolean; + onClick: () => void; +} + +const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButtonSize) => { + const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size); + const c = theme.colors; + + const textColor = stv({ light: c.gray33, dark: c.gray70 }, theme.type); + const textColorHover = stv({ light: c.blueShade, dark: c.blueLight }, theme.type); + const textColorActive = stv({ light: c.blueShade, dark: c.blueLight }, theme.type); + const borderColor = stv({ light: c.gray4, dark: c.gray25 }, theme.type); + const borderColorHover = stv({ light: c.gray70, dark: c.gray33 }, theme.type); + const borderColorActive = stv({ light: c.blueShade, dark: c.blueLight }, theme.type); + const bg = stv({ light: c.gray98, dark: c.gray10 }, theme.type); + const bgDisabled = stv({ light: c.gray95, dark: c.gray15 }, theme.type); + const bgActive = stv({ light: c.white, dark: c.gray05 }, theme.type); + + const border = `1px solid ${borderColor}`; + const borderActive = `1px solid ${borderColorActive}`; + const borderHover = `1px solid ${borderColorHover}`; + const fakeBold = `0 0 0.65px ${textColorHover}, 0 0 0.65px ${textColorHover}`; + + return { + button: css` + cursor: pointer; + position: relative; + z-index: 0; + background: ${bg}; + border: ${border}; + color: ${textColor}; + font-size: ${fontSize}; + padding: ${padding}; + height: ${height}; + border-left: 0; + + /* This pseudo element is responsible for rendering the lines between buttons when they are groupped */ + &:before { + content: ''; + position: absolute; + top: -1px; + left: -1px; + width: 1px; + height: calc(100% + 2px); + } + + &:hover { + border: ${borderHover}; + border-left: 0; + &:before { + /* renders line between elements */ + background: ${borderColorHover}; + } + &:first-child { + border-left: ${borderHover}; + } + &:last-child { + border-right: ${borderHover}; + } + &:first-child:before { + /* Don't render divider line on first element*/ + display: none; + } + } + + &:not(:disabled):hover { + color: ${textColorHover}; + /* The text shadow imitates font-weight:bold; + * Using font weight on hover makes the button size slighlty change which looks like a glitch + * */ + text-shadow: ${fakeBold}; + } + + &:focus { + z-index: 1; + ${getFocusCss(theme)}; + &:before { + background: ${borderColor}; + } + &:hover { + &:before { + background: ${borderColorHover}; + } + } + } + + &:disabled { + background: ${bgDisabled}; + color: ${textColor}; + } + + &:first-child { + border-top-left-radius: ${theme.border.radius.sm}; + border-bottom-left-radius: ${theme.border.radius.sm}; + border-left: ${border}; + } + &:last-child { + border-top-right-radius: ${theme.border.radius.sm}; + border-bottom-right-radius: ${theme.border.radius.sm}; + border-right: ${border}; + } + `, + + buttonActive: css` + background: ${bgActive}; + border: ${borderActive}; + border-left: none; + color: ${textColorActive}; + text-shadow: ${fakeBold}; + + &:hover { + border: ${borderActive}; + border-left: none; + } + + &:focus { + &:before { + background: ${borderColorActive}; + } + &:hover:before { + background: ${borderColorActive}; + } + } + + &:before, + &:hover:before { + background: ${borderColorActive}; + } + + &:first-child, + &:first-child:hover { + border-left: ${borderActive}; + } + &:last-child, + &:last-child:hover { + border-right: ${borderActive}; + } + + &:first-child { + &:before { + display: none; + } + } + + & + button:hover { + &:before { + display: none; + } + } + &:focus { + border-color: ${borderActive}; + } + `, + }; +}); + +export const RadioButton: React.FC = ({ + children, + active = false, + disabled = false, + size = 'md', + onClick, +}) => { + const theme = useTheme(); + const styles = getRadioButtonStyles(theme, size); + + return ( + + ); +}; + +RadioButton.displayName = 'RadioButton'; diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.mdx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.mdx new file mode 100644 index 00000000000..384a81150be --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.mdx @@ -0,0 +1,44 @@ +import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks'; +import { RadioButtonGroup } from './RadioButtonGroup'; + + + +# RadioButtonGroup + +`RadioButtonGroup` is used for selecting single value from multiple options. + +Use `RadioButtonGroup` if there are up to four options available. Otherwise use Select component. + +### Usage + +```jsx +import { Forms } from '@grafana/ui'; + + +``` + +#### Disabling options + +To disable some options pass those options to the `RadioButtonGroup` via `disabledOptions` property: + +```jsx + +const options = [ + { label: 'Prometheus', value: 'prometheus' }, + { label: 'Graphite', value: 'graphite' }, + { label: 'Elastic', value: 'elastic' }, + { label: 'InfluxDB', value: 'influx' }, +]; + +const disabledOptions = ['prometheus', 'elastic']; + + + +``` + + diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.story.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.story.tsx new file mode 100644 index 00000000000..ddb13145e7f --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.story.tsx @@ -0,0 +1,43 @@ +import React, { useState } from 'react'; +import mdx from './RadioButtonGroup.mdx'; +import { RadioButtonGroup } from './RadioButtonGroup'; +import { RadioButtonSize } from './RadioButton'; +import { boolean, select } from '@storybook/addon-knobs'; + +export default { + title: 'UI/Forms/RadioButtonGroup', + component: RadioButtonGroup, + parameters: { + docs: { + page: mdx, + }, + }, +}; + +const sizes: RadioButtonSize[] = ['sm', 'md']; + +export const simple = () => { + const [selected, setSelected] = useState(); + const BEHAVIOUR_GROUP = 'Behaviour props'; + const disabled = boolean('Disabled', false, BEHAVIOUR_GROUP); + const disabledItem = select('Disabled item', ['', 'graphite', 'prometheus', 'elastic'], '', BEHAVIOUR_GROUP); + const VISUAL_GROUP = 'Visual options'; + const size = select('Size', sizes, 'md', VISUAL_GROUP); + + const options = [ + { label: 'Prometheus', value: 'prometheus' }, + { label: 'Graphite', value: 'graphite' }, + { label: 'Elastic', value: 'elastic' }, + ]; + + return ( + + ); +}; diff --git a/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx new file mode 100644 index 00000000000..aa32e97bc9b --- /dev/null +++ b/packages/grafana-ui/src/components/Forms/RadioButtonGroup/RadioButtonGroup.tsx @@ -0,0 +1,57 @@ +import React from 'react'; +import { css } from 'emotion'; +import { SelectableValue } from '@grafana/data'; +import { RadioButtonSize, RadioButton } from './RadioButton'; + +const getRadioButtonGroupStyles = () => { + return { + wrapper: css` + display: flex; + flex-direction: row; + flex-wrap: nowrap; + position: relative; + `, + }; +}; +interface RadioButtonGroupProps { + value: T; + disabled?: boolean; + disabledOptions?: T[]; + options: Array>; + onChange: (value?: T) => void; + size?: RadioButtonSize; +} + +export function RadioButtonGroup({ + options, + value, + onChange, + disabled, + disabledOptions, + size = 'md', +}: RadioButtonGroupProps) { + const styles = getRadioButtonGroupStyles(); + + return ( +
+ {options.map(o => { + const isItemDisabled = disabledOptions && o.value && disabledOptions.indexOf(o.value) > -1; + return ( + { + onChange(o.value); + }} + > + {o.label} + + ); + })} +
+ ); +} + +RadioButtonGroup.displayName = 'RadioButtonGroup'; diff --git a/packages/grafana-ui/src/components/Forms/commonStyles.ts b/packages/grafana-ui/src/components/Forms/commonStyles.ts index bce41bee55a..ac5568d113b 100644 --- a/packages/grafana-ui/src/components/Forms/commonStyles.ts +++ b/packages/grafana-ui/src/components/Forms/commonStyles.ts @@ -1,5 +1,6 @@ import { css } from 'emotion'; import { GrafanaTheme } from '@grafana/data'; +import { ButtonSize } from '../Button/types'; export const getFocusCss = (theme: GrafanaTheme) => ` outline: 2px dotted transparent; @@ -56,3 +57,35 @@ export const inputSizes = () => { `, }; }; + +export const getPropertiesForButtonSize = (theme: GrafanaTheme, size: ButtonSize) => { + switch (size) { + case 'sm': + return { + padding: `0 ${theme.spacing.sm}`, + fontSize: theme.typography.size.sm, + height: theme.height.sm, + }; + + case 'md': + return { + padding: `0 ${theme.spacing.md}`, + fontSize: theme.typography.size.md, + height: `${theme.spacing.formButtonHeight}px`, + }; + + case 'lg': + return { + padding: `0 ${theme.spacing.lg}`, + fontSize: theme.typography.size.lg, + height: theme.height.lg, + }; + + default: + return { + padding: `0 ${theme.spacing.md}`, + fontSize: theme.typography.size.base, + height: theme.height.md, + }; + } +};