mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06: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:
parent
534295a9ae
commit
ca85176ac6
@ -21,7 +21,8 @@ export const simple = () => {
|
||||
disabled={disabled}
|
||||
size={size}
|
||||
active={active}
|
||||
onClick={() => {
|
||||
id="standalone"
|
||||
onChange={() => {
|
||||
setActive(!active);
|
||||
}}
|
||||
>
|
||||
|
@ -8,12 +8,15 @@ export type RadioButtonSize = 'sm' | 'md';
|
||||
export interface RadioButtonProps {
|
||||
size?: RadioButtonSize;
|
||||
disabled?: boolean;
|
||||
name?: string;
|
||||
active: boolean;
|
||||
onClick: () => void;
|
||||
id: string;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
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 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}`;
|
||||
|
||||
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;
|
||||
radio: css`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: -100vw;
|
||||
opacity: 0;
|
||||
z-index: -1000;
|
||||
|
||||
/* 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
|
||||
* */
|
||||
&:checked + label {
|
||||
border: ${borderActive};
|
||||
color: ${textColorActive};
|
||||
text-shadow: ${fakeBold};
|
||||
background: ${bgActive};
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
z-index: 1;
|
||||
&:focus + label {
|
||||
${getFocusCss(theme)};
|
||||
&:before {
|
||||
background: ${borderColor};
|
||||
}
|
||||
&:hover {
|
||||
&:before {
|
||||
background: ${borderColorHover};
|
||||
}
|
||||
}
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
&:disabled + label {
|
||||
cursor: default;
|
||||
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};
|
||||
&:enabled + label:hover {
|
||||
text-shadow: ${fakeBold};
|
||||
}
|
||||
`,
|
||||
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`
|
||||
background: ${bgActive};
|
||||
border: ${borderActive};
|
||||
border-left: 0;
|
||||
color: ${textColorActive};
|
||||
text-shadow: ${fakeBold};
|
||||
user-select: none;
|
||||
|
||||
&: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};
|
||||
color: ${textColorHover};
|
||||
border: ${borderHover};
|
||||
z-index: 2;
|
||||
}
|
||||
`,
|
||||
};
|
||||
@ -169,20 +97,28 @@ export const RadioButton: React.FC<RadioButtonProps> = ({
|
||||
active = false,
|
||||
disabled = false,
|
||||
size = 'md',
|
||||
onClick,
|
||||
onChange,
|
||||
id,
|
||||
name = undefined,
|
||||
}) => {
|
||||
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>
|
||||
<>
|
||||
<input
|
||||
type="radio"
|
||||
className={cx(styles.radio)}
|
||||
onChange={onChange}
|
||||
disabled={disabled}
|
||||
id={id}
|
||||
checked={active}
|
||||
name={name}
|
||||
/>
|
||||
<label className={cx(styles.radioLabel)} htmlFor={id}>
|
||||
{children}
|
||||
</label>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import React, { useCallback } from 'react';
|
||||
import React, { useCallback, useRef } from 'react';
|
||||
import { css } from 'emotion';
|
||||
import uniqueId from 'lodash/uniqueId';
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { RadioButtonSize, RadioButton } from './RadioButton';
|
||||
|
||||
@ -11,6 +12,23 @@ const getRadioButtonGroupStyles = () => {
|
||||
flex-wrap: nowrap;
|
||||
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> {
|
||||
@ -30,7 +48,7 @@ export function RadioButtonGroup<T>({
|
||||
disabledOptions,
|
||||
size = 'md',
|
||||
}: RadioButtonGroupProps<T>) {
|
||||
const handleOnClick = useCallback(
|
||||
const handleOnChange = useCallback(
|
||||
(option: SelectableValue<T>) => {
|
||||
return () => {
|
||||
if (onChange) {
|
||||
@ -40,19 +58,22 @@ export function RadioButtonGroup<T>({
|
||||
},
|
||||
[onChange]
|
||||
);
|
||||
const groupName = useRef(uniqueId('radiogroup-'));
|
||||
const styles = getRadioButtonGroupStyles();
|
||||
|
||||
return (
|
||||
<div className={styles.wrapper}>
|
||||
{options.map(o => {
|
||||
const isItemDisabled = disabledOptions && o.value && disabledOptions.indexOf(o.value) > -1;
|
||||
<div className={styles.radioGroup}>
|
||||
{options.map((o, i) => {
|
||||
const isItemDisabled = disabledOptions && o.value && disabledOptions.includes(o.value);
|
||||
return (
|
||||
<RadioButton
|
||||
size={size}
|
||||
disabled={isItemDisabled || disabled}
|
||||
active={value === o.value}
|
||||
key={o.label}
|
||||
onClick={handleOnClick(o)}
|
||||
onChange={handleOnChange(o)}
|
||||
id={`option-${i}`}
|
||||
name={groupName.current}
|
||||
>
|
||||
{o.label}
|
||||
</RadioButton>
|
||||
|
Loading…
Reference in New Issue
Block a user