claw-code/.guardrails/examples/flutter/cross-platform/accessibility_wrapper.dart

116 lines
3.3 KiB
Dart

/// Accessibility Wrapper
///
/// Enforces WCAG 3.0+ accessibility on all wrapped widgets.
/// AI agents: wrap generated UI components with these helpers
/// to guarantee accessibility compliance.
library;
import 'package:flutter/material.dart';
/// Wraps any widget to enforce minimum touch target size
/// Per WCAG 3.0+ and platform guidelines: 44x44 logical pixels minimum
class AccessibleTouchTarget extends StatelessWidget {
final Widget child;
final String semanticLabel;
static const double minSize = 44.0;
const AccessibleTouchTarget({
super.key,
required this.child,
required this.semanticLabel,
});
@override
Widget build(BuildContext context) {
return Semantics(
label: semanticLabel,
child: ConstrainedBox(
constraints: const BoxConstraints(
minWidth: minSize,
minHeight: minSize,
),
child: child,
),
);
}
}
/// Focus-aware wrapper that provides visible focus indicators
/// Per WCAG 3.0+: 3px outline with 3:1 contrast ratio
class AccessibleFocusWrapper extends StatelessWidget {
final Widget child;
final Color focusColor;
final double focusWidth;
const AccessibleFocusWrapper({
super.key,
required this.child,
this.focusColor = Colors.blue,
this.focusWidth = 3.0,
});
@override
Widget build(BuildContext context) {
return Focus(
child: Builder(
builder: (context) {
final hasFocus = Focus.of(context).hasFocus;
return Container(
decoration: hasFocus
? BoxDecoration(
border: Border.all(
color: focusColor,
width: focusWidth,
),
borderRadius: BorderRadius.circular(4),
)
: null,
child: child,
);
},
),
);
}
}
/// Screen reader announcement helper
/// Use this when state changes need to be announced
class AccessibleAnnouncement {
/// Announce a state change to screen readers
static void announce(BuildContext context, String message) {
SemanticsService.announce(message, TextDirection.ltr);
}
/// Announce a polite update (doesn't interrupt current speech)
static void announcePolite(BuildContext context, String message) {
SemanticsService.announce(message, TextDirection.ltr);
}
}
/// Contrast checker utility
/// Validates that text/background combinations meet WCAG AAA (7:1)
class ContrastChecker {
static const double wcagAAA = 7.0;
static const double wcagAA = 4.5;
/// Calculate contrast ratio between two colors
static double contrastRatio(Color foreground, Color background) {
final fgLuminance = foreground.computeLuminance();
final bgLuminance = background.computeLuminance();
final lighter = fgLuminance > bgLuminance ? fgLuminance : bgLuminance;
final darker = fgLuminance > bgLuminance ? bgLuminance : fgLuminance;
return (lighter + 0.05) / (darker + 0.05);
}
/// Check if a color pair meets WCAG AAA
static bool meetsAAA(Color foreground, Color background) {
return contrastRatio(foreground, background) >= wcagAAA;
}
/// Check if a color pair meets WCAG AA
static bool meetsAA(Color foreground, Color background) {
return contrastRatio(foreground, background) >= wcagAA;
}
}