Skip to content

Commit

Permalink
Merge pull request #1319 from dxc-technology/gomezivann-radioGroup-focus
Browse files Browse the repository at this point in the history
Radio Group focus behaviour update
  • Loading branch information
Jialecl authored Oct 7, 2022
2 parents e922b5b + e5da602 commit 74ba970
Show file tree
Hide file tree
Showing 5 changed files with 92 additions and 157 deletions.
186 changes: 73 additions & 113 deletions lib/src/radio-group/Radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { v4 as uuidv4 } from "uuid";
import useTheme from "../useTheme";

const DxcRadio = ({
option,
currentValue,
label,
checked,
onClick,
error,
disabled,
Expand All @@ -15,12 +15,12 @@ const DxcRadio = ({
tabIndex,
}: RadioProps): JSX.Element => {
const [radioLabelId] = useState(`radio-${uuidv4()}`);
const ref = useRef<HTMLDivElement>(null);
const ref = useRef<HTMLSpanElement>(null);
const colorsTheme = useTheme();

const handleOnClick = () => {
onClick();
focused && document.activeElement !== ref?.current && ref?.current?.focus();
document.activeElement !== ref?.current && ref?.current?.focus();
};

const [firstUpdate, setFirstUpdate] = useState(true);
Expand All @@ -40,163 +40,100 @@ const DxcRadio = ({
error={error}
disabled={disabled}
readonly={readonly}
onMouseDown={(event) => {
// Prevents div's onClick from stealing the radio input's focus
event.preventDefault();
}}
onClick={handleOnClick}
onClick={disabled ? undefined : handleOnClick}
>
<RadioInputContainer>
<RadioInput
error={error}
disabled={disabled}
readonly={readonly}
role="radio"
aria-checked={option.value === currentValue}
aria-disabled={option.disabled ?? false}
aria-checked={checked}
aria-disabled={disabled}
aria-labelledby={radioLabelId}
tabIndex={disabled ? -1 : focused ? tabIndex : -1}
ref={ref}
>
{option.value === currentValue && <Dot disabled={disabled} readonly={readonly} error={error} />}
{checked && <Dot disabled={disabled} readonly={readonly} error={error} />}
</RadioInput>
</RadioInputContainer>
<Label id={radioLabelId} disabled={disabled}>
{option.label}
{label}
</Label>
</RadioContainer>
</RadioMainContainer>
</ThemeProvider>
);
};

const RadioMainContainer = styled.div`
display: flex;
`;

type RadioContainerProps = {
type CommonStylingProps = {
error?: string;
disabled?: boolean;
disabled: boolean;
readonly: boolean;
};
const RadioContainer = styled.span<RadioContainerProps>`
display: inline-flex;
align-items: center;
cursor: ${(props) => (props.disabled ? "not-allowed" : props.readonly ? "default" : "pointer")};
const getRadioInputStateColor = (props: CommonStylingProps & { theme: any }, state: "enabled" | "hover" | "active") => {
switch (state) {
case "enabled":
return props.disabled
? props.theme.disabledRadioInputColor
: props.error
? props.theme.errorRadioInputColor
: props.readonly
? props.theme.readonlyRadioInputColor
: props.theme.radioInputColor;
case "hover":
return props.error
? props.theme.hoverErrorRadioInputColor
: props.readonly
? props.theme.hoverReadonlyRadioInputColor
: props.theme.hoverRadioInputColor;
case "active":
return props.error
? props.theme.activeErrorRadioInputColor
: props.readonly
? props.theme.activeReadonlyRadioInputColor
: props.theme.activeRadioInputColor;
}
};

${(props) =>
!props.disabled
? `
&:hover {
& > div > div {
border-color: ${
props.error
? props.theme.hoverErrorRadioInputColor
: props.readonly
? props.theme.hoverReadonlyRadioInputColor
: props.theme.hoverRadioInputColor
};
& > span {
background-color: ${
props.error
? props.theme.hoverErrorRadioInputColor
: props.readonly
? props.theme.hoverReadonlyRadioInputColor
: props.theme.hoverRadioInputColor
};
}
};
}
&:active {
& > div > div {
border-color: ${
props.error
? props.theme.activeErrorRadioInputColor
: props.readonly
? props.theme.activeReadonlyRadioInputColor
: props.theme.activeRadioInputColor
};
& > span {
background-color: ${
props.error
? props.theme.activeErrorRadioInputColor
: props.readonly
? props.theme.activeReadonlyRadioInputColor
: props.theme.activeRadioInputColor
};
}
}
}
`
: "pointer-events: none;"}
const RadioMainContainer = styled.div`
display: flex;
`;

const RadioInputContainer = styled.div`
const RadioInputContainer = styled.span`
display: flex;
align-items: center;
justify-content: center;
height: 24px;
width: 24px;
`;

type RadioInputProps = {
error?: string;
disabled?: boolean;
readonly: boolean;
};
const RadioInput = styled.div<RadioInputProps>`
const RadioInput = styled.span<CommonStylingProps>`
box-sizing: border-box;
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 18px;
height: 18px;
border: 2px solid
${(props) => {
if (props.disabled) return props.theme.disabledRadioInputColor;
else if (props.error) return props.theme.errorRadioInputColor;
else if (props.readonly) return props.theme.readonlyRadioInputColor;
else return props.theme.radioInputColor;
}};
border: 2px solid ${(props) => getRadioInputStateColor(props, "enabled")};
border-radius: 50%;
${(props) =>
!props.disabled
? `&:focus {
outline: 2px solid ${props.theme.focusBorderColor};
outline-offset: 1px;
}
&:focus-visible {
outline: 2px solid ${props.theme.focusBorderColor};
outline-offset: 1px;
}
`
: `
:focus-visible {
outline: none;
}
`}
&:focus {
outline: 2px solid ${(props) => props.theme.focusBorderColor};
outline-offset: 1px;
}
${(props) => props.disabled && "pointer-events: none;"}
`;

type DotProps = {
error?: string;
disabled?: boolean;
readonly: boolean;
};
const Dot = styled.span<DotProps>`
const Dot = styled.span<CommonStylingProps>`
height: 10px;
width: 10px;
border-radius: 50%;
background-color: ${(props) => {
if (props.disabled) return props.theme.disabledRadioInputColor;
else if (props.error) return props.theme.errorRadioInputColor;
else if (props.readonly) return props.theme.readonlyRadioInputColor;
else return props.theme.radioInputColor;
}};
background-color: ${(props) => getRadioInputStateColor(props, "enabled")};
`;

type LabelProps = {
disabled?: boolean;
disabled: boolean;
};
const Label = styled.span<LabelProps>`
margin-left: ${(props) => props.theme.radioInputLabelMargin};
Expand All @@ -207,8 +144,31 @@ const Label = styled.span<LabelProps>`
line-height: ${(props) => props.theme.radioInputLabelLineHeight};
${(props) =>
props.disabled
? `color: ${props.theme.disabledRadioInputLabelFontColor}; pointer-events: none;`
? `color: ${props.theme.disabledRadioInputLabelFontColor};`
: `color: ${props.theme.radioInputLabelFontColor}`}
`;

const RadioContainer = styled.span<CommonStylingProps>`
display: inline-flex;
align-items: center;
cursor: ${(props) => (props.disabled ? "not-allowed" : props.readonly ? "default" : "pointer")};
&:hover {
${RadioInput} {
border-color: ${(props) => !props.disabled && getRadioInputStateColor(props, "hover")};
}
${Dot} {
background-color: ${(props) => !props.disabled && getRadioInputStateColor(props, "hover")};
}
}
&:active {
${RadioInput} {
border-color: ${(props) => !props.disabled && getRadioInputStateColor(props, "active")};
}
${Dot} {
background-color: ${(props) => !props.disabled && getRadioInputStateColor(props, "active")};
}
}
`;

export default React.memo(DxcRadio);
1 change: 1 addition & 0 deletions lib/src/radio-group/RadioGroup.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import React from "react";
import ExampleContainer from "../../.storybook/components/ExampleContainer";
import Title from "../../.storybook/components/Title";
import DxcRadioGroup from "./RadioGroup";
Expand Down
31 changes: 5 additions & 26 deletions lib/src/radio-group/RadioGroup.test.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from "react";
import DxcRadioGroup from "./RadioGroup";
import { render, fireEvent } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import DxcRadioGroup from "./RadioGroup.tsx";

const options = [
{ label: "Option 01", value: "1" },
Expand All @@ -15,7 +15,7 @@ const options = [
{ label: "Option 09", value: "9" },
];

const single_disabled_options = [
const singleDisabledOptions = [
{ label: "Option 01", value: "1" },
{ label: "Option 02", value: "2" },
{ label: "Option 03", value: "3", disabled: true },
Expand Down Expand Up @@ -62,7 +62,7 @@ describe("Radio Group component tests", () => {
radios.forEach((radio) => {
expect(radio.tabIndex).toBe(-1);
});
fireEvent.keyDown(radioGroup, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 });
fireEvent.keyDown(radioGroup, { key: " ", code: "Space", keyCode: 13, charCode: 13 });
fireEvent.keyDown(radioGroup, { key: "ArrowLeft", code: "ArrowLeft", keyCode: 37, charCode: 37 });
fireEvent.keyDown(radioGroup, { key: "ArrowDown", code: "ArrowDown", keyCode: 40, charCode: 40 });
radios.forEach((radio) => {
Expand All @@ -71,7 +71,7 @@ describe("Radio Group component tests", () => {
});
test("Disabled option renders with correct aria attribute, correct tabIndex value and it is not focusable by keyboard (focus 'jumps' the disabled option)", () => {
const { getByRole, getAllByRole } = render(
<DxcRadioGroup name="test" label="test-radioGroup-label" options={single_disabled_options} />
<DxcRadioGroup name="test" label="test-radioGroup-label" options={singleDisabledOptions} />
);
const radioGroup = getByRole("radiogroup");
const radios = getAllByRole("radio");
Expand Down Expand Up @@ -266,27 +266,6 @@ describe("Radio Group component tests", () => {
expect(onChange).not.toHaveBeenCalled();
expect(document.activeElement).toEqual(checkedRadio);
});
test("The 'enter' key checks the current focused option if anyone is checked", () => {
const onChange = jest.fn();
const { getByRole, getAllByRole, container } = render(
<DxcRadioGroup
name="test"
label="test-radio-group-label"
helperText="test-radio-group-helper-text"
options={options}
onChange={onChange}
/>
);
const radioGroup = getByRole("radiogroup");
const checkedRadio = getAllByRole("radio")[0];
const submitInput = container.querySelector(`input[name="test"]`);

fireEvent.keyDown(radioGroup, { key: "Enter", code: "Enter", keyCode: 13, charCode: 13 });
expect(onChange).toHaveBeenCalledWith("1");
expect(checkedRadio.getAttribute("aria-checked")).toBe("true");
expect(checkedRadio.tabIndex).toBe(0);
expect(submitInput.value).toBe("1");
});
test("The 'space' key checks the current focused option if anyone is checked", () => {
const onChange = jest.fn();
const { getByRole, getAllByRole, container } = render(
Expand All @@ -302,7 +281,7 @@ describe("Radio Group component tests", () => {
const checkedRadio = getAllByRole("radio")[0];
const submitInput = container.querySelector(`input[name="test"]`);

fireEvent.keyDown(radioGroup, { key: "Space", code: "Space", keyCode: 32, charCode: 32 });
fireEvent.keyDown(radioGroup, { key: " ", code: "Space", keyCode: 32, charCode: 32 });
expect(onChange).toHaveBeenCalledWith("1");
expect(checkedRadio.getAttribute("aria-checked")).toBe("true");
expect(checkedRadio.tabIndex).toBe(0);
Expand Down
Loading

0 comments on commit 74ba970

Please sign in to comment.