Commit ca85176a by kay delaney Committed by GitHub

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
parent 534295a9
......@@ -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;
/* This pseudo element is responsible for rendering the lines between buttons when they are groupped */
&:before {
content: '';
radio: css`
position: absolute;
top: -1px;
left: -1px;
width: 1px;
height: calc(100% + 2px);
}
top: 0;
left: -100vw;
opacity: 0;
z-index: -1000;
&: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}
<>
<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}
</button>
</label>
</>
);
};
......
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>
......
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