mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 00:01:48 +08:00
Forms/RadioButtonGroup: Improves semantics and simplifies CSS (#22093)
* Forms/RadioButtonGroup: Improves semantics and simplifies CSS - Changes base element to radio input for improved semantics & automatic keyboard support - Simplifies CSS
This commit is contained in:
@ -21,7 +21,8 @@ export const simple = () => {
|
|||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
size={size}
|
size={size}
|
||||||
active={active}
|
active={active}
|
||||||
onClick={() => {
|
id="standalone"
|
||||||
|
onChange={() => {
|
||||||
setActive(!active);
|
setActive(!active);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
@ -8,12 +8,15 @@ export type RadioButtonSize = 'sm' | 'md';
|
|||||||
export interface RadioButtonProps {
|
export interface RadioButtonProps {
|
||||||
size?: RadioButtonSize;
|
size?: RadioButtonSize;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
|
name?: string;
|
||||||
active: boolean;
|
active: boolean;
|
||||||
onClick: () => void;
|
id: string;
|
||||||
|
onChange: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButtonSize) => {
|
const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButtonSize) => {
|
||||||
const { padding, fontSize, height } = getPropertiesForButtonSize(theme, size);
|
const { fontSize, height } = getPropertiesForButtonSize(theme, size);
|
||||||
|
const horizontalPadding = theme.spacing[size] ?? theme.spacing.md;
|
||||||
const c = theme.colors;
|
const c = theme.colors;
|
||||||
|
|
||||||
const textColor = stv({ light: c.gray33, dark: c.gray70 }, theme.type);
|
const textColor = stv({ light: c.gray33, dark: c.gray70 }, theme.type);
|
||||||
@ -32,133 +35,58 @@ const getRadioButtonStyles = stylesFactory((theme: GrafanaTheme, size: RadioButt
|
|||||||
const fakeBold = `0 0 0.65px ${textColorHover}, 0 0 0.65px ${textColorHover}`;
|
const fakeBold = `0 0 0.65px ${textColorHover}, 0 0 0.65px ${textColorHover}`;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
button: css`
|
radio: 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;
|
position: absolute;
|
||||||
top: -1px;
|
top: 0;
|
||||||
left: -1px;
|
left: -100vw;
|
||||||
width: 1px;
|
opacity: 0;
|
||||||
height: calc(100% + 2px);
|
z-index: -1000;
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:checked + label {
|
||||||
border: ${borderHover};
|
border: ${borderActive};
|
||||||
border-left: 0;
|
color: ${textColorActive};
|
||||||
&: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};
|
text-shadow: ${fakeBold};
|
||||||
|
background: ${bgActive};
|
||||||
|
z-index: 3;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:focus {
|
&:focus + label {
|
||||||
z-index: 1;
|
|
||||||
${getFocusCss(theme)};
|
${getFocusCss(theme)};
|
||||||
&:before {
|
z-index: 3;
|
||||||
background: ${borderColor};
|
|
||||||
}
|
|
||||||
&:hover {
|
|
||||||
&:before {
|
|
||||||
background: ${borderColorHover};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&:disabled {
|
&:disabled + label {
|
||||||
|
cursor: default;
|
||||||
background: ${bgDisabled};
|
background: ${bgDisabled};
|
||||||
color: ${textColor};
|
color: ${textColor};
|
||||||
}
|
}
|
||||||
|
|
||||||
&:first-child {
|
&:enabled + label:hover {
|
||||||
border-top-left-radius: ${theme.border.radius.sm};
|
text-shadow: ${fakeBold};
|
||||||
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};
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
|
radioLabel: css`
|
||||||
|
display: inline-block;
|
||||||
|
position: relative;
|
||||||
|
font-size: ${fontSize};
|
||||||
|
min-height: ${fontSize};
|
||||||
|
color: ${textColor};
|
||||||
|
padding: calc((${height} - ${fontSize}) / 2) ${horizontalPadding} calc((${height} - ${fontSize}) / 2)
|
||||||
|
${horizontalPadding};
|
||||||
|
line-height: 1;
|
||||||
|
margin-left: -1px;
|
||||||
|
border-radius: ${theme.border.radius.sm};
|
||||||
|
border: ${border};
|
||||||
|
background: ${bg};
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
buttonActive: css`
|
user-select: none;
|
||||||
background: ${bgActive};
|
|
||||||
border: ${borderActive};
|
|
||||||
border-left: 0;
|
|
||||||
color: ${textColorActive};
|
|
||||||
text-shadow: ${fakeBold};
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border: ${borderActive};
|
color: ${textColorHover};
|
||||||
border-left: none;
|
border: ${borderHover};
|
||||||
}
|
z-index: 2;
|
||||||
|
|
||||||
&: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};
|
|
||||||
}
|
}
|
||||||
`,
|
`,
|
||||||
};
|
};
|
||||||
@ -169,20 +97,28 @@ export const RadioButton: React.FC<RadioButtonProps> = ({
|
|||||||
active = false,
|
active = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
onClick,
|
onChange,
|
||||||
|
id,
|
||||||
|
name = undefined,
|
||||||
}) => {
|
}) => {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const styles = getRadioButtonStyles(theme, size);
|
const styles = getRadioButtonStyles(theme, size);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<>
|
||||||
type="button"
|
<input
|
||||||
className={cx(styles.button, active && styles.buttonActive)}
|
type="radio"
|
||||||
onClick={onClick}
|
className={cx(styles.radio)}
|
||||||
|
onChange={onChange}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
>
|
id={id}
|
||||||
|
checked={active}
|
||||||
|
name={name}
|
||||||
|
/>
|
||||||
|
<label className={cx(styles.radioLabel)} htmlFor={id}>
|
||||||
{children}
|
{children}
|
||||||
</button>
|
</label>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React, { useCallback } from 'react';
|
import React, { useCallback, useRef } from 'react';
|
||||||
import { css } from 'emotion';
|
import { css } from 'emotion';
|
||||||
|
import uniqueId from 'lodash/uniqueId';
|
||||||
import { SelectableValue } from '@grafana/data';
|
import { SelectableValue } from '@grafana/data';
|
||||||
import { RadioButtonSize, RadioButton } from './RadioButton';
|
import { RadioButtonSize, RadioButton } from './RadioButton';
|
||||||
|
|
||||||
@ -11,6 +12,23 @@ const getRadioButtonGroupStyles = () => {
|
|||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
position: relative;
|
position: relative;
|
||||||
`,
|
`,
|
||||||
|
radioGroup: css`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
|
||||||
|
label {
|
||||||
|
border-radius: 0px;
|
||||||
|
|
||||||
|
&:first-of-type {
|
||||||
|
border-radius: 2px 0px 0px 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-of-type {
|
||||||
|
border-radius: 0px 2px 2px 0px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
interface RadioButtonGroupProps<T> {
|
interface RadioButtonGroupProps<T> {
|
||||||
@ -30,7 +48,7 @@ export function RadioButtonGroup<T>({
|
|||||||
disabledOptions,
|
disabledOptions,
|
||||||
size = 'md',
|
size = 'md',
|
||||||
}: RadioButtonGroupProps<T>) {
|
}: RadioButtonGroupProps<T>) {
|
||||||
const handleOnClick = useCallback(
|
const handleOnChange = useCallback(
|
||||||
(option: SelectableValue<T>) => {
|
(option: SelectableValue<T>) => {
|
||||||
return () => {
|
return () => {
|
||||||
if (onChange) {
|
if (onChange) {
|
||||||
@ -40,19 +58,22 @@ export function RadioButtonGroup<T>({
|
|||||||
},
|
},
|
||||||
[onChange]
|
[onChange]
|
||||||
);
|
);
|
||||||
|
const groupName = useRef(uniqueId('radiogroup-'));
|
||||||
const styles = getRadioButtonGroupStyles();
|
const styles = getRadioButtonGroupStyles();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.wrapper}>
|
<div className={styles.radioGroup}>
|
||||||
{options.map(o => {
|
{options.map((o, i) => {
|
||||||
const isItemDisabled = disabledOptions && o.value && disabledOptions.indexOf(o.value) > -1;
|
const isItemDisabled = disabledOptions && o.value && disabledOptions.includes(o.value);
|
||||||
return (
|
return (
|
||||||
<RadioButton
|
<RadioButton
|
||||||
size={size}
|
size={size}
|
||||||
disabled={isItemDisabled || disabled}
|
disabled={isItemDisabled || disabled}
|
||||||
active={value === o.value}
|
active={value === o.value}
|
||||||
key={o.label}
|
key={o.label}
|
||||||
onClick={handleOnClick(o)}
|
onChange={handleOnChange(o)}
|
||||||
|
id={`option-${i}`}
|
||||||
|
name={groupName.current}
|
||||||
>
|
>
|
||||||
{o.label}
|
{o.label}
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
|
Reference in New Issue
Block a user