mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Forms: introduce RadioButtonGroup (#20828)
* introduce checkbox theme variables * Add checkbox component * Style tweaks * Namespace form styles returned from getFormStyles * wip * Radio button ui * Add simple docs for RadioButtonGroup * Merge fix * Move radio button variables from theme to component style getter
This commit is contained in:
@@ -3,7 +3,7 @@ import { css, cx } from 'emotion';
|
|||||||
import tinycolor from 'tinycolor2';
|
import tinycolor from 'tinycolor2';
|
||||||
import { selectThemeVariant, stylesFactory, ThemeContext } from '../../themes';
|
import { selectThemeVariant, stylesFactory, ThemeContext } from '../../themes';
|
||||||
import { Button as DefaultButton, LinkButton as DefaultLinkButton } from '../Button/Button';
|
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 { ButtonSize, StyleDeps } from '../Button/types';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
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) => {
|
const getPropertiesForVariant = (theme: GrafanaTheme, variant: ButtonVariant) => {
|
||||||
switch (variant) {
|
switch (variant) {
|
||||||
case 'secondary':
|
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
|
// 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 } = getPropertiesForSize(theme, size);
|
const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size);
|
||||||
const { background, borderColor } = getPropertiesForVariant(theme, variant);
|
const { background, borderColor } = getPropertiesForVariant(theme, variant);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -107,14 +75,14 @@ export const getButtonStyles = stylesFactory(({ theme, size, variant }: StylePro
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
font-weight: ${theme.typography.weight.semibold};
|
font-weight: ${theme.typography.weight.semibold};
|
||||||
font-size: ${fontSize};
|
|
||||||
font-family: ${theme.typography.fontFamily.sansSerif};
|
font-family: ${theme.typography.fontFamily.sansSerif};
|
||||||
line-height: ${theme.typography.lineHeight.sm};
|
line-height: ${theme.typography.lineHeight.sm};
|
||||||
|
font-size: ${fontSize};
|
||||||
padding: ${padding};
|
padding: ${padding};
|
||||||
|
height: ${height};
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
border: 1px solid ${borderColor};
|
border: 1px solid ${borderColor};
|
||||||
height: ${height};
|
|
||||||
border-radius: ${theme.border.radius.sm};
|
border-radius: ${theme.border.radius.sm};
|
||||||
${background};
|
${background};
|
||||||
|
|
||||||
|
|||||||
@@ -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<RadioButtonSize>('Size', sizes, 'md', VISUAL_GROUP);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioButton
|
||||||
|
disabled={disabled}
|
||||||
|
size={size}
|
||||||
|
active={active}
|
||||||
|
onClick={() => {
|
||||||
|
setActive(!active);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Radio button
|
||||||
|
</RadioButton>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<RadioButtonProps> = ({
|
||||||
|
children,
|
||||||
|
active = false,
|
||||||
|
disabled = false,
|
||||||
|
size = 'md',
|
||||||
|
onClick,
|
||||||
|
}) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getRadioButtonStyles(theme, size);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={cx(styles.button, active && styles.buttonActive)}
|
||||||
|
onClick={onClick}
|
||||||
|
disabled={disabled}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
RadioButton.displayName = 'RadioButton';
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import { Meta, Story, Preview, Props } from '@storybook/addon-docs/blocks';
|
||||||
|
import { RadioButtonGroup } from './RadioButtonGroup';
|
||||||
|
|
||||||
|
<Meta title="MDX|RadioButtonGroup" component={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';
|
||||||
|
|
||||||
|
<Forms.RadioButtonGroup options={...} value={...} onChange={...} />
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 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'];
|
||||||
|
|
||||||
|
|
||||||
|
<Forms.RadioButtonGroup
|
||||||
|
options={options}
|
||||||
|
disabledOptions={disabledOptions}
|
||||||
|
value={...}
|
||||||
|
onChange={...}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
<Props of={RadioButtonGroup} />
|
||||||
@@ -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<RadioButtonSize>('Size', sizes, 'md', VISUAL_GROUP);
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ label: 'Prometheus', value: 'prometheus' },
|
||||||
|
{ label: 'Graphite', value: 'graphite' },
|
||||||
|
{ label: 'Elastic', value: 'elastic' },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RadioButtonGroup
|
||||||
|
options={options}
|
||||||
|
disabled={disabled}
|
||||||
|
disabledOptions={[disabledItem]}
|
||||||
|
value={selected}
|
||||||
|
onChange={setSelected}
|
||||||
|
size={size}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -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<T> {
|
||||||
|
value: T;
|
||||||
|
disabled?: boolean;
|
||||||
|
disabledOptions?: T[];
|
||||||
|
options: Array<SelectableValue<T>>;
|
||||||
|
onChange: (value?: T) => void;
|
||||||
|
size?: RadioButtonSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function RadioButtonGroup<T>({
|
||||||
|
options,
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
disabled,
|
||||||
|
disabledOptions,
|
||||||
|
size = 'md',
|
||||||
|
}: RadioButtonGroupProps<T>) {
|
||||||
|
const styles = getRadioButtonGroupStyles();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.wrapper}>
|
||||||
|
{options.map(o => {
|
||||||
|
const isItemDisabled = disabledOptions && o.value && disabledOptions.indexOf(o.value) > -1;
|
||||||
|
return (
|
||||||
|
<RadioButton
|
||||||
|
size={size}
|
||||||
|
disabled={isItemDisabled || disabled}
|
||||||
|
active={value === o.value}
|
||||||
|
key={o.label}
|
||||||
|
onClick={() => {
|
||||||
|
onChange(o.value);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{o.label}
|
||||||
|
</RadioButton>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
RadioButtonGroup.displayName = 'RadioButtonGroup';
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
import { GrafanaTheme } from '@grafana/data';
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { ButtonSize } from '../Button/types';
|
||||||
|
|
||||||
export const getFocusCss = (theme: GrafanaTheme) => `
|
export const getFocusCss = (theme: GrafanaTheme) => `
|
||||||
outline: 2px dotted transparent;
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user