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
5 changes: 4 additions & 1 deletion components/moderation/MarketStatusBadge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function MarketStatusBadge({ state, className, showTooltip = true }: Mark
removed: 'neutral',
resolving: 'info',
};

const badge = (
<Badge
role="status"
Expand All @@ -45,7 +46,9 @@ export function MarketStatusBadge({ state, className, showTooltip = true }: Mark
size="md"
className={cn(
config.badgeClass,
isResolving && 'animate-status-live-pulse',
// motion-safe: ensures the pulse is suppressed for users who have
// enabled "reduce motion" in their OS — required for WCAG 2.1 AA §2.3.3.
isResolving && 'motion-safe:animate-status-live-pulse',
className
)}
>
Expand Down
12 changes: 8 additions & 4 deletions components/moderation/__tests__/MarketStatusBadge.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@ import { render, screen } from '@testing-library/react';
import { MarketStatusBadge } from '../MarketStatusBadge';
import type { ModerationState } from '@/types/moderation';

// The animation class uses the Tailwind `motion-safe:` variant so the pulse is
// suppressed automatically when the user has enabled "prefers-reduced-motion".
const PULSE_CLASS = 'motion-safe:animate-status-live-pulse';

describe('MarketStatusBadge', () => {
it('renders the resolving badge with a live glow-pulse class', () => {
render(<MarketStatusBadge state="resolving" showTooltip={false} />);

const badge = screen.getByRole('status');
expect(badge).toHaveClass('animate-status-live-pulse');
expect(badge).toHaveClass(PULSE_CLASS);
});

it('announces "Resolving now" in the aria-label for the resolving state', () => {
Expand All @@ -23,7 +27,7 @@ describe('MarketStatusBadge', () => {
render(<MarketStatusBadge state={state} showTooltip={false} />);

const badge = screen.getByRole('status');
expect(badge).not.toHaveClass('animate-status-live-pulse');
expect(badge).not.toHaveClass(PULSE_CLASS);
expect(badge.getAttribute('aria-label')).not.toContain('Resolving now');
}
);
Expand All @@ -32,12 +36,12 @@ describe('MarketStatusBadge', () => {
const { rerender } = render(<MarketStatusBadge state="resolving" showTooltip={false} />);

let badge = screen.getByRole('status');
expect(badge).toHaveClass('animate-status-live-pulse');
expect(badge).toHaveClass(PULSE_CLASS);

rerender(<MarketStatusBadge state="removed" showTooltip={false} />);

badge = screen.getByRole('status');
expect(badge).not.toHaveClass('animate-status-live-pulse');
expect(badge).not.toHaveClass(PULSE_CLASS);
expect(badge.getAttribute('aria-label')).not.toContain('Resolving now');
});
});
14 changes: 12 additions & 2 deletions tailwind.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,21 @@ const config: Config = {
to: {
height: '0'
}
}
},
// Subtle glow-pulse used by MarketStatusBadge on the `resolving` state.
// Respects prefers-reduced-motion via the Tailwind motion-safe variant.
'status-live-pulse': {
'0%, 100%': { opacity: '1', boxShadow: '0 0 0 0 transparent' },
'50%': { opacity: '0.75', boxShadow: '0 0 0 4px hsl(var(--ring) / 0.35)' },
},
},
animation: {
'accordion-down': 'accordion-down 0.2s ease-out',
'accordion-up': 'accordion-up 0.2s ease-out'
'accordion-up': 'accordion-up 0.2s ease-out',
// 2 s loop, ease-in-out — intentionally gentle so it doesn't distract.
// The `motion-safe:` prefix in the component ensures it is skipped when
// the user has "reduce motion" enabled in their OS accessibility settings.
'status-live-pulse': 'status-live-pulse 2s ease-in-out infinite',
}
}
},
Expand Down