Skip to content

Commit

Permalink
Merge pull request #1191 from nsbno/radio-card-uu
Browse files Browse the repository at this point in the history
Radio card accessibility
  • Loading branch information
alicemacl authored Jun 12, 2024
2 parents c5f30bf + 89889cc commit 4a546dd
Show file tree
Hide file tree
Showing 5 changed files with 94 additions and 64 deletions.
5 changes: 5 additions & 0 deletions .changeset/orange-coats-taste.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@vygruppen/spor-react": patch
---

RadioCard: Fix accessibility features
2 changes: 1 addition & 1 deletion apps/docs/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"private": true,
"version": "0.0.35",
"version": "0.0.36",
"name": "@vygruppen/docs",
"description": "The Spor documentation",
"license": "MIT",
Expand Down
78 changes: 45 additions & 33 deletions packages/spor-react/src/layout/RadioCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
forwardRef,
useMultiStyleConfig,
} from "@chakra-ui/react";
import React, { useContext, useEffect, useId } from "react";
import React, { useContext, useEffect, useId, useState } from "react";
import { RadioCardGroupContext } from "./RadioCardGroup";

/**
Expand Down Expand Up @@ -38,7 +38,7 @@ export type RadioCardProps = BoxProps & {
};

export const RadioCard = forwardRef(
({ children, value = "base", isDisabled, ...props }: RadioCardProps, ref) => {
({ children, value, isDisabled, ...props }: RadioCardProps, ref) => {
const context = useContext(RadioCardGroupContext);

if (!context) {
Expand All @@ -51,57 +51,69 @@ export const RadioCard = forwardRef(

const styles = useMultiStyleConfig("RadioCard", { variant });

const [isKeyboardUser, setKeyboardUser] = useState(false);
const [isFocused, setFocus] = useState(false);

const isChecked = selectedValue === value;

const radioCardId = `radio-card-${useId()}`;
useEffect(() => {
const handleMouseDown = () => setKeyboardUser(false);
const handleKeyDown = (event: KeyboardEvent) => {
if (event.key === " ") {
setFocus(false);
} else {
setKeyboardUser(true);
}
};

window.addEventListener("mousedown", handleMouseDown);
window.addEventListener("keydown", handleKeyDown);

return () => {
window.removeEventListener("mousedown", handleMouseDown);
window.removeEventListener("keydown", handleKeyDown);
};
}, []);

useEffect(() => {
if (isChecked && typeof ref !== "function" && ref?.current) {
ref.current.focus();
if (isKeyboardUser && isChecked) {
setFocus(true);
} else {
setFocus(false);
}
}, [isChecked]);
}, [isKeyboardUser, isChecked]);

const handleKeyDown = (event: React.KeyboardEvent<HTMLDivElement>) => {
if (event.key === "Enter" || event.key === " ") {
onChange(value);
}
if (
event.key === "ArrowRight" ||
event.key === "ArrowDown" ||
event.key === "ArrowLeft" ||
event.key === "ArrowUp"
) {
const nextRadioCard = event.currentTarget
.nextElementSibling as HTMLElement;
if (nextRadioCard) {
nextRadioCard.focus();
}
}
};
const inputId = `radio-card-${useId()}`;

return (
<Box as="label" aria-label={String(children)} onKeyDown={handleKeyDown}>
<Box
onFocus={() => isKeyboardUser && setFocus(true)}
onBlur={() => setFocus(false)}
>
<chakra.input
type="radio"
id={radioCardId}
ref={ref}
value={value}
id={inputId}
name={name}
ref={ref}
checked={isChecked}
onChange={() => onChange(value)}
disabled={isDisabled}
__css={styles.radioInput}
/>
<Box
{...props}
tabIndex={0}
ref={ref}
role="radio"
as="label"
name={name}
htmlFor={inputId}
aria-checked={isChecked}
aria-labelledby={radioCardId}
__css={{ ...styles.container, ...(isChecked && styles.checked) }}
data-checked={isChecked}
data-disabled={isDisabled}
{...props}
__css={{
...styles.container,
...(isChecked && styles.checked),
...(isFocused && !isChecked && styles.focused),
...(isChecked && isFocused && styles.focusedChecked),
}}
>
{children}
</Box>
Expand Down
11 changes: 2 additions & 9 deletions packages/spor-react/src/layout/RadioCardGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { BoxProps, Stack } from "@chakra-ui/react";
import React, { useState } from "react";
import { FormLabel } from "../input";
import { RadioCard } from "./RadioCard";

/**
* RadioCardGroupContext is used to pass down the state and handlers to the RadioCard components.
Expand All @@ -15,6 +14,7 @@ type RadioGroupContextProps = {
onChange: (value: string) => void;
variant?: "base" | "floating";
defaultValue?: string;
value?: string;
};

export const RadioCardGroupContext =
Expand Down Expand Up @@ -59,14 +59,7 @@ export const RadioCardGroup: React.FC<RadioCardGroupProps> = ({
defaultValue: defaultValue || "",
}}
>
<Stack
as="fieldset"
direction={direction}
aria-labelledby={groupLabel || name}
role="radiogroup"
tabIndex={0}
{...props}
>
<Stack as="fieldset" direction={direction} {...props}>
{groupLabel && (
<FormLabel as="legend" id={groupLabel}>
{groupLabel}
Expand Down
62 changes: 41 additions & 21 deletions packages/spor-react/src/theme/components/radio-card.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,29 @@
import { createMultiStyleConfigHelpers } from "@chakra-ui/react";
import { baseBackground, baseBorder, baseText } from "../utils/base-utils";
import { floatingBackground, floatingBorder } from "../utils/floating-utils";
import { focusVisibleStyles } from "../utils/focus-utils";
import { anatomy, mode } from "@chakra-ui/theme-tools";
import { anatomy } from "@chakra-ui/theme-tools";
import { outlineBorder } from "../utils/outline-utils";

const parts = anatomy("radio-card").parts("container", "checked", "radioInput");
const parts = anatomy("radio-card").parts(
"container",
"checked",
"radioInput",
"focused",
"focusedChecked",
);
const helpers = createMultiStyleConfigHelpers(parts.keys);

const config = helpers.defineMultiStyleConfig({
baseStyle: (props: any) => ({
container: {
border: "none",
overflow: "hidden",
fontSize: "inherit",
display: "block",
cursor: "pointer",
borderRadius: "sm",
...focusVisibleStyles(props),
transitionProperty: "common",
transitionDuration: "fast",

_disabled: {
pointerEvents: "none",
...baseBackground("disabled", props),
Expand Down Expand Up @@ -53,9 +57,6 @@ const config = helpers.defineMultiStyleConfig({
...baseBackground("active", props),
...baseBorder("active", props),
},
_focus: {
...outlineBorder("focus", props),
},
},
checked: {
_hover: {
Expand All @@ -65,11 +66,22 @@ const config = helpers.defineMultiStyleConfig({
...baseBackground("active", props),
...baseBorder("active", props),
},
_focus: {
outline: "4px solid",
outlineStyle: "double",
...outlineBorder("focus", props),
outlineOffset: "-1px",
},
focusedChecked: {
outline: "4px solid",
outlineStyle: "double",
...outlineBorder("focus", props),
outlineOffset: "-1px",
},
focused: {
outline: "2px solid",
...outlineBorder("focus", props),
outlineOffset: "1px",
boxShadow: `inset 0 0 0 1px rgba(0, 0, 0, 0.40)`,
_hover: {
...baseBorder("hover", props),
boxShadow: "none",
outlineOffset: "0px",
},
},
}),
Expand All @@ -87,9 +99,6 @@ const config = helpers.defineMultiStyleConfig({
...floatingBackground("active", props),
...floatingBorder("active", props),
},
_focus: {
...outlineBorder("focus", props),
},
},
checked: {
_hover: {
Expand All @@ -100,11 +109,22 @@ const config = helpers.defineMultiStyleConfig({
...floatingBackground("active", props),
...floatingBorder("active", props),
},
_focus: {
outline: "4px solid",
outlineStyle: "double",
...outlineBorder("focus", props),
outlineOffset: "-1px",
},
focusedChecked: {
outline: "4px solid",
outlineStyle: "double",
...outlineBorder("focus", props),
outlineOffset: "-1px",
},
focused: {
outline: "2px solid",
...outlineBorder("focus", props),
outlineOffset: "1px",
boxShadow: `inset 0 0 0 1px rgba(0, 0, 0, 0.10)`,
_hover: {
...floatingBorder("hover", props),
boxShadow: "md",
outlineOffset: "0px",
},
},
}),
Expand Down

0 comments on commit 4a546dd

Please sign in to comment.