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 = () => { ...@@ -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'; ...@@ -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 ...@@ -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: absolute;
position: relative; top: 0;
z-index: 0; left: -100vw;
background: ${bg}; opacity: 0;
border: ${border}; z-index: -1000;
color: ${textColor};
font-size: ${fontSize}; &:checked + label {
padding: ${padding}; border: ${borderActive};
height: ${height}; color: ${textColorActive};
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}; 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> = ({ ...@@ -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)}
disabled={disabled} onChange={onChange}
> disabled={disabled}
{children} id={id}
</button> checked={active}
name={name}
/>
<label className={cx(styles.radioLabel)} htmlFor={id}>
{children}
</label>
</>
); );
}; };
......
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 = () => { ...@@ -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>({ ...@@ -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>({ ...@@ -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>
......
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