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:
parent
a3ab04c0de
commit
5d6d5bf64d
@ -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<StyleDeps, 'variant'> & { 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};
|
||||
|
||||
|
@ -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 { 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,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user