Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2024-05-08 - TabBar and SaveRow Accessibility
**Learning:** Dynamic status messages in UI components (like success/error text in save bars) should be wrapped in an `aria-live="polite"` region with `role="status"` to ensure screen readers announce updates dynamically. Tab bars need explicit `role="tablist"` and `role="tab"` with `aria-selected` tracking the active state.
**Action:** When implementing custom interactive components like tabs or dynamic save notifications, always verify standard ARIA roles and live regions are included.
13 changes: 8 additions & 5 deletions src/components/ui/SaveRow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,17 +18,20 @@ export default function SaveRow({
const isError = message?.toLowerCase().includes('erreur');
return (
<div class="pt-4 flex items-center justify-between gap-4 border-t border-gray-200 flex-wrap">
{message ? (
<span class={`text-sm font-medium ${isError ? 'text-red-500' : 'text-green-600'}`}>{message}</span>
) : (
<span />
)}
<div aria-live="polite" role="status">
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve the accessibility of the status message, consider adding aria-atomic="true" so that the entire message is announced by screen readers when it updates. Additionally, while role="status" is appropriate for general updates, using role="alert" for error messages ensures they are announced assertively to the user. Note that role="status" has an implicit aria-live="polite", so that attribute can be omitted if the role is static.

Suggested change
<div aria-live="polite" role="status">
<div role={isError ? "alert" : "status"} aria-atomic="true">

{message ? (
<span class={`text-sm font-medium ${isError ? 'text-red-500' : 'text-green-600'}`}>{message}</span>
) : (
<span />
)}
</div>
<div class="flex gap-2">
{extraActions}
<button
type="button"
onClick={onSave}
disabled={saving}
aria-busy={saving}
class="px-5 py-2 rounded-full text-sm font-semibold text-white transition-opacity hover:opacity-90 disabled:opacity-50"
style="background:#175B37"
>
Expand Down
4 changes: 3 additions & 1 deletion src/components/ui/TabBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default function TabBar({ tabs, active, onChange, className = '', tone =
: 'flex flex-wrap gap-1 p-1 bg-gray-100 rounded-xl w-fit';

return (
<div class={`${rail} ${className}`}>
<div role="tablist" class={`${rail} ${className}`}>
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Using role="tablist" and role="tab" is a great start for accessibility, but it also triggers expectations for standard keyboard interaction patterns. To be fully compliant with WAI-ARIA standards, the component should handle arrow keys to move focus between tabs and implement a "roving tabindex" (where only the active tab is focusable via the Tab key). Without these behaviors, screen reader users may find the component confusing as it doesn't behave like a standard tab widget. Additionally, a tablist should ideally have an accessible name via aria-label or aria-labelledby.

{tabs.map((tab) => {
const selected = active === tab.id;
const base =
Expand All @@ -32,6 +32,8 @@ export default function TabBar({ tabs, active, onChange, className = '', tone =
return (
<button
key={tab.id}
role="tab"
aria-selected={selected}
type="button"
onClick={() => onChange(tab.id)}
class={`${base} ${sizing} ${selected ? activeCls : idleCls}`}
Expand Down