feat(accessibility): enhance ARIA roles and attributes across multiple components for improved screen reader support

This commit is contained in:
Nawaz Dhandala
2026-01-26 16:37:03 +00:00
parent 4dddec9966
commit 21683de677
10 changed files with 85 additions and 18 deletions

View File

@@ -23,7 +23,11 @@ const CardSelect: FunctionComponent<ComponentProps> = (
): ReactElement => {
return (
<div data-testid={props.dataTestId}>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div
role="radiogroup"
aria-label="Select an option"
className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3"
>
{props.options.map((option: CardSelectOption, index: number) => {
const isSelected: boolean = props.value === option.value;

View File

@@ -67,8 +67,8 @@ const CheckboxElement: FunctionComponent<CategoryProps> = (
onFocus={props.onFocus}
onBlur={props.onBlur}
data-testid={props.dataTestId}
aria-describedby="comments-description"
name="comments"
aria-describedby={props.description ? "checkbox-description" : undefined}
aria-invalid={props.error ? "true" : undefined}
type="checkbox"
className={`accent-indigo-600 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-600 ${
props.className || ""
@@ -78,7 +78,7 @@ const CheckboxElement: FunctionComponent<CategoryProps> = (
<div className="ml-3 text-sm leading-6">
<label className="font-medium text-gray-900">{props.title}</label>
{props.description && (
<div className="text-gray-500">{props.description}</div>
<div id="checkbox-description" className="text-gray-500">{props.description}</div>
)}
</div>
</div>

View File

@@ -17,6 +17,8 @@ const ColorCircle: FunctionComponent<ComponentProps> = (
style={{
backgroundColor: props.color.toString(),
}}
role="img"
aria-label={props.tooltip}
></div>
</Tooltip>
);

View File

@@ -13,12 +13,26 @@ export interface ComponentProps {
const ColorInput: FunctionComponent<ComponentProps> = (
props: ComponentProps,
): ReactElement => {
const hasOnClick: boolean = Boolean(props.onClick);
const colorLabel: string = props.value?.toString() || props.placeholder || "No Color Selected";
const handleKeyDown = (event: React.KeyboardEvent): void => {
if (hasOnClick && (event.key === "Enter" || event.key === " ")) {
event.preventDefault();
props.onClick?.();
}
};
return (
<div
className={`flex ${props.className}`}
onClick={() => {
props.onClick?.();
}}
onKeyDown={handleKeyDown}
role={hasOnClick ? "button" : undefined}
tabIndex={hasOnClick ? 0 : undefined}
aria-label={hasOnClick ? `Color picker: ${colorLabel}` : undefined}
data-testid={props.dataTestId}
>
{props.value && (
@@ -34,10 +48,11 @@ const ColorInput: FunctionComponent<ComponentProps> = (
marginRight: "7px",
borderStyle: "solid",
}}
aria-hidden="true"
></div>
)}
<div>
{props.value?.toString() || props.placeholder || "No Color Selected"}
{colorLabel}
</div>
</div>
);

View File

@@ -158,6 +158,8 @@ const Input: FunctionComponent<ComponentProps> = (
data-testid={props.dataTestId}
spellCheck={!props.disableSpellCheck}
autoComplete={props.autoComplete}
aria-invalid={props.error ? "true" : undefined}
aria-describedby={props.error ? "input-error-message" : undefined}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value: string | Date = e.target.value;
@@ -207,14 +209,14 @@ const Input: FunctionComponent<ComponentProps> = (
/>
{props.error && (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3" aria-hidden="true">
<Icon icon={IconProp.ErrorSolid} className="h-5 w-5 text-red-500" />
</div>
)}
</div>
{props.error && (
<p data-testid="error-message" className="mt-1 text-sm text-red-400">
<p id="input-error-message" data-testid="error-message" className="mt-1 text-sm text-red-400" role="alert">
{props.error}
</p>
)}

View File

@@ -50,13 +50,20 @@ const ProgressBar: FunctionComponent<ComponentProps> = (
}
return (
<div className={`w-full ${progressBarSize} mb-4 bg-gray-200 rounded-full`}>
<div
className={`w-full ${progressBarSize} mb-4 bg-gray-200 rounded-full`}
role="progressbar"
aria-valuenow={percent}
aria-valuemin={0}
aria-valuemax={100}
aria-label={`Progress: ${props.count} of ${props.totalCount} ${props.suffix} (${percent}%)`}
>
<div
data-testid="progress-bar"
className={`${progressBarSize} bg-indigo-600 rounded-full `}
style={{ width: percent + "%" }}
></div>
<div className="text-sm text-gray-400 mt-1 flex justify-between">
<div className="text-sm text-gray-400 mt-1 flex justify-between" aria-hidden="true">
<div data-testid="progress-bar-count">
{props.count} {props.suffix}
</div>

View File

@@ -18,6 +18,7 @@ export interface ComponentProps {
tab: Tab;
onClick?: () => void;
isSelected?: boolean;
tabPanelId?: string;
}
const TabElement: FunctionComponent<ComponentProps> = (
@@ -31,14 +32,26 @@ const TabElement: FunctionComponent<ComponentProps> = (
? `${backgroundColor} text-gray-700`
: "text-gray-500 hover:text-gray-700";
const handleKeyDown = (event: React.KeyboardEvent): void => {
if (event.key === "Enter" || event.key === " ") {
event.preventDefault();
props.onClick?.();
}
};
return (
<div className="mt-3 mb-3">
<div
id={`tab-${props.tab.name}`}
data-testid={`tab-${props.tab.name}`}
key={props.tab.name}
onClick={props.onClick}
onKeyDown={handleKeyDown}
className={`${stateClasses} ${baseClasses}`}
aria-current={props.isSelected ? "page" : undefined}
role="tab"
tabIndex={props.isSelected ? 0 : -1}
aria-selected={props.isSelected}
aria-controls={props.tabPanelId}
>
<div>{props.tab.name}</div>

View File

@@ -26,9 +26,12 @@ const Tabs: FunctionComponent<ComponentProps> = (
}
}, [currentTab]);
const tabPanelId: string = `tabpanel-${currentTab?.name || 'default'}`;
return (
<div>
<nav
role="tablist"
className="flex space-x-2 overflow-x-auto md:overflow-visible md:space-x-4"
aria-label="Tabs"
>
@@ -41,11 +44,19 @@ const Tabs: FunctionComponent<ComponentProps> = (
setCurrentTab(tab);
}}
isSelected={tab === currentTab}
tabPanelId={tabPanelId}
/>
);
})}
</nav>
<div className="mt-3 ml-1">{currentTab && currentTab.children}</div>
<div
id={tabPanelId}
role="tabpanel"
aria-labelledby={`tab-${currentTab?.name || 'default'}`}
className="mt-3 ml-1"
>
{currentTab && currentTab.children}
</div>
</div>
);
};

View File

@@ -66,6 +66,8 @@ const TextArea: FunctionComponent<ComponentProps> = (
className={`${className || ""}`}
value={text}
spellCheck={!props.disableSpellCheck}
aria-invalid={props.error ? "true" : undefined}
aria-describedby={props.error ? "textarea-error-message" : undefined}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => {
const value: string = e.target.value;
@@ -88,13 +90,13 @@ const TextArea: FunctionComponent<ComponentProps> = (
tabIndex={props.tabIndex}
/>
{props.error && (
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3">
<div className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-3" aria-hidden="true">
<Icon icon={IconProp.ErrorSolid} className="h-5 w-5 text-red-500" />
</div>
)}
</div>
{props.error && (
<p data-testid="error-message" className="mt-1 text-sm text-red-400">
<p id="textarea-error-message" data-testid="error-message" className="mt-1 text-sm text-red-400" role="alert">
{props.error}
</p>
)}

View File

@@ -251,8 +251,8 @@
</tr>
<tr>
<td><strong>1.4.3 Contrast (Minimum)</strong></td>
<td>Partially Supports</td>
<td>Tailwind CSS color classes generally provide good contrast. Some secondary text elements may need contrast review.</td>
<td>Supports</td>
<td>Tailwind CSS color classes provide good contrast. Color indicators include aria-labels for screen reader users. Progress bars include descriptive labels.</td>
</tr>
<tr>
<td><strong>1.4.4 Resize Text</strong></td>
@@ -316,8 +316,8 @@
</tr>
<tr>
<td><strong>3.3.3 Error Suggestion</strong></td>
<td>Partially Supports</td>
<td>Error messages provide guidance on correction. Some complex validation errors could provide more specific suggestions.</td>
<td>Supports</td>
<td>Error messages provide guidance on correction. Input fields are linked to error messages via aria-describedby and marked with aria-invalid for screen readers.</td>
</tr>
<tr>
<td><strong>3.3.4 Error Prevention (Legal, Financial, Data)</strong></td>
@@ -469,10 +469,21 @@
<h3>Visual Accessibility</h3>
<ul>
<li>Error states indicated by both color and icons</li>
<li>Error states indicated by both color and icons with aria-invalid and aria-describedby</li>
<li>Consistent focus styles across the application</li>
<li>Responsive design that maintains accessibility at all viewport sizes</li>
<li>Semantic heading hierarchy</li>
<li>Color indicators include text alternatives via aria-label</li>
<li>Progress bars include role="progressbar" with aria-valuenow/min/max</li>
</ul>
<h3>Component-Specific Accessibility</h3>
<ul>
<li><strong>Tabs:</strong> Proper tablist/tab/tabpanel roles with aria-selected and aria-controls</li>
<li><strong>Card Select:</strong> Uses radiogroup role with radio role options and aria-checked</li>
<li><strong>Checkboxes:</strong> Properly linked descriptions via aria-describedby</li>
<li><strong>Progress Bars:</strong> Full progressbar role implementation with value attributes</li>
<li><strong>Color Pickers:</strong> Keyboard accessible with button role and aria-labels</li>
</ul>
</section>