Commit 5d6d5bf6 by Dominik Prokop Committed by GitHub

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
parent a3ab04c0
...@@ -3,7 +3,7 @@ import { css, cx } from 'emotion'; ...@@ -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 ...@@ -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) => ...@@ -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 ...@@ -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};
......
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>
);
};
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';
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} />
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}
/>
);
};
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';
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 = () => { ...@@ -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,
};
}
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment